Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/heavy-hairs-hover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"bits-ui": minor
---

breaking(Command): rename `Command.Item`'s transient `data-selected` attribute to `data-highlighted` to prevent Chromium `:has([data-selected])` invalidation collisions with Calendar/Select persistent selection (#2044) — migrate `[data-selected]`/`data-selected:` styles on Command items to `data-highlighted`; `aria-selected` is unchanged
12 changes: 6 additions & 6 deletions docs/src/lib/components/demos/command-demo-dialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,21 @@
</Command.GroupHeading>
<Command.GroupItems>
<Command.Item
class="rounded-button data-selected:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
class="rounded-button data-highlighted:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
keywords={["getting started", "tutorial"]}
>
<Sticker class="size-4" />
Introduction
</Command.Item>
<Command.Item
class="rounded-button data-selected:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
class="rounded-button data-highlighted:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
keywords={["child", "custom element", "snippets"]}
>
<CodeBlock class="size-4 " />
Delegation
</Command.Item>
<Command.Item
class="rounded-button data-selected:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
class="rounded-button data-highlighted:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
keywords={["css", "theme", "colors", "fonts", "tailwind"]}
>
<Palette class="size-4" />
Expand All @@ -92,21 +92,21 @@
</Command.GroupHeading>
<Command.GroupItems>
<Command.Item
class="rounded-button data-selected:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
class="rounded-button data-highlighted:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
keywords={["dates", "times"]}
>
<CalendarBlank class="size-4" />
Calendar
</Command.Item>
<Command.Item
class="rounded-button data-selected:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
class="rounded-button data-highlighted:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
keywords={["buttons", "forms"]}
>
<RadioButton class="size-4" />
Radio Group
</Command.Item>
<Command.Item
class="rounded-button data-selected:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
class="rounded-button data-highlighted:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
keywords={["inputs", "text", "autocomplete"]}
>
<Textbox class="size-4" />
Expand Down
12 changes: 6 additions & 6 deletions docs/src/lib/components/demos/command-demo.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,21 @@
</Command.GroupHeading>
<Command.GroupItems>
<Command.Item
class="rounded-button data-selected:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
class="rounded-button data-highlighted:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
keywords={["getting started", "tutorial"]}
>
<Sticker class="size-4" />
Introduction
</Command.Item>
<Command.Item
class="rounded-button data-selected:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
class="rounded-button data-highlighted:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
keywords={["child", "custom element", "snippets"]}
>
<CodeBlock class="size-4 " />
Delegation
</Command.Item>
<Command.Item
class="rounded-button data-selected:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
class="rounded-button data-highlighted:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
keywords={["css", "theme", "colors", "fonts", "tailwind"]}
>
<Palette class="size-4" />
Expand All @@ -57,21 +57,21 @@
</Command.GroupHeading>
<Command.GroupItems>
<Command.Item
class="rounded-button data-selected:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
class="rounded-button data-highlighted:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
keywords={["dates", "times"]}
>
<CalendarBlank class="size-4" />
Calendar
</Command.Item>
<Command.Item
class="rounded-button data-selected:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
class="rounded-button data-highlighted:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
keywords={["buttons", "forms"]}
>
<RadioButton class="size-4" />
Radio Group
</Command.Item>
<Command.Item
class="rounded-button data-selected:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
class="rounded-button data-highlighted:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
keywords={["inputs", "text", "autocomplete"]}
>
<Textbox class="size-4" />
Expand Down
4 changes: 2 additions & 2 deletions docs/src/lib/components/demos/command-grid-demo.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@
<Command.GroupItems class="grid grid-cols-8 gap-2 px-2">
{#each group.items as groupItem (groupItem)}
<Command.Item
class="rounded-button bg-muted data-selected:ring-foreground outline-hidden flex aspect-square size-full cursor-pointer select-none items-center justify-center text-2xl ring-2 ring-transparent aria-disabled:cursor-not-allowed aria-disabled:opacity-50"
class="rounded-button bg-muted data-highlighted:ring-foreground outline-hidden flex aspect-square size-full cursor-pointer select-none items-center justify-center text-2xl ring-2 ring-transparent aria-disabled:cursor-not-allowed aria-disabled:opacity-50"
keywords={groupItem.keywords}
disabled={groupItem.disabled}
>
Expand Down Expand Up @@ -216,7 +216,7 @@
<Command.GroupItems>
{#each group.items as groupItem (groupItem)}
<Command.Item
class="rounded-button data-selected:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
class="rounded-button data-highlighted:bg-muted outline-hidden flex h-10 cursor-pointer select-none items-center gap-2 px-3 py-2.5 text-sm capitalize"
keywords={groupItem.keywords}
disabled={groupItem.disabled}
onSelect={groupItem.action}
Expand Down
2 changes: 1 addition & 1 deletion docs/src/lib/components/examples/command/framer/framer.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
transition: all 150ms ease;
transition-property: none;

&[data-selected] {
&[data-highlighted] {
background: var(--blue9);
color: #ffffff;

Expand Down
2 changes: 1 addition & 1 deletion docs/src/lib/components/examples/command/linear/linear.css
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
transition-property: none;
position: relative;

&[data-selected] {
&[data-highlighted] {
background: var(--gray3);

svg {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@
transition: all 150ms ease;
transition-property: none;

&[data-selected] {
&[data-highlighted] {
background: var(--gray4);
color: var(--gray12);
}
Expand Down
2 changes: 1 addition & 1 deletion docs/src/lib/components/examples/command/vercel/vercel.css
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
transition: all 150ms ease;
transition-property: none;

&[data-selected] {
&[data-highlighted] {
background: var(--grayA3);
color: var(--gray12);
}
Expand Down
2 changes: 1 addition & 1 deletion docs/src/lib/components/search.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@
{#each results as { title, href, snippet, category } (title + href)}
<Command.LinkItem
{href}
class="rounded-button data-selected:bg-muted outline-hidden flex cursor-pointer select-none flex-col items-start gap-1 px-3 py-2.5 text-sm"
class="rounded-button data-highlighted:bg-muted outline-hidden flex cursor-pointer select-none flex-col items-start gap-1 px-3 py-2.5 text-sm"
onSelect={() => {
searchQuery = "";
open = false;
Expand Down
4 changes: 2 additions & 2 deletions docs/src/lib/content/api-reference/command.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,8 @@ const item = defineComponentApiSchema<CommandItemPropsWithoutHTML>({
description: "Present when the item is disabled.",
}),
defineSimpleDataAttr({
name: "selected",
description: "Present when the item is selected.",
name: "highlighted",
description: "Present when the item is highlighted, via keyboard navigation or pointer hover.",
}),
defineSimpleDataAttr({
name: "command-item",
Expand Down
2 changes: 1 addition & 1 deletion docs/src/lib/styles/command/command.css
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@
height: 14px;
}

&[data-selected] {
&[data-highlighted] {
color: var(--gray12);

&:hover .activeTheme {
Expand Down
4 changes: 2 additions & 2 deletions packages/bits-ui/src/lib/bits/command/command.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ export class CommandRootState {
const node = this.opts.ref.current;
if (!node) return;
const selectedNode = node.querySelector<HTMLElement>(
`${COMMAND_VALID_ITEM_SELECTOR}[data-selected]`
`${COMMAND_VALID_ITEM_SELECTOR}[data-highlighted]`
);
if (!selectedNode) return;
return selectedNode;
Expand Down Expand Up @@ -1477,7 +1477,7 @@ export class CommandItemState {
"aria-disabled": boolToStr(this.opts.disabled.current),
"aria-selected": boolToStr(this.isSelected),
"data-disabled": boolToEmptyStrOrUndef(this.opts.disabled.current),
"data-selected": boolToEmptyStrOrUndef(this.isSelected),
"data-highlighted": boolToEmptyStrOrUndef(this.isSelected),
"data-value": this.trueValue,
"data-group": this.#group?.trueValue,
[commandAttrs.item]: "",
Expand Down
20 changes: 10 additions & 10 deletions tests/src/tests/command/command-grid.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ function setup(props: Partial<ComponentProps<typeof CommandGridTest>> = {}) {
it("should select the first item by default", async () => {
setup();

await expect.element(page.getByText("Introduction")).toHaveAttribute("data-selected");
await expect.element(page.getByText("Introduction")).toHaveAttribute("data-highlighted");
});

it("should allow forcing the selected value", async () => {
setup({ value: "Introduction" });
await expect.element(page.getByText("Introduction")).toHaveAttribute("data-selected");
await expect.element(page.getByText("Introduction")).toHaveAttribute("data-highlighted");
});

it("should render the separator when search is empty and remove it when search is not empty", async () => {
Expand Down Expand Up @@ -74,13 +74,13 @@ it("should restore original order when search is cleared", async () => {
input.focus();
await userEvent.keyboard("d");
await expect.element(t.input).toHaveValue("d");
await expect.element(page.getByText("Delegation")).toHaveAttribute("data-selected");
await expect.element(page.getByText("Delegation")).toHaveAttribute("data-highlighted");
await expect
.element(page.getByTestId("group-a-items").element().children[0])
.toHaveTextContent("Delegation");
await userEvent.keyboard(kbd.BACKSPACE);
await expect.element(t.input).toHaveValue("");
await expect.element(page.getByText("Introduction")).toHaveAttribute("data-selected");
await expect.element(page.getByText("Introduction")).toHaveAttribute("data-highlighted");
await expect
.element(page.getByTestId("group-a-items").element().children[0])
.toHaveTextContent("Introduction");
Expand All @@ -92,18 +92,18 @@ it("should hide the group if all items are filtered out", async () => {
await userEvent.type(t.input.element(), "radio");
await expect.element(page.getByTestId("group-a")).not.toBeVisible();
await expect.element(page.getByTestId("group-b")).toBeVisible();
await expect.element(page.getByText("Radio Group")).toHaveAttribute("data-selected");
await expect.element(page.getByText("Radio Group")).toHaveAttribute("data-highlighted");
});

it("should navigate to the correct column in the next row", async () => {
const t = setup();
await expect.element(page.getByText("Introduction")).toHaveAttribute("data-selected");
await expect.element(page.getByText("Introduction")).toHaveAttribute("data-highlighted");
await userEvent.type(t.input.element(), kbd.ARROW_DOWN);
await expect.element(page.getByText("Calendar")).toHaveAttribute("data-selected");
await expect.element(page.getByText("Calendar")).toHaveAttribute("data-highlighted");
await userEvent.type(t.input.element(), kbd.ARROW_UP);
await expect.element(page.getByText("Introduction")).toHaveAttribute("data-selected");
await expect.element(page.getByText("Introduction")).toHaveAttribute("data-highlighted");
await userEvent.type(t.input.element(), kbd.ARROW_RIGHT);
await expect.element(page.getByText("Delegation")).toHaveAttribute("data-selected");
await expect.element(page.getByText("Delegation")).toHaveAttribute("data-highlighted");
await userEvent.type(t.input.element(), kbd.ARROW_DOWN);
await expect.element(page.getByText("Radio Group")).toHaveAttribute("data-selected");
await expect.element(page.getByText("Radio Group")).toHaveAttribute("data-highlighted");
});
8 changes: 4 additions & 4 deletions tests/src/tests/command/command-scroll.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ it("should scroll initial value into view when it's not the first item", async (
setup({ value: "Popover" });

const item = page.getByText("Popover");
await expect.element(item).toHaveAttribute("data-selected");
await expect.element(item).toHaveAttribute("data-highlighted");

// check that the item is visible in the viewport
const itemElement = item.element() as HTMLElement;
Expand All @@ -45,7 +45,7 @@ it("should scroll initial value in the middle of the list into view", async () =
setup({ value: "Radio Group" });

const item = page.getByText("Radio Group");
await expect.element(item).toHaveAttribute("data-selected");
await expect.element(item).toHaveAttribute("data-highlighted");

const itemElement = item.element() as HTMLElement;
const viewport = page.getByTestId("viewport").element() as HTMLElement;
Expand All @@ -65,7 +65,7 @@ it("should respect disableInitialScroll prop and not scroll", async () => {
setup({ value: "Popover", disableInitialScroll: true });

const item = page.getByText("Popover");
await expect.element(item).toHaveAttribute("data-selected");
await expect.element(item).toHaveAttribute("data-highlighted");

const viewport = page.getByTestId("viewport").element() as HTMLElement;

Expand All @@ -80,7 +80,7 @@ it("should not scroll when initial value is the first item", async () => {
setup({ value: "Introduction" });

const item = page.getByText("Introduction");
await expect.element(item).toHaveAttribute("data-selected");
await expect.element(item).toHaveAttribute("data-highlighted");

const viewport = page.getByTestId("viewport").element() as HTMLElement;

Expand Down
18 changes: 9 additions & 9 deletions tests/src/tests/command/command.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,29 +27,29 @@ it("should select the first item by default", async () => {

// since we aren't hardcoding a value for the item, we need to wait for
// the component to render before we can check if the first item is selected
await expect.element(page.getByText("Introduction")).toHaveAttribute("data-selected");
await expect.element(page.getByText("Introduction")).toHaveAttribute("data-highlighted");
});

it("should allow forcing the selected value", async () => {
setup({ value: "Introduction" });

await expect.element(page.getByText("Introduction")).toHaveAttribute("data-selected");
await expect.element(page.getByText("Introduction")).toHaveAttribute("data-highlighted");
});

it("should respect initial value when it's not the first item", async () => {
setup({ value: "Radio Group" });

// ensure the initial value is selected, not the first item
await expect.element(page.getByText("Radio Group")).toHaveAttribute("data-selected");
await expect.element(page.getByText("Introduction")).not.toHaveAttribute("data-selected");
await expect.element(page.getByText("Radio Group")).toHaveAttribute("data-highlighted");
await expect.element(page.getByText("Introduction")).not.toHaveAttribute("data-highlighted");
});

it("should respect initial value for items in the first group", async () => {
setup({ value: "Styling" });

// ensure the provided value is selected, not the first item
await expect.element(page.getByText("Styling")).toHaveAttribute("data-selected");
await expect.element(page.getByText("Introduction")).not.toHaveAttribute("data-selected");
await expect.element(page.getByText("Styling")).toHaveAttribute("data-highlighted");
await expect.element(page.getByText("Introduction")).not.toHaveAttribute("data-highlighted");
});

it("should render the separator when search is empty and remove it when search is not empty", async () => {
Expand Down Expand Up @@ -88,13 +88,13 @@ it("should restore original order when search is cleared", async () => {
(t.input.element() as HTMLElement).focus();
await userEvent.keyboard("d");
await expect.element(t.input).toHaveValue("d");
await expect.element(page.getByText("Delegation")).toHaveAttribute("data-selected");
await expect.element(page.getByText("Delegation")).toHaveAttribute("data-highlighted");
await expect
.element(page.getByTestId("group-a-items").element().children[0])
.toHaveTextContent("Delegation");
await userEvent.keyboard(kbd.BACKSPACE);
await expect.element(t.input).toHaveValue("");
await expect.element(page.getByText("Introduction")).toHaveAttribute("data-selected");
await expect.element(page.getByText("Introduction")).toHaveAttribute("data-highlighted");
await expect
.element(page.getByTestId("group-a-items").element().children[0])
.toHaveTextContent("Introduction");
Expand All @@ -106,5 +106,5 @@ it("should hide the group if all items are filtered out", async () => {
await userEvent.type(t.input, "radio");
await expect.element(page.getByTestId("group-a")).not.toBeVisible();
await expect.element(page.getByTestId("group-b")).toBeVisible();
await expect.element(page.getByText("Radio Group")).toHaveAttribute("data-selected");
await expect.element(page.getByText("Radio Group")).toHaveAttribute("data-highlighted");
});
Loading