Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
fb8e4e0
refactor: update popover component structure and enhance accessibility
thejackshelton-kunaico Nov 15, 2025
12c2e2a
feat: add Select component and enhance Popover integration
thejackshelton-kunaico Nov 16, 2025
8f7271e
feat: enhance carousel component with utility functions and improve d…
thejackshelton-kunaico Nov 16, 2025
29bef40
feat: enhance Select component with improved item management and keyb…
thejackshelton-kunaico Nov 16, 2025
da8899a
feat: add SelectItem export and refactor Select component state manag…
thejackshelton-kunaico Nov 16, 2025
fc4fe22
feat: refactor CarouselItem and SelectItem to utilize registerItem ut…
thejackshelton-kunaico Nov 16, 2025
36a61aa
refactor: improve keyboard interaction and state management in Select…
thejackshelton-kunaico Nov 16, 2025
ac7d83c
feat: enhance SelectContent with improved focus management on toggle
thejackshelton-kunaico Nov 16, 2025
b36776d
refactor: streamline keyboard handling in SelectTrigger component
thejackshelton-kunaico Nov 16, 2025
f5ca1b5
feat: enhance Select component with item labeling and styling improve…
thejackshelton-kunaico Nov 17, 2025
cc1f22d
feat: enhance Select component with initial value handling and improv…
thejackshelton-kunaico Nov 17, 2025
2027206
feat: enhance Select component with multiple selections and improved …
thejackshelton-kunaico Nov 17, 2025
b8c4ef5
fix: update default selected value in basic example and clean up unus…
thejackshelton-kunaico Nov 17, 2025
8a5f63d
feat: enhance Select component with distinct value handling and examp…
thejackshelton-kunaico Nov 18, 2025
d3ed3fc
feat: update Select component example and enhance value display logic
thejackshelton-kunaico Nov 18, 2025
13de879
feat: enhance Select component with placeholder support and example u…
thejackshelton-kunaico Nov 20, 2025
63b307b
feat: enhance Select component with dynamic rendering and improved ex…
thejackshelton-kunaico Nov 20, 2025
851a350
fix: update default selected value and improve Select component example
thejackshelton-kunaico Nov 20, 2025
47bad6a
feat: add conditional rendering to Select component with initial valu…
thejackshelton-kunaico Nov 20, 2025
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
2 changes: 1 addition & 1 deletion docs/src/docs-widgets/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ const DesktopNav = component$(() => {
class="absolute top-15 h-10 w-full z-99999 cursor-pointer"
/>
<Navbar.ItemTrigger
class="w-fit flex items-center gap-2 group [&[ui-open]]:text-blue-600 transition-colors duration-200 px-5 h-[76px]"
class="w-fit flex items-center gap-2 group ui-open:text-blue-600 transition-colors duration-200 px-5 h-[76px]"
ui-mega-collapsible
>
<span>{item.label}</span>
Expand Down
11 changes: 11 additions & 0 deletions docs/src/routes/components/select/examples/basic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Select } from "@qds.dev/ui";
import { component$ } from "@qwik.dev/core";

export default component$(() => {
return (
<Select.Root>
<Select.Trigger>Trigger</Select.Trigger>
<Select.Content>Content</Select.Content>
</Select.Root>
);
});
5 changes: 5 additions & 0 deletions docs/src/routes/components/select/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Basic from "./examples/basic";

# Select

<Basic />
1 change: 1 addition & 0 deletions libs/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export * as RadioGroup from "./radio-group";
export { Render } from "./render/render";
export * as Resizable from "./resizable";
export * as ScrollArea from "./scroll-area";
export * as Select from "./select";
export * as Slider from "./slider";
export * as Switch from "./switch";
export * as Tabs from "./tabs";
Expand Down
2 changes: 0 additions & 2 deletions libs/components/src/popover/anchor-logic.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@

[ui-qds-popover-trigger] {
anchor-name: --qds-popover;
width: 10em;
}

[ui-qds-popover-content] {
/* biome-ignore lint/correctness/noUnknownProperty: <explanation> */
position-anchor: --qds-popover;
width: anchor-size(width);
margin: unset;
top: anchor(bottom);
left: anchor(inside);
Expand Down
11 changes: 5 additions & 6 deletions libs/components/src/popover/popover-content.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { $, type PropsOf, Slot, component$, useContext } from "@qwik.dev/core";
import type { CorrectedToggleEvent } from "@qwik.dev/core/internal";
import { $, component$, type PropsOf, Slot, useContext } from "@qwik.dev/core";
import { Render } from "../render/render";
import { popoverContextId } from "./popover-root";

export const PopoverContent = component$((props: PropsOf<"div">) => {
const context = useContext(popoverContextId);
const panelId = `${context.localId}-panel`;
const contentId = `${context.localId}-content`;

const handleToggle$ = $((e: CorrectedToggleEvent) => {
const handleToggle$ = $((e: ToggleEvent) => {
// prevent InvalidStateError: browser already toggled, skip useTask$ re-execution
context.canExternallyChange.value = false;
context.isOpen.value = e.newState === "open";
Expand All @@ -20,14 +19,14 @@ export const PopoverContent = component$((props: PropsOf<"div">) => {

return (
<Render
{...props}
hidden={context.isHidden.value}
onToggle$={[handleToggle$, props.onToggle$]}
popover="auto"
id={panelId}
id={contentId}
internalRef={context.contentRef}
fallback="div"
ui-qds-popover-content
{...props}
>
<Slot />
</Render>
Expand Down
1 change: 1 addition & 0 deletions libs/components/src/popover/popover-root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ export const PopoverRoot = component$((props: PopoverRootProps) => {
onPointerOut$={handlePointerOut$}
onPointerOver$={handlePointerOver$}
ui-open={isOpen.value}
ui-hover={hover}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Hover Mode: Pointer Events Go Awry

When hover mode is enabled, handlePointerMove$ incorrectly chains with props.onPointerOver$ instead of props.onPointerMove$. This causes any user-provided onPointerMove$ handler to be ignored in hover mode, while incorrectly calling the onPointerOver$ handler on pointer move events.

Fix in Cursor Fix in Web

ui-closed={!isOpen.value}
ui-qds-popover-root
ui-qds-scope
Expand Down
11 changes: 5 additions & 6 deletions libs/components/src/popover/popover-trigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { popoverContextId } from "./popover-root";

export const PopoverTrigger = component$((props: PropsOf<"button">) => {
const context = useContext(popoverContextId);
const panelId = `${context.localId}-panel`;
const contentId = `${context.localId}-content`;

const handleClick = sync$((e: PointerEvent, el: HTMLElement) => {
const isHover = el.getAttribute("ui-hover") === "true";
Expand All @@ -17,15 +17,14 @@ export const PopoverTrigger = component$((props: PropsOf<"button">) => {

return (
<Render
ui-hover={context.hover}
ui-open={context.isOpen.value}
ui-closed={!context.isOpen.value}
{...props}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Hovered Popovers Fail Click Prevention

The handleClick function reads ui-hover from the trigger element, but this attribute was moved to the root element in this PR. The isHover check will always be false, breaking click prevention for hover-enabled popovers. The function should use context.hover from the popover context instead of reading the DOM attribute.

Fix in Cursor Fix in Web

aria-expanded={context.isOpen.value}
internalRef={context.triggerRef}
popovertarget={panelId}
aria-controls={contentId}
popovertarget={contentId}
onClick$={[handleClick, props.onClick$]}
ui-qds-popover-trigger
fallback="button"
{...props}
>
<Slot />
</Render>
Expand Down
40 changes: 40 additions & 0 deletions libs/components/src/popover/popover.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,43 @@ test("trigger and content are connected via id", async () => {

expect(triggerTarget).toBe(contentId);
});

test("trigger aria-controls matches content id", async () => {
render(<Basic />);

await expect.element(Trigger).toBeVisible();
await userEvent.click(Trigger);
await expect.element(Content).toBeVisible();

const triggerElement = Trigger.element();
const contentElement = Content.element();
const contentId = contentElement?.getAttribute("id");
const ariaControls = triggerElement?.getAttribute("aria-controls");

expect(contentId).toBeTruthy();
expect(ariaControls).toBe(contentId);
});

test("trigger aria-expanded is false when closed", async () => {
render(<Basic />);

await expect.element(Trigger).toHaveAttribute("aria-expanded", "false");
});

test("trigger aria-expanded is true when open", async () => {
render(<Basic open />);

await expect.element(Trigger).toHaveAttribute("aria-expanded", "true");
});

test("trigger aria-expanded toggles with popover state", async () => {
render(<Basic />);

await expect.element(Trigger).toHaveAttribute("aria-expanded", "false");

await userEvent.click(Trigger);
await expect.element(Trigger).toHaveAttribute("aria-expanded", "true");

await userEvent.click(Trigger);
await expect.element(Trigger).toHaveAttribute("aria-expanded", "false");
});
3 changes: 3 additions & 0 deletions libs/components/src/select/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { SelectContent as Content } from "./select-content";
export { SelectRoot as Root } from "./select-root";
export { SelectTrigger as Trigger } from "./select-trigger";
12 changes: 12 additions & 0 deletions libs/components/src/select/select-content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { component$, PropsOf, Slot } from "@qwik.dev/core";
import { PopoverContent } from "../popover/popover-content";

type SelectContentProps = PropsOf<typeof PopoverContent>;

export const SelectContent = component$((props: SelectContentProps) => {
return (
<PopoverContent {...props} role="listbox">
<Slot />
</PopoverContent>
);
});
36 changes: 36 additions & 0 deletions libs/components/src/select/select-root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useBindings } from "@qds.dev/utils";
import {
component$,
createContextId,
PropsOf,
Signal,
Slot,
useContextProvider
} from "@qwik.dev/core";
import { PopoverRoot } from "../popover/popover-root";

type SelectRootProps = PropsOf<typeof PopoverRoot>;

type SelectContext = {
isOpen: Signal<boolean>;
};

export const selectContextId = createContextId<SelectContext>("qds-select");

export const SelectRoot = component$((props: SelectRootProps) => {
const { openSig: isOpen } = useBindings(props, {
open: false
});

const context: SelectContext = {
isOpen
};

useContextProvider(selectContextId, context);

return (
<PopoverRoot {...props} bind:open={isOpen}>
<Slot />
</PopoverRoot>
);
});
12 changes: 12 additions & 0 deletions libs/components/src/select/select-trigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { component$, PropsOf, Slot } from "@qwik.dev/core";
import { PopoverTrigger } from "../popover/popover-trigger";

type SelectTriggerProps = PropsOf<typeof PopoverTrigger>;

export const SelectTrigger = component$((props: SelectTriggerProps) => {
return (
<PopoverTrigger {...props} aria-haspopup="listbox">
<Slot />
</PopoverTrigger>
);
});
Loading