diff --git a/docs/src/docs-widgets/header/header.tsx b/docs/src/docs-widgets/header/header.tsx index 583a8d52b..6c38d35a3 100644 --- a/docs/src/docs-widgets/header/header.tsx +++ b/docs/src/docs-widgets/header/header.tsx @@ -159,7 +159,7 @@ const DesktopNav = component$(() => { class="absolute top-15 h-10 w-full z-99999 cursor-pointer" /> {item.label} diff --git a/docs/src/routes/components/select/examples/basic.tsx b/docs/src/routes/components/select/examples/basic.tsx new file mode 100644 index 000000000..8d0cdec55 --- /dev/null +++ b/docs/src/routes/components/select/examples/basic.tsx @@ -0,0 +1,67 @@ +import { Select } from "@qds.dev/ui"; +import { component$, useSignal } from "@qwik.dev/core"; + +export default component$(() => { + const selectedValueTwo = useSignal("Item 2"); + const isRendered = useSignal(false); + + const items = [ + { value: "1", label: "Item 1" }, + { value: "2", label: "Item 2" }, + { value: "3", label: "Item 3" }, + { value: "4", label: "Item 4" }, + { value: "5", label: "Item 5" }, + { value: "6", label: "Item 6" }, + { value: "7", label: "Item 7" } + ]; + + return ( + <> + + + + + + {items.map((item) => ( + + {item.label} + + ))} + + + + {isRendered.value && ( + + + + + + {items.map((item) => ( + + {item.label} + + ))} + + + )} + + ); +}); + +export const CompOne = component$(() => { + const selectedValue = useSignal("5"); + + return ( + + + + + + ); +}); diff --git a/docs/src/routes/components/select/index.mdx b/docs/src/routes/components/select/index.mdx new file mode 100644 index 000000000..8c272a7b2 --- /dev/null +++ b/docs/src/routes/components/select/index.mdx @@ -0,0 +1,5 @@ +import Basic from "./examples/basic"; + +# Select + + \ No newline at end of file diff --git a/docs/src/routes/contributing/research/index.mdx b/docs/src/routes/contributing/research/index.mdx index 113b0f1d7..a9910b8a8 100644 --- a/docs/src/routes/contributing/research/index.mdx +++ b/docs/src/routes/contributing/research/index.mdx @@ -28,10 +28,18 @@ Take a look at the following projects and see if you can find any inspiration. T - [Material Angular CDK](https://material.angular.io/cdk/categories) - [Fritz2](https://www.fritz2.dev/headless/) - [Bits UI](https://bits-ui.com/) +- [Qwik UI](https://qwikui.com/) + +Specific component libraries: +- [Formisch](https://formisch.dev/qwik/guides/introduction/) +- [Downshift](https://www.downshift-js.com/) +- [React Resizable Panels](https://react-resizable-panels.vercel.app/) +- [React Select](https://react-select.com/) #### Not headless but interesting - [Shadcn UI](https://ui.shadcn.com/) +- [Shoelace](https://shoelace.style/) - [Flux UI](https://fluxui.dev/) - [Angular Material](https://material.angular.io/components/categories) diff --git a/libs/components/src/carousel/carousel-item.tsx b/libs/components/src/carousel/carousel-item.tsx index 946ff874d..b18585c7f 100644 --- a/libs/components/src/carousel/carousel-item.tsx +++ b/libs/components/src/carousel/carousel-item.tsx @@ -1,3 +1,4 @@ +import { registerItem } from "@qds.dev/utils"; import { $, component$, @@ -50,20 +51,13 @@ export const CarouselItem = component$((props: CarouselItemProps) => { }); useTask$(function getIndexOrder() { - if (index === undefined) { - throw new Error("QDS: Carousel Item cannot find its proper index."); - } - - context.itemRefsArray.value[index] = itemRef; - - const newValuesArray = [...context.itemValuesArray.value]; - if (newValuesArray.length <= index) { - while (newValuesArray.length <= index) { - newValuesArray.push(String(newValuesArray.length)); - } - } - newValuesArray[index] = itemValue; - context.itemValuesArray.value = newValuesArray; + registerItem({ + index, + itemRef, + itemValue, + itemRefsArray: context.itemRefsArray, + itemValuesArray: context.itemValuesArray + }); }); const handleFocusIn$ = $(() => { diff --git a/libs/components/src/carousel/carousel-nav-trigger.tsx b/libs/components/src/carousel/carousel-nav-trigger.tsx index 598a9aea5..a446b49da 100644 --- a/libs/components/src/carousel/carousel-nav-trigger.tsx +++ b/libs/components/src/carousel/carousel-nav-trigger.tsx @@ -10,9 +10,9 @@ import { useSignal, useTask$ } from "@qwik.dev/core"; +import { getValueFromIndex } from "@qds.dev/utils"; import { Render } from "../render/render"; import { carouselContextId } from "./carousel-root"; -import { getValueFromIndex } from "./carousel-utils"; type NavTriggerProps = PropsOf<"button">; diff --git a/libs/components/src/carousel/carousel-root.tsx b/libs/components/src/carousel/carousel-root.tsx index a8d531537..2955be0b1 100644 --- a/libs/components/src/carousel/carousel-root.tsx +++ b/libs/components/src/carousel/carousel-root.tsx @@ -1,4 +1,9 @@ -import { BindableProps, useBindings } from "@qds.dev/utils"; +import { + BindableProps, + getIndexFromValue, + getValueFromIndex, + useBindings +} from "@qds.dev/utils"; import { component$, createContextId, @@ -13,7 +18,6 @@ import { useTask$ } from "@qwik.dev/core"; import { Render } from "../render/render"; -import { getIndexFromValue, getValueFromIndex } from "./carousel-utils"; import { useAutoplay } from "./hooks/use-autoplay"; export const carouselContextId = createContextId("qui-carousel-context"); diff --git a/libs/components/src/carousel/carousel-scroll-area.tsx b/libs/components/src/carousel/carousel-scroll-area.tsx index 37d85d974..7c4505006 100644 --- a/libs/components/src/carousel/carousel-scroll-area.tsx +++ b/libs/components/src/carousel/carousel-scroll-area.tsx @@ -1,4 +1,4 @@ -import { useResumed$ } from "@qds.dev/utils"; +import { getValueFromIndex, useResumed$ } from "@qds.dev/utils"; import { $, component$, @@ -17,7 +17,6 @@ import styles from "./carousel.css?inline"; import { carouselContextId } from "./carousel-root"; import { getItemPosition, - getValueFromIndex, getVelocityBasedTargetIndex, isSwipeInCorrectDirection, type OrientationProps, diff --git a/libs/components/src/carousel/carousel-utils.ts b/libs/components/src/carousel/carousel-utils.ts index 77db43f6e..3627bf9a2 100644 --- a/libs/components/src/carousel/carousel-utils.ts +++ b/libs/components/src/carousel/carousel-utils.ts @@ -1,24 +1,3 @@ -export function getIndexFromValue(value: string, itemValuesArray: string[]): number { - const index = itemValuesArray.indexOf(value); - if (index !== -1) return index; - - if (itemValuesArray.length === 0) { - const numIndex = parseInt(value, 10); - if (!isNaN(numIndex) && numIndex >= 0) { - return numIndex; - } - } - - return 0; -} - -export function getValueFromIndex(index: number, itemValuesArray: string[]): string { - if (index < 0 || index >= itemValuesArray.length) { - return String(index); - } - return itemValuesArray[index] ?? String(index); -} - export type OrientationProps = { size: "width" | "height"; scroll: "scrollWidth" | "scrollHeight"; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index c9f5d1bd3..14fce8d3d 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -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"; diff --git a/libs/components/src/popover/anchor-logic.css b/libs/components/src/popover/anchor-logic.css index 3bb95bbde..8434ee937 100644 --- a/libs/components/src/popover/anchor-logic.css +++ b/libs/components/src/popover/anchor-logic.css @@ -6,13 +6,11 @@ [ui-qds-popover-trigger] { anchor-name: --qds-popover; - width: 10em; } [ui-qds-popover-content] { /* biome-ignore lint/correctness/noUnknownProperty: */ position-anchor: --qds-popover; - width: anchor-size(width); margin: unset; top: anchor(bottom); left: anchor(inside); diff --git a/libs/components/src/popover/popover-content.tsx b/libs/components/src/popover/popover-content.tsx index 29c320dbe..124da882b 100644 --- a/libs/components/src/popover/popover-content.tsx +++ b/libs/components/src/popover/popover-content.tsx @@ -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"; @@ -20,14 +19,14 @@ export const PopoverContent = component$((props: PropsOf<"div">) => { return ( diff --git a/libs/components/src/popover/popover-root.tsx b/libs/components/src/popover/popover-root.tsx index 165fb8d5b..c2484e9c1 100644 --- a/libs/components/src/popover/popover-root.tsx +++ b/libs/components/src/popover/popover-root.tsx @@ -239,6 +239,7 @@ export const PopoverRoot = component$((props: PopoverRootProps) => { onPointerOut$={handlePointerOut$} onPointerOver$={handlePointerOver$} ui-open={isOpen.value} + ui-hover={hover} ui-closed={!isOpen.value} ui-qds-popover-root ui-qds-scope diff --git a/libs/components/src/popover/popover-trigger.tsx b/libs/components/src/popover/popover-trigger.tsx index 239018855..3ae2cf999 100644 --- a/libs/components/src/popover/popover-trigger.tsx +++ b/libs/components/src/popover/popover-trigger.tsx @@ -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"; @@ -17,15 +17,14 @@ export const PopoverTrigger = component$((props: PropsOf<"button">) => { return ( diff --git a/libs/components/src/popover/popover.browser.tsx b/libs/components/src/popover/popover.browser.tsx index 700e766f1..cb536fde6 100644 --- a/libs/components/src/popover/popover.browser.tsx +++ b/libs/components/src/popover/popover.browser.tsx @@ -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(); + + 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(); + + await expect.element(Trigger).toHaveAttribute("aria-expanded", "false"); +}); + +test("trigger aria-expanded is true when open", async () => { + render(); + + await expect.element(Trigger).toHaveAttribute("aria-expanded", "true"); +}); + +test("trigger aria-expanded toggles with popover state", async () => { + render(); + + 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"); +}); diff --git a/libs/components/src/select/index.ts b/libs/components/src/select/index.ts new file mode 100644 index 000000000..6f468223f --- /dev/null +++ b/libs/components/src/select/index.ts @@ -0,0 +1,6 @@ +export { SelectContent as Content } from "./select-content"; +export { SelectItem as Item } from "./select-item"; +export { SelectItemLabel as ItemLabel } from "./select-item-label"; +export { SelectRoot as Root } from "./select-root"; +export { SelectTrigger as Trigger } from "./select-trigger"; +export { SelectValueLabel as ValueLabel } from "./select-value-label"; diff --git a/libs/components/src/select/select-content.tsx b/libs/components/src/select/select-content.tsx new file mode 100644 index 000000000..3673e8def --- /dev/null +++ b/libs/components/src/select/select-content.tsx @@ -0,0 +1,186 @@ +import { getValueFromIndex } from "@qds.dev/utils"; +import { + $, + component$, + type PropsOf, + Slot, + sync$, + useComputed$, + useContext, + useTask$ +} from "@qwik.dev/core"; +import { PopoverContent } from "../popover/popover-content"; +import { selectContextId } from "./select-root"; +import { SelectScript } from "./select-script"; + +type SelectContentProps = PropsOf; + +export const SelectContent = component$((props: SelectContentProps) => { + const context = useContext(selectContextId); + + const enabledItems = useComputed$(() => { + return context.itemRefsArray.value + .map((ref, index) => ({ ref, index })) + .filter(({ index }) => { + const isDisabled = context.itemDisabledArray.value[index] ?? false; + return !isDisabled; + }); + }); + + const handleToggle$ = $((e: ToggleEvent) => { + if (e.newState !== "open") return; + + const items = enabledItems.value; + if (items.length === 0) return; + + const highlightedIdx = context.highlightedIndex.value; + if (highlightedIdx !== null) { + const targetItem = items.find(({ index }) => index === highlightedIdx); + if (targetItem) { + targetItem.ref.value?.focus(); + return; + } + } + + // If no highlighted index, check if there's a selected item + const currentIdx = context.currentIndex.value; + if (currentIdx !== null) { + const targetItem = items.find(({ index }) => index === currentIdx); + if (targetItem) { + context.highlightedIndex.value = currentIdx; + targetItem.ref.value?.focus(); + return; + } + } + + // Focus first enabled item if nothing is selected or highlighted + const firstItem = items[0]; + context.highlightedIndex.value = firstItem.index; + firstItem.ref.value?.focus(); + }); + + const handleKeyDownSync$ = sync$((e: KeyboardEvent) => { + const keys = ["ArrowUp", "ArrowDown", "Home", "End", "Enter", " ", "Escape"]; + if (!keys.includes(e.key)) return; + e.preventDefault(); + }); + + const handleKeyDown$ = $((e: KeyboardEvent) => { + if (context.isDisabled.value) return; + + const items = enabledItems.value; + if (items.length === 0) return; + + const currentHighlighted = context.highlightedIndex.value; + const currentIndex = + currentHighlighted !== null + ? items.findIndex(({ index }) => index === currentHighlighted) + : -1; + + let nextIndex: number; + + switch (e.key) { + case "ArrowDown": { + if (currentIndex === -1) { + nextIndex = 0; + } else { + nextIndex = (currentIndex + 1) % items.length; + } + break; + } + + case "ArrowUp": { + if (currentIndex === -1) { + nextIndex = items.length - 1; + } else { + nextIndex = (currentIndex - 1 + items.length) % items.length; + } + break; + } + + case "Home": { + nextIndex = 0; + break; + } + + case "End": { + nextIndex = items.length - 1; + break; + } + + case "Escape": + case "Tab": { + context.isOpen.value = false; + return; + } + + case "Enter": + case " ": { + if (currentHighlighted === null) return; + + const value = getValueFromIndex( + currentHighlighted, + context.itemValuesArray.value + ); + + if (context.multiple) { + const currentValues = [...(context.selectedValues.value as string[])]; + const index = currentValues.indexOf(value); + if (index > -1) { + currentValues.splice(index, 1); + } else { + currentValues.push(value); + } + context.selectedValues.value = currentValues; + return; + } + + context.selectedValues.value = value; + context.isOpen.value = false; + return; + } + + default: + return; + } + + if (nextIndex >= 0 && nextIndex < items.length) { + const targetItem = items[nextIndex]; + context.highlightedIndex.value = targetItem.index; + targetItem.ref.value?.focus(); + } + }); + + useTask$(({ track }) => { + track(() => context.isOpen.value); + + if (!context.isOpen.value) return; + + const currentIdx = context.currentIndex.value; + if (currentIdx !== null) { + context.highlightedIndex.value = currentIdx; + return; + } + + if (context.highlightedIndex.value !== null) return; + + const items = enabledItems.value; + if (items.length === 0) return; + + context.highlightedIndex.value = items[0].index; + }); + + return ( + + + + + ); +}); diff --git a/libs/components/src/select/select-item-label.tsx b/libs/components/src/select/select-item-label.tsx new file mode 100644 index 000000000..b961ea694 --- /dev/null +++ b/libs/components/src/select/select-item-label.tsx @@ -0,0 +1,28 @@ +import { + component$, + PropsOf, + Slot, + useContext, + useSignal, + useTask$ +} from "@qwik.dev/core"; +import { Render } from "../render/render"; +import { selectItemContextId } from "./select-item"; +import { selectContextId } from "./select-root"; + +export const SelectItemLabel = component$>((props) => { + const context = useContext(selectContextId); + const itemContext = useContext(selectItemContextId); + const labelRef = useSignal(); + + useTask$(() => { + const index = itemContext.index; + context.itemLabelRefsArray.value[index] = labelRef; + }); + + return ( + + + + ); +}); diff --git a/libs/components/src/select/select-item.tsx b/libs/components/src/select/select-item.tsx new file mode 100644 index 000000000..527fe6300 --- /dev/null +++ b/libs/components/src/select/select-item.tsx @@ -0,0 +1,141 @@ +import { registerItem } from "@qds.dev/utils"; +import { + $, + component$, + createContextId, + type PropsOf, + Slot, + sync$, + useComputed$, + useConstant, + useContext, + useContextProvider, + useSignal, + useTask$ +} from "@qwik.dev/core"; +import { Render } from "../render/render"; +import { selectContextId } from "./select-root"; + +export const selectItemContextId = createContextId("qds-select-item"); + +export type SelectItemContext = { + index: number; + itemValue: string; +}; + +export type SelectItemProps = PropsOf<"div"> & { + /** The value for this select item. Defaults to string index if not provided. */ + value?: string; + /** Whether this item is disabled */ + disabled?: boolean; +}; + +export const SelectItem = component$((props: SelectItemProps) => { + const context = useContext(selectContextId); + const itemRef = useSignal(); + const { value: givenValue, disabled, ...rest } = props; + + const index = useConstant(() => { + const currItemIndex = context.currItemIndex; + context.currItemIndex++; + return currItemIndex; + }); + + const itemValue = useConstant(() => { + return givenValue ?? String(index); + }); + + const itemId = `${context.localId}-item-${itemValue}`; + + const itemContext: SelectItemContext = { + index, + itemValue + }; + + useContextProvider(selectItemContextId, itemContext); + + const isSelected = useComputed$(() => { + if (context.multiple) { + const values = context.selectedValues.value as string[]; + return values.includes(itemValue); + } else { + const value = context.selectedValues.value as string; + return value === itemValue; + } + }); + + const isHighlighted = useComputed$(() => { + return context.highlightedIndex.value === index; + }); + + useTask$(({ track }) => { + track(() => context.isDisabled.value); + + if (givenValue !== undefined) { + context.isDistinctValue.value = true; + } + + registerItem({ + index, + itemRef, + itemValue, + disabled: context.isDisabled.value || disabled, + itemRefsArray: context.itemRefsArray, + itemValuesArray: context.itemValuesArray, + itemDisabledArray: context.itemDisabledArray + }); + }); + + const handleClick$ = $(() => { + if (disabled || context.isDisabled.value) return; + + if (context.multiple) { + const currentValues = [...(context.selectedValues.value as string[])]; + const index = currentValues.indexOf(itemValue); + if (index > -1) { + currentValues.splice(index, 1); + } else { + currentValues.push(itemValue); + } + context.selectedValues.value = currentValues; + return; + } + + context.selectedValues.value = itemValue; + context.isOpen.value = false; + }); + + const handleKeyDownSync$ = sync$((e: KeyboardEvent) => { + const keys = ["Enter", " "]; + if (!keys.includes(e.key)) return; + e.preventDefault(); + }); + + const handleKeyDown$ = $(async (e: KeyboardEvent) => { + if (disabled || context.isDisabled.value) return; + + if (e.key === "Enter" || e.key === " ") { + await handleClick$(); + } + }); + + return ( + + + + ); +}); diff --git a/libs/components/src/select/select-root.tsx b/libs/components/src/select/select-root.tsx new file mode 100644 index 000000000..64c4ff6b6 --- /dev/null +++ b/libs/components/src/select/select-root.tsx @@ -0,0 +1,128 @@ +import { BindableProps, getIndexFromValue, useBindings } from "@qds.dev/utils"; +import { + component$, + createContextId, + type PropsOf, + type Signal, + Slot, + useComputed$, + useContextProvider, + useId, + useSignal, + useTask$ +} from "@qwik.dev/core"; +import { PopoverRoot } from "../popover/popover-root"; + +export const selectContextId = createContextId("qds-select"); + +export type SelectContext = { + localId: string; + isOpen: Signal; + itemRefsArray: Signal>>; + itemLabelRefsArray: Signal>>; + itemValuesArray: Signal; + itemDisabledArray: Signal; + selectedValues: Signal; + highlightedIndex: Signal; + currentIndex: Signal; + multiple: boolean; + isDisabled: Signal; + currItemIndex: number; + totalItems: Signal; + isDistinctValue: Signal; +}; + +export type PublicSelectRootProps = PropsOf & { + /** Whether multiple selections are allowed */ + multiple?: boolean; + /** Callback when selection changes */ + onChange$?: (value: string | string[]) => void; +} & BindableProps; + +type SelectBinds = { + value: string | string[]; + open: boolean; + disabled: boolean; +}; + +export const SelectRoot = component$((props: PublicSelectRootProps) => { + const localId = useId(); + const itemRefsArray = useSignal>>([]); + const itemLabelRefsArray = useSignal>>([]); + const itemValuesArray = useSignal([]); + const itemDisabledArray = useSignal([]); + const highlightedIndex = useSignal(null); + const totalItems = useSignal(0); + const isInitialLoad = useSignal(true); + const currItemIndex = 0; + const isDistinctValue = useSignal(false); + + const multiple = props.multiple ?? false; + + const initialSingle = "" as string; + const initialMultiple = [] as string[]; + + const { + openSig: isOpen, + valueSig: selectedValues, + disabledSig: isDisabled + } = useBindings(props, { + open: false, + value: multiple ? initialMultiple : initialSingle, + disabled: false + }); + + const currentIndex = useComputed$(() => { + if (multiple) { + const values = selectedValues.value as string[]; + if (values.length === 0) return null; + const firstValue = values[0]; + return getIndexFromValue(firstValue, itemValuesArray.value); + } else { + const value = selectedValues.value as string; + if (!value) return null; + return getIndexFromValue(value, itemValuesArray.value); + } + }); + + useTask$(({ track }) => { + track(() => itemRefsArray.value); + const total = itemRefsArray.value.length; + totalItems.value = total; + }); + + useTask$(({ track }) => { + track(() => selectedValues.value); + + if (!isInitialLoad.value) { + props.onChange$?.(selectedValues.value); + } + + isInitialLoad.value = false; + }); + + const context: SelectContext = { + localId, + isOpen, + itemRefsArray, + itemLabelRefsArray, + itemValuesArray, + itemDisabledArray, + selectedValues, + highlightedIndex, + currentIndex, + multiple, + isDisabled, + currItemIndex, + totalItems, + isDistinctValue + }; + + useContextProvider(selectContextId, context); + + return ( + + + + ); +}); diff --git a/libs/components/src/select/select-script.tsx b/libs/components/src/select/select-script.tsx new file mode 100644 index 000000000..402f1a98b --- /dev/null +++ b/libs/components/src/select/select-script.tsx @@ -0,0 +1,54 @@ +import { component$, useConstant, useContext } from "@qwik.dev/core"; +import { selectContextId } from "./select-root"; + +export const SelectScript = component$(() => { + const context = useContext(selectContextId); + + const hasInitialValue = useConstant(() => { + if (context.multiple) { + return (context.selectedValues.value as string[]).length > 0; + } + return !!(context.selectedValues.value as string); + }); + + const isDistinctValue = useConstant(() => { + return context.isDistinctValue.value; + }); + + if (!hasInitialValue || !isDistinctValue) return null; + + return ( +