@fanvue/ui 2.19.0 → 2.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/Autocomplete/Autocomplete.cjs +6 -2
- package/dist/cjs/components/Autocomplete/Autocomplete.cjs.map +1 -1
- package/dist/cjs/components/Autocomplete/AutocompleteDropdownContent.cjs +46 -3
- package/dist/cjs/components/Autocomplete/AutocompleteDropdownContent.cjs.map +1 -1
- package/dist/cjs/components/Autocomplete/constants.cjs +32 -0
- package/dist/cjs/components/Autocomplete/constants.cjs.map +1 -1
- package/dist/cjs/components/Autocomplete/useAutocomplete.cjs +15 -9
- package/dist/cjs/components/Autocomplete/useAutocomplete.cjs.map +1 -1
- package/dist/components/Autocomplete/Autocomplete.mjs +6 -2
- package/dist/components/Autocomplete/Autocomplete.mjs.map +1 -1
- package/dist/components/Autocomplete/AutocompleteDropdownContent.mjs +46 -3
- package/dist/components/Autocomplete/AutocompleteDropdownContent.mjs.map +1 -1
- package/dist/components/Autocomplete/constants.mjs +33 -1
- package/dist/components/Autocomplete/constants.mjs.map +1 -1
- package/dist/components/Autocomplete/useAutocomplete.mjs +16 -10
- package/dist/components/Autocomplete/useAutocomplete.mjs.map +1 -1
- package/dist/index.d.ts +75 -0
- package/package.json +5 -2
|
@@ -74,7 +74,8 @@ const Autocomplete = React__namespace.forwardRef((props, ref) => {
|
|
|
74
74
|
loadingText,
|
|
75
75
|
emptyText = "No results",
|
|
76
76
|
renderOption,
|
|
77
|
-
renderTag
|
|
77
|
+
renderTag,
|
|
78
|
+
renderGroupHeading
|
|
78
79
|
} = props;
|
|
79
80
|
const ac = useAutocomplete.useAutocomplete(props);
|
|
80
81
|
React__namespace.useImperativeHandle(ref, () => ac.inputRef.current);
|
|
@@ -206,6 +207,8 @@ const Autocomplete = React__namespace.forwardRef((props, ref) => {
|
|
|
206
207
|
loadingText,
|
|
207
208
|
emptyText,
|
|
208
209
|
visibleOptions: ac.visibleOptions,
|
|
210
|
+
visibleSections: ac.visibleSections,
|
|
211
|
+
createOption: ac.createOption,
|
|
209
212
|
listboxId: ac.listboxId,
|
|
210
213
|
activeIndex: ac.activeIndex,
|
|
211
214
|
isMulti: ac.isMulti,
|
|
@@ -213,7 +216,8 @@ const Autocomplete = React__namespace.forwardRef((props, ref) => {
|
|
|
213
216
|
selectedValue: ac.selectedValue,
|
|
214
217
|
onSelect: ac.handleSelect,
|
|
215
218
|
onMouseEnter: ac.setActiveIndex,
|
|
216
|
-
renderOption
|
|
219
|
+
renderOption,
|
|
220
|
+
renderGroupHeading
|
|
217
221
|
}
|
|
218
222
|
)
|
|
219
223
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Autocomplete.cjs","sources":["../../../../src/components/Autocomplete/Autocomplete.tsx"],"sourcesContent":["import * as Popover from \"@radix-ui/react-popover\";\nimport * as React from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { FLOATING_CONTENT_COLLISION_PADDING } from \"@/utils/floatingContentCollisionPadding\";\nimport { ChevronDownIcon } from \"../Icons/ChevronDownIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\nimport { SpinnerIcon } from \"../Icons/SpinnerIcon\";\nimport { AutocompleteDropdownContent } from \"./AutocompleteDropdownContent\";\nimport { AutocompleteTag } from \"./AutocompleteTag\";\nimport { useAutocomplete } from \"./useAutocomplete\";\n\nexport type AutocompleteSize = \"48\" | \"40\" | \"32\";\n\nexport interface AutocompleteOption {\n value: string;\n label?: string;\n disabled?: boolean;\n}\n\ninterface AutocompleteBaseProps {\n label?: string;\n \"aria-label\"?: string;\n \"aria-labelledby\"?: string;\n helperText?: string;\n /** @default \"48\" */\n size?: AutocompleteSize;\n /** @default false */\n error?: boolean;\n errorMessage?: string;\n placeholder?: string;\n leftIcon?: React.ReactNode;\n /** @default false */\n fullWidth?: boolean;\n /** @default false */\n disabled?: boolean;\n /** @default false */\n clearable?: boolean;\n clearAriaLabel?: string;\n id?: string;\n className?: string;\n options: AutocompleteOption[];\n inputValue?: string;\n onInputChange?: (value: string) => void;\n filterFn?: (option: AutocompleteOption, query: string) => boolean;\n /** @default false */\n creatable?: boolean;\n creatableLabel?: (inputValue: string) => string;\n onCreate?: (inputValue: string) => void;\n /** @default false */\n loading?: boolean;\n loadingText?: string;\n emptyText?: string;\n renderOption?: (\n option: AutocompleteOption,\n state: { selected: boolean; active: boolean },\n ) => React.ReactNode;\n renderTag?: (option: AutocompleteOption, onRemove: () => void) => React.ReactNode;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n}\n\ninterface AutocompleteSingleProps extends AutocompleteBaseProps {\n multiple?: false;\n value?: string | null;\n defaultValue?: string | null;\n onChange?: (value: string | null) => void;\n}\n\ninterface AutocompleteMultiProps extends AutocompleteBaseProps {\n multiple: true;\n value?: string[];\n defaultValue?: string[];\n onChange?: (values: string[]) => void;\n}\n\nexport type AutocompleteProps = AutocompleteSingleProps | AutocompleteMultiProps;\n\nconst CONTAINER_HEIGHT: Record<AutocompleteSize, string> = {\n \"48\": \"min-h-12\",\n \"40\": \"min-h-10\",\n \"32\": \"min-h-8\",\n};\n\nconst INPUT_SIZE_CLASSES: Record<AutocompleteSize, string> = {\n \"48\": \"typography-regular-body-lg\",\n \"40\": \"typography-regular-body-lg\",\n \"32\": \"typography-regular-body-md\",\n};\n\nconst PADDING_CLASSES: Record<AutocompleteSize, string> = {\n \"48\": \"px-4 py-1.5 gap-3\",\n \"40\": \"px-4 py-1 gap-3\",\n \"32\": \"px-3 py-1 gap-2\",\n};\n\nfunction warnMissingAccessibleName(label?: string, ariaLabel?: string, ariaLabelledBy?: string) {\n if (process.env.NODE_ENV !== \"production\") {\n if (!label && !ariaLabel && !ariaLabelledBy) {\n console.warn(\n \"Autocomplete: no accessible name provided. Pass a `label`, `aria-label`, or `aria-labelledby` prop.\",\n );\n }\n }\n}\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Conditional JSX branches in the render template\nexport const Autocomplete = React.forwardRef<HTMLInputElement, AutocompleteProps>((props, ref) => {\n const {\n label,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledby,\n helperText,\n size = \"48\",\n error = false,\n errorMessage,\n placeholder,\n leftIcon,\n fullWidth = false,\n disabled = false,\n clearable = false,\n clearAriaLabel = \"Clear\",\n className,\n loading = false,\n loadingText,\n emptyText = \"No results\",\n renderOption,\n renderTag,\n } = props;\n\n const ac = useAutocomplete(props);\n\n React.useImperativeHandle(ref, () => ac.inputRef.current as HTMLInputElement);\n\n warnMissingAccessibleName(label, ariaLabel, ariaLabelledby);\n\n const bottomText = error && errorMessage ? errorMessage : helperText;\n\n return (\n <Popover.Root open={ac.isOpen && !disabled} onOpenChange={ac.handleOpenChange}>\n <div\n className={cn(\"flex flex-col\", fullWidth && \"w-full\", className)}\n data-autocomplete-root=\"\"\n data-disabled={disabled ? \"\" : undefined}\n data-error={error ? \"\" : undefined}\n >\n {label && (\n <label\n htmlFor={ac.inputId}\n className=\"typography-semibold-body-sm px-1 pt-1 pb-2 text-content-primary\"\n >\n {label}\n </label>\n )}\n\n <Popover.Anchor asChild>\n {/* biome-ignore lint/a11y/noStaticElementInteractions: Container delegates click to the inner input */}\n {/* biome-ignore lint/a11y/useKeyWithClickEvents: Keyboard interaction is handled by the inner combobox input */}\n <div\n className={cn(\n \"flex flex-wrap items-center overflow-hidden rounded-sm border bg-neutral-alphas-100 has-focus-visible:shadow-focus-ring has-focus-visible:outline-none motion-safe:transition-colors\",\n error ? \"border-error-content\" : \"border-transparent\",\n !disabled && !error && \"hover:border-neutral-alphas-400\",\n ac.isOpen && !error && !disabled && \"border-neutral-alphas-400\",\n CONTAINER_HEIGHT[size],\n PADDING_CLASSES[size],\n disabled && \"opacity-50\",\n )}\n onClick={ac.handleContainerClick}\n >\n {leftIcon && (\n <div className=\"flex size-5 shrink-0 items-center justify-center text-content-secondary\">\n {leftIcon}\n </div>\n )}\n\n <div className=\"flex min-w-0 flex-1 flex-wrap items-center gap-1.5\">\n {ac.isMulti &&\n ac.selectedMultiOptions.map((opt) => (\n <AutocompleteTag\n key={opt.value}\n option={opt}\n disabled={disabled}\n onRemove={() => ac.toggleMulti(opt.value)}\n renderTag={renderTag}\n />\n ))}\n\n <input\n ref={ac.inputRef}\n id={ac.inputId}\n role=\"combobox\"\n type=\"text\"\n disabled={disabled}\n aria-expanded={ac.isOpen}\n aria-controls={ac.isOpen ? ac.listboxId : undefined}\n aria-activedescendant={ac.activeDescendantId}\n aria-autocomplete=\"list\"\n aria-describedby={bottomText ? ac.helperTextId : undefined}\n aria-invalid={error || undefined}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n autoComplete=\"off\"\n placeholder={\n ac.isMulti && ac.selectedMultiValues.length > 0 ? undefined : placeholder\n }\n value={ac.displayInputValue}\n onChange={ac.handleInputChange}\n onKeyDown={ac.handleKeyDown}\n onFocus={ac.handleFocus}\n onBlur={ac.handleBlur}\n className={cn(\n \"min-w-[40px] flex-1 truncate bg-transparent text-content-primary no-underline placeholder:text-content-secondary placeholder:opacity-40 focus:outline-none disabled:cursor-not-allowed\",\n INPUT_SIZE_CLASSES[size],\n )}\n />\n </div>\n\n <div className=\"flex shrink-0 items-center gap-1\">\n {loading && <SpinnerIcon className=\"size-4 animate-spin text-content-secondary\" />}\n {clearable && ac.hasClearableValue && !disabled && (\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={clearAriaLabel}\n className=\"flex size-5 shrink-0 cursor-pointer items-center justify-center text-content-secondary hover:text-content-primary active:scale-95\"\n onClick={ac.handleClear}\n >\n <CloseIcon className=\"size-4\" />\n </button>\n )}\n <div className=\"flex size-5 shrink-0 items-center justify-center text-content-secondary\">\n <ChevronDownIcon\n className={cn(\"size-5 transition-transform\", ac.isOpen && \"rotate-180\")}\n />\n </div>\n </div>\n </div>\n </Popover.Anchor>\n\n <Popover.Portal>\n <Popover.Content\n sideOffset={4}\n collisionPadding={FLOATING_CONTENT_COLLISION_PADDING}\n onOpenAutoFocus={(e) => e.preventDefault()}\n onCloseAutoFocus={(e) => e.preventDefault()}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\" }}\n className={cn(\n \"w-max min-w-(--radix-popper-anchor-width) max-w-(--radix-popover-content-available-width) overflow-hidden rounded-sm border border-neutral-alphas-200 bg-bg-primary text-content-primary shadow-[0_4px_16px_rgba(0,0,0,0.10)]\",\n \"data-[state=closed]:animate-out data-[state=open]:animate-in\",\n \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n \"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\",\n \"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2\",\n )}\n >\n <div\n ref={ac.listRef}\n id={ac.listboxId}\n role=\"listbox\"\n aria-label={label ?? ariaLabel ?? \"Options\"}\n aria-multiselectable={ac.isMulti || undefined}\n className=\"max-h-60 overflow-y-auto p-1\"\n >\n <AutocompleteDropdownContent\n loading={loading}\n loadingText={loadingText}\n emptyText={emptyText}\n visibleOptions={ac.visibleOptions}\n listboxId={ac.listboxId}\n activeIndex={ac.activeIndex}\n isMulti={ac.isMulti}\n selectedMultiValues={ac.selectedMultiValues}\n selectedValue={ac.selectedValue}\n onSelect={ac.handleSelect}\n onMouseEnter={ac.setActiveIndex}\n renderOption={renderOption}\n />\n </div>\n </Popover.Content>\n </Popover.Portal>\n\n {bottomText && (\n <p\n id={ac.helperTextId}\n className={cn(\n \"typography-regular-body-sm px-2 pt-1 pb-0.5\",\n error ? \"text-error-content\" : \"text-content-secondary\",\n )}\n >\n {bottomText}\n </p>\n )}\n </div>\n </Popover.Root>\n );\n});\n\nAutocomplete.displayName = \"Autocomplete\";\n"],"names":["React","useAutocomplete","jsx","Popover","jsxs","cn","AutocompleteTag","SpinnerIcon","CloseIcon","ChevronDownIcon","FLOATING_CONTENT_COLLISION_PADDING","AutocompleteDropdownContent"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Autocomplete.cjs","sources":["../../../../src/components/Autocomplete/Autocomplete.tsx"],"sourcesContent":["import * as Popover from \"@radix-ui/react-popover\";\nimport * as React from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { FLOATING_CONTENT_COLLISION_PADDING } from \"@/utils/floatingContentCollisionPadding\";\nimport { ChevronDownIcon } from \"../Icons/ChevronDownIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\nimport { SpinnerIcon } from \"../Icons/SpinnerIcon\";\nimport { AutocompleteDropdownContent } from \"./AutocompleteDropdownContent\";\nimport { AutocompleteTag } from \"./AutocompleteTag\";\nimport { useAutocomplete } from \"./useAutocomplete\";\n\nexport type AutocompleteSize = \"48\" | \"40\" | \"32\";\n\nexport interface AutocompleteOption {\n /** Unique value identifying the option. Returned via `onChange`. */\n value: string;\n /** Visible label. Falls back to `value` when omitted. */\n label?: string;\n /** When `true`, the option renders but cannot be selected. @default false */\n disabled?: boolean;\n /**\n * ID of the group this option belongs to. Must match an entry in the\n * component's `groups` prop. Options without a `groupId` render in an\n * implicit \"ungrouped\" bucket above the first declared group.\n */\n groupId?: string;\n /**\n * Pinned options bypass the search filter and stay visible at the top of\n * the list regardless of the current query. Useful for \"+ Create new\"\n * affordances. Pinned options ignore `groupId` and render before any\n * grouped or ungrouped content. @default false\n */\n pinned?: boolean;\n}\n\n/**\n * Describes a single group rendered above its matching options.\n * Indentation of nested rows (e.g. price under product) is not built in —\n * consumers control it via `renderOption` styling.\n */\nexport interface AutocompleteGroup {\n /** Stable identifier referenced by `option.groupId`. */\n id: string;\n /**\n * Group heading text. Used as the group's accessible name and matched\n * against the search query: when the query matches a group's `label`,\n * every option under that group is kept regardless of whether it\n * individually matches the per-option filter. This supports the common\n * \"heading is the searchable label, items are sub-rows\" shape (e.g.\n * heading = product name, items = prices).\n */\n label: string;\n}\n\ninterface AutocompleteBaseProps {\n label?: string;\n \"aria-label\"?: string;\n \"aria-labelledby\"?: string;\n helperText?: string;\n /** @default \"48\" */\n size?: AutocompleteSize;\n /** @default false */\n error?: boolean;\n errorMessage?: string;\n placeholder?: string;\n leftIcon?: React.ReactNode;\n /** @default false */\n fullWidth?: boolean;\n /** @default false */\n disabled?: boolean;\n /** @default false */\n clearable?: boolean;\n clearAriaLabel?: string;\n id?: string;\n className?: string;\n options: AutocompleteOption[];\n inputValue?: string;\n onInputChange?: (value: string) => void;\n filterFn?: (option: AutocompleteOption, query: string) => boolean;\n /** @default false */\n creatable?: boolean;\n creatableLabel?: (inputValue: string) => string;\n onCreate?: (inputValue: string) => void;\n /** @default false */\n loading?: boolean;\n loadingText?: string;\n emptyText?: string;\n renderOption?: (\n option: AutocompleteOption,\n state: { selected: boolean; active: boolean },\n ) => React.ReactNode;\n renderTag?: (option: AutocompleteOption, onRemove: () => void) => React.ReactNode;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n /**\n * Ordered list of groups. When provided, options whose `groupId` matches\n * an entry render under the corresponding heading. Groups with no visible\n * (post-filter) options collapse silently. Pinned options render above\n * everything; ungrouped options render between pinned and the first group.\n * Only one level of grouping is supported.\n */\n groups?: AutocompleteGroup[];\n /**\n * Custom renderer for group headings. The returned node is wrapped by the\n * component in an element carrying the `id` referenced by the surrounding\n * `role=\"group\"` wrapper's `aria-labelledby`, so consumers only need to\n * return visual content.\n */\n renderGroupHeading?: (group: AutocompleteGroup) => React.ReactNode;\n}\n\ninterface AutocompleteSingleProps extends AutocompleteBaseProps {\n multiple?: false;\n value?: string | null;\n defaultValue?: string | null;\n onChange?: (value: string | null) => void;\n}\n\ninterface AutocompleteMultiProps extends AutocompleteBaseProps {\n multiple: true;\n value?: string[];\n defaultValue?: string[];\n onChange?: (values: string[]) => void;\n}\n\nexport type AutocompleteProps = AutocompleteSingleProps | AutocompleteMultiProps;\n\nconst CONTAINER_HEIGHT: Record<AutocompleteSize, string> = {\n \"48\": \"min-h-12\",\n \"40\": \"min-h-10\",\n \"32\": \"min-h-8\",\n};\n\nconst INPUT_SIZE_CLASSES: Record<AutocompleteSize, string> = {\n \"48\": \"typography-regular-body-lg\",\n \"40\": \"typography-regular-body-lg\",\n \"32\": \"typography-regular-body-md\",\n};\n\nconst PADDING_CLASSES: Record<AutocompleteSize, string> = {\n \"48\": \"px-4 py-1.5 gap-3\",\n \"40\": \"px-4 py-1 gap-3\",\n \"32\": \"px-3 py-1 gap-2\",\n};\n\nfunction warnMissingAccessibleName(label?: string, ariaLabel?: string, ariaLabelledBy?: string) {\n if (process.env.NODE_ENV !== \"production\") {\n if (!label && !ariaLabel && !ariaLabelledBy) {\n console.warn(\n \"Autocomplete: no accessible name provided. Pass a `label`, `aria-label`, or `aria-labelledby` prop.\",\n );\n }\n }\n}\n\n/**\n * A combobox input with single- or multi-select, optional async loading, and\n * native support for grouped + pinned options.\n *\n * - Pass `groups` plus an `options` array whose entries reference each group\n * via `groupId` to render hierarchical lists with proper `role=\"group\"` +\n * `aria-labelledby` semantics. Options without a `groupId` render above\n * the first group; options marked `pinned` render above everything and\n * bypass the search filter.\n * - Indentation of nested rows (e.g. price under product) is controlled by\n * the consumer via `renderOption` styling — there is no built-in indent.\n *\n * @example\n * ```tsx\n * <Autocomplete\n * aria-label=\"Choose a product\"\n * options={[\n * { value: \"__new__\", label: \"+ Create new product\", pinned: true },\n * { value: \"product:abc\", label: \"Demo Product\", groupId: \"recent\" },\n * ]}\n * groups={[{ id: \"recent\", label: \"Recent products\" }]}\n * onChange={handleSelect}\n * />\n * ```\n */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Conditional JSX branches in the render template\nexport const Autocomplete = React.forwardRef<HTMLInputElement, AutocompleteProps>((props, ref) => {\n const {\n label,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledby,\n helperText,\n size = \"48\",\n error = false,\n errorMessage,\n placeholder,\n leftIcon,\n fullWidth = false,\n disabled = false,\n clearable = false,\n clearAriaLabel = \"Clear\",\n className,\n loading = false,\n loadingText,\n emptyText = \"No results\",\n renderOption,\n renderTag,\n renderGroupHeading,\n } = props;\n\n const ac = useAutocomplete(props);\n\n React.useImperativeHandle(ref, () => ac.inputRef.current as HTMLInputElement);\n\n warnMissingAccessibleName(label, ariaLabel, ariaLabelledby);\n\n const bottomText = error && errorMessage ? errorMessage : helperText;\n\n return (\n <Popover.Root open={ac.isOpen && !disabled} onOpenChange={ac.handleOpenChange}>\n <div\n className={cn(\"flex flex-col\", fullWidth && \"w-full\", className)}\n data-autocomplete-root=\"\"\n data-disabled={disabled ? \"\" : undefined}\n data-error={error ? \"\" : undefined}\n >\n {label && (\n <label\n htmlFor={ac.inputId}\n className=\"typography-semibold-body-sm px-1 pt-1 pb-2 text-content-primary\"\n >\n {label}\n </label>\n )}\n\n <Popover.Anchor asChild>\n {/* biome-ignore lint/a11y/noStaticElementInteractions: Container delegates click to the inner input */}\n {/* biome-ignore lint/a11y/useKeyWithClickEvents: Keyboard interaction is handled by the inner combobox input */}\n <div\n className={cn(\n \"flex flex-wrap items-center overflow-hidden rounded-sm border bg-neutral-alphas-100 has-focus-visible:shadow-focus-ring has-focus-visible:outline-none motion-safe:transition-colors\",\n error ? \"border-error-content\" : \"border-transparent\",\n !disabled && !error && \"hover:border-neutral-alphas-400\",\n ac.isOpen && !error && !disabled && \"border-neutral-alphas-400\",\n CONTAINER_HEIGHT[size],\n PADDING_CLASSES[size],\n disabled && \"opacity-50\",\n )}\n onClick={ac.handleContainerClick}\n >\n {leftIcon && (\n <div className=\"flex size-5 shrink-0 items-center justify-center text-content-secondary\">\n {leftIcon}\n </div>\n )}\n\n <div className=\"flex min-w-0 flex-1 flex-wrap items-center gap-1.5\">\n {ac.isMulti &&\n ac.selectedMultiOptions.map((opt) => (\n <AutocompleteTag\n key={opt.value}\n option={opt}\n disabled={disabled}\n onRemove={() => ac.toggleMulti(opt.value)}\n renderTag={renderTag}\n />\n ))}\n\n <input\n ref={ac.inputRef}\n id={ac.inputId}\n role=\"combobox\"\n type=\"text\"\n disabled={disabled}\n aria-expanded={ac.isOpen}\n aria-controls={ac.isOpen ? ac.listboxId : undefined}\n aria-activedescendant={ac.activeDescendantId}\n aria-autocomplete=\"list\"\n aria-describedby={bottomText ? ac.helperTextId : undefined}\n aria-invalid={error || undefined}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n autoComplete=\"off\"\n placeholder={\n ac.isMulti && ac.selectedMultiValues.length > 0 ? undefined : placeholder\n }\n value={ac.displayInputValue}\n onChange={ac.handleInputChange}\n onKeyDown={ac.handleKeyDown}\n onFocus={ac.handleFocus}\n onBlur={ac.handleBlur}\n className={cn(\n \"min-w-[40px] flex-1 truncate bg-transparent text-content-primary no-underline placeholder:text-content-secondary placeholder:opacity-40 focus:outline-none disabled:cursor-not-allowed\",\n INPUT_SIZE_CLASSES[size],\n )}\n />\n </div>\n\n <div className=\"flex shrink-0 items-center gap-1\">\n {loading && <SpinnerIcon className=\"size-4 animate-spin text-content-secondary\" />}\n {clearable && ac.hasClearableValue && !disabled && (\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={clearAriaLabel}\n className=\"flex size-5 shrink-0 cursor-pointer items-center justify-center text-content-secondary hover:text-content-primary active:scale-95\"\n onClick={ac.handleClear}\n >\n <CloseIcon className=\"size-4\" />\n </button>\n )}\n <div className=\"flex size-5 shrink-0 items-center justify-center text-content-secondary\">\n <ChevronDownIcon\n className={cn(\"size-5 transition-transform\", ac.isOpen && \"rotate-180\")}\n />\n </div>\n </div>\n </div>\n </Popover.Anchor>\n\n <Popover.Portal>\n <Popover.Content\n sideOffset={4}\n collisionPadding={FLOATING_CONTENT_COLLISION_PADDING}\n onOpenAutoFocus={(e) => e.preventDefault()}\n onCloseAutoFocus={(e) => e.preventDefault()}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\" }}\n className={cn(\n \"w-max min-w-(--radix-popper-anchor-width) max-w-(--radix-popover-content-available-width) overflow-hidden rounded-sm border border-neutral-alphas-200 bg-bg-primary text-content-primary shadow-[0_4px_16px_rgba(0,0,0,0.10)]\",\n \"data-[state=closed]:animate-out data-[state=open]:animate-in\",\n \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n \"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\",\n \"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2\",\n )}\n >\n <div\n ref={ac.listRef}\n id={ac.listboxId}\n role=\"listbox\"\n aria-label={label ?? ariaLabel ?? \"Options\"}\n aria-multiselectable={ac.isMulti || undefined}\n className=\"max-h-60 overflow-y-auto p-1\"\n >\n <AutocompleteDropdownContent\n loading={loading}\n loadingText={loadingText}\n emptyText={emptyText}\n visibleOptions={ac.visibleOptions}\n visibleSections={ac.visibleSections}\n createOption={ac.createOption}\n listboxId={ac.listboxId}\n activeIndex={ac.activeIndex}\n isMulti={ac.isMulti}\n selectedMultiValues={ac.selectedMultiValues}\n selectedValue={ac.selectedValue}\n onSelect={ac.handleSelect}\n onMouseEnter={ac.setActiveIndex}\n renderOption={renderOption}\n renderGroupHeading={renderGroupHeading}\n />\n </div>\n </Popover.Content>\n </Popover.Portal>\n\n {bottomText && (\n <p\n id={ac.helperTextId}\n className={cn(\n \"typography-regular-body-sm px-2 pt-1 pb-0.5\",\n error ? \"text-error-content\" : \"text-content-secondary\",\n )}\n >\n {bottomText}\n </p>\n )}\n </div>\n </Popover.Root>\n );\n});\n\nAutocomplete.displayName = \"Autocomplete\";\n"],"names":["React","useAutocomplete","jsx","Popover","jsxs","cn","AutocompleteTag","SpinnerIcon","CloseIcon","ChevronDownIcon","FLOATING_CONTENT_COLLISION_PADDING","AutocompleteDropdownContent"],"mappings":"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"}
|
|
@@ -2,13 +2,19 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
4
4
|
const jsxRuntime = require("react/jsx-runtime");
|
|
5
|
+
const cn = require("../../utils/cn.cjs");
|
|
5
6
|
const SpinnerIcon = require("../Icons/SpinnerIcon.cjs");
|
|
6
7
|
const AutocompleteOptionItem = require("./AutocompleteOptionItem.cjs");
|
|
8
|
+
function DefaultGroupHeading({ children }) {
|
|
9
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "typography-semibold-body-sm block px-3 pt-2 pb-1 text-content-secondary uppercase tracking-wide", children });
|
|
10
|
+
}
|
|
7
11
|
function AutocompleteDropdownContent({
|
|
8
12
|
loading,
|
|
9
13
|
loadingText,
|
|
10
14
|
emptyText,
|
|
11
15
|
visibleOptions,
|
|
16
|
+
visibleSections,
|
|
17
|
+
createOption,
|
|
12
18
|
listboxId,
|
|
13
19
|
activeIndex,
|
|
14
20
|
isMulti,
|
|
@@ -16,7 +22,8 @@ function AutocompleteDropdownContent({
|
|
|
16
22
|
selectedValue,
|
|
17
23
|
onSelect,
|
|
18
24
|
onMouseEnter,
|
|
19
|
-
renderOption
|
|
25
|
+
renderOption,
|
|
26
|
+
renderGroupHeading
|
|
20
27
|
}) {
|
|
21
28
|
if (loading) {
|
|
22
29
|
return (
|
|
@@ -27,10 +34,16 @@ function AutocompleteDropdownContent({
|
|
|
27
34
|
] })
|
|
28
35
|
);
|
|
29
36
|
}
|
|
37
|
+
const { pinned, ungrouped, groups } = visibleSections;
|
|
38
|
+
const hasTopBlock = pinned.length > 0 || ungrouped.length > 0;
|
|
39
|
+
const hasGroups = groups.length > 0;
|
|
40
|
+
const nonPinnedVisibleCount = ungrouped.length + groups.reduce((n, g) => n + g.options.length, 0) + (createOption ? 1 : 0);
|
|
30
41
|
if (visibleOptions.length === 0) {
|
|
31
42
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "typography-regular-body-md block px-3 py-4 text-center text-content-secondary", children: emptyText });
|
|
32
43
|
}
|
|
33
|
-
|
|
44
|
+
let cursor = 0;
|
|
45
|
+
const renderOptionRow = (option) => {
|
|
46
|
+
const index = cursor++;
|
|
34
47
|
const isSelected = isMulti ? selectedMultiValues.includes(option.value) : option.value === selectedValue;
|
|
35
48
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
36
49
|
AutocompleteOptionItem.AutocompleteOptionItem,
|
|
@@ -46,7 +59,37 @@ function AutocompleteDropdownContent({
|
|
|
46
59
|
},
|
|
47
60
|
option.value
|
|
48
61
|
);
|
|
49
|
-
}
|
|
62
|
+
};
|
|
63
|
+
const showDivider = hasTopBlock && hasGroups;
|
|
64
|
+
const emptyAfterPinned = nonPinnedVisibleCount === 0 && pinned.length > 0;
|
|
65
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
66
|
+
pinned.map(renderOptionRow),
|
|
67
|
+
ungrouped.map(renderOptionRow),
|
|
68
|
+
groups.map((section, sectionIndex) => {
|
|
69
|
+
const headingId = `${listboxId}-group-${section.group.id}`;
|
|
70
|
+
const headingNode = renderGroupHeading ? renderGroupHeading(section.group) : section.group.label;
|
|
71
|
+
return (
|
|
72
|
+
// biome-ignore lint/a11y/useSemanticElements: <fieldset> carries form semantics and default styling we don't want inside a listbox; role="group" is the correct ARIA pattern here
|
|
73
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
74
|
+
"div",
|
|
75
|
+
{
|
|
76
|
+
role: "group",
|
|
77
|
+
"aria-labelledby": headingId,
|
|
78
|
+
className: cn.cn(
|
|
79
|
+
showDivider && sectionIndex === 0 && "mt-1 border-neutral-alphas-200 border-t pt-1"
|
|
80
|
+
),
|
|
81
|
+
children: [
|
|
82
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { role: "presentation", id: headingId, children: renderGroupHeading ? headingNode : /* @__PURE__ */ jsxRuntime.jsx(DefaultGroupHeading, { children: headingNode }) }),
|
|
83
|
+
section.options.map(renderOptionRow)
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
section.group.id
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
}),
|
|
90
|
+
createOption && renderOptionRow(createOption),
|
|
91
|
+
emptyAfterPinned && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "typography-regular-body-md block px-3 py-4 text-center text-content-secondary", children: emptyText })
|
|
92
|
+
] });
|
|
50
93
|
}
|
|
51
94
|
exports.AutocompleteDropdownContent = AutocompleteDropdownContent;
|
|
52
95
|
//# sourceMappingURL=AutocompleteDropdownContent.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AutocompleteDropdownContent.cjs","sources":["../../../../src/components/Autocomplete/AutocompleteDropdownContent.tsx"],"sourcesContent":["import type * as React from \"react\";\nimport { SpinnerIcon } from \"../Icons/SpinnerIcon\";\nimport type { AutocompleteOption } from \"./Autocomplete\";\nimport { AutocompleteOptionItem } from \"./AutocompleteOptionItem\";\
|
|
1
|
+
{"version":3,"file":"AutocompleteDropdownContent.cjs","sources":["../../../../src/components/Autocomplete/AutocompleteDropdownContent.tsx"],"sourcesContent":["import type * as React from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { SpinnerIcon } from \"../Icons/SpinnerIcon\";\nimport type { AutocompleteGroup, AutocompleteOption } from \"./Autocomplete\";\nimport { AutocompleteOptionItem } from \"./AutocompleteOptionItem\";\nimport type { AutocompleteVisibleSections } from \"./constants\";\n\ninterface AutocompleteDropdownContentProps {\n loading: boolean;\n loadingText?: string;\n emptyText?: string;\n visibleOptions: AutocompleteOption[];\n visibleSections: AutocompleteVisibleSections;\n /**\n * Synthetic \"Create new …\" row, when `creatable` is on and the search\n * doesn't already match an existing option. Rendered as a trailing row,\n * always shown regardless of filter, and indexed last in\n * `visibleOptions` so keyboard navigation works.\n */\n createOption?: AutocompleteOption | null;\n listboxId: string;\n activeIndex: number;\n isMulti: boolean;\n selectedMultiValues: string[];\n selectedValue: string | null;\n onSelect: (option: AutocompleteOption) => void;\n onMouseEnter: (index: number) => void;\n renderOption?: (\n option: AutocompleteOption,\n state: { selected: boolean; active: boolean },\n ) => React.ReactNode;\n renderGroupHeading?: (group: AutocompleteGroup) => React.ReactNode;\n}\n\nfunction DefaultGroupHeading({ children }: { children: React.ReactNode }) {\n return (\n <span className=\"typography-semibold-body-sm block px-3 pt-2 pb-1 text-content-secondary uppercase tracking-wide\">\n {children}\n </span>\n );\n}\n\nexport function AutocompleteDropdownContent({\n loading,\n loadingText,\n emptyText,\n visibleOptions,\n visibleSections,\n createOption,\n listboxId,\n activeIndex,\n isMulti,\n selectedMultiValues,\n selectedValue,\n onSelect,\n onMouseEnter,\n renderOption,\n renderGroupHeading,\n}: AutocompleteDropdownContentProps) {\n if (loading) {\n return (\n // biome-ignore lint/a11y/useSemanticElements: <output> is not appropriate here; using role=\"status\" for live region announcements\n <div role=\"status\" className=\"flex items-center justify-center py-4\">\n <SpinnerIcon className=\"size-5 animate-spin text-content-secondary\" />\n {loadingText && (\n <span className=\"typography-regular-body-md ml-2 text-content-secondary\">\n {loadingText}\n </span>\n )}\n </div>\n );\n }\n\n const { pinned, ungrouped, groups } = visibleSections;\n const hasTopBlock = pinned.length > 0 || ungrouped.length > 0;\n const hasGroups = groups.length > 0;\n const nonPinnedVisibleCount =\n ungrouped.length + groups.reduce((n, g) => n + g.options.length, 0) + (createOption ? 1 : 0);\n\n if (visibleOptions.length === 0) {\n return (\n <div className=\"typography-regular-body-md block px-3 py-4 text-center text-content-secondary\">\n {emptyText}\n </div>\n );\n }\n\n let cursor = 0;\n const renderOptionRow = (option: AutocompleteOption) => {\n const index = cursor++;\n const isSelected = isMulti\n ? selectedMultiValues.includes(option.value)\n : option.value === selectedValue;\n return (\n <AutocompleteOptionItem\n key={option.value}\n option={option}\n optionId={`${listboxId}-option-${index}`}\n index={index}\n isSelected={isSelected}\n isActive={index === activeIndex}\n onSelect={() => onSelect(option)}\n onMouseEnter={() => onMouseEnter(index)}\n renderOption={renderOption}\n />\n );\n };\n\n const showDivider = hasTopBlock && hasGroups;\n const emptyAfterPinned = nonPinnedVisibleCount === 0 && pinned.length > 0;\n\n return (\n <>\n {pinned.map(renderOptionRow)}\n {ungrouped.map(renderOptionRow)}\n {groups.map((section, sectionIndex) => {\n const headingId = `${listboxId}-group-${section.group.id}`;\n const headingNode = renderGroupHeading\n ? renderGroupHeading(section.group)\n : section.group.label;\n return (\n // biome-ignore lint/a11y/useSemanticElements: <fieldset> carries form semantics and default styling we don't want inside a listbox; role=\"group\" is the correct ARIA pattern here\n <div\n key={section.group.id}\n role=\"group\"\n aria-labelledby={headingId}\n className={cn(\n showDivider && sectionIndex === 0 && \"mt-1 border-neutral-alphas-200 border-t pt-1\",\n )}\n >\n <div role=\"presentation\" id={headingId}>\n {renderGroupHeading ? (\n headingNode\n ) : (\n <DefaultGroupHeading>{headingNode}</DefaultGroupHeading>\n )}\n </div>\n {section.options.map(renderOptionRow)}\n </div>\n );\n })}\n {createOption && renderOptionRow(createOption)}\n {emptyAfterPinned && (\n <div className=\"typography-regular-body-md block px-3 py-4 text-center text-content-secondary\">\n {emptyText}\n </div>\n )}\n </>\n );\n}\n"],"names":["jsx","jsxs","SpinnerIcon","AutocompleteOptionItem","Fragment","cn"],"mappings":";;;;;;;AAkCA,SAAS,oBAAoB,EAAE,YAA2C;AACxE,SACEA,2BAAAA,IAAC,QAAA,EAAK,WAAU,mGACb,SAAA,CACH;AAEJ;AAEO,SAAS,4BAA4B;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAqC;AACnC,MAAI,SAAS;AACX;AAAA;AAAA,MAEEC,2BAAAA,KAAC,OAAA,EAAI,MAAK,UAAS,WAAU,yCAC3B,UAAA;AAAA,QAAAD,2BAAAA,IAACE,YAAAA,aAAA,EAAY,WAAU,6CAAA,CAA6C;AAAA,QACnE,eACCF,2BAAAA,IAAC,QAAA,EAAK,WAAU,0DACb,UAAA,YAAA,CACH;AAAA,MAAA,EAAA,CAEJ;AAAA;AAAA,EAEJ;AAEA,QAAM,EAAE,QAAQ,WAAW,OAAA,IAAW;AACtC,QAAM,cAAc,OAAO,SAAS,KAAK,UAAU,SAAS;AAC5D,QAAM,YAAY,OAAO,SAAS;AAClC,QAAM,wBACJ,UAAU,SAAS,OAAO,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,QAAQ,QAAQ,CAAC,KAAK,eAAe,IAAI;AAE5F,MAAI,eAAe,WAAW,GAAG;AAC/B,WACEA,2BAAAA,IAAC,OAAA,EAAI,WAAU,iFACZ,UAAA,WACH;AAAA,EAEJ;AAEA,MAAI,SAAS;AACb,QAAM,kBAAkB,CAAC,WAA+B;AACtD,UAAM,QAAQ;AACd,UAAM,aAAa,UACf,oBAAoB,SAAS,OAAO,KAAK,IACzC,OAAO,UAAU;AACrB,WACEA,2BAAAA;AAAAA,MAACG,uBAAAA;AAAAA,MAAA;AAAA,QAEC;AAAA,QACA,UAAU,GAAG,SAAS,WAAW,KAAK;AAAA,QACtC;AAAA,QACA;AAAA,QACA,UAAU,UAAU;AAAA,QACpB,UAAU,MAAM,SAAS,MAAM;AAAA,QAC/B,cAAc,MAAM,aAAa,KAAK;AAAA,QACtC;AAAA,MAAA;AAAA,MARK,OAAO;AAAA,IAAA;AAAA,EAWlB;AAEA,QAAM,cAAc,eAAe;AACnC,QAAM,mBAAmB,0BAA0B,KAAK,OAAO,SAAS;AAExE,SACEF,2BAAAA,KAAAG,qBAAA,EACG,UAAA;AAAA,IAAA,OAAO,IAAI,eAAe;AAAA,IAC1B,UAAU,IAAI,eAAe;AAAA,IAC7B,OAAO,IAAI,CAAC,SAAS,iBAAiB;AACrC,YAAM,YAAY,GAAG,SAAS,UAAU,QAAQ,MAAM,EAAE;AACxD,YAAM,cAAc,qBAChB,mBAAmB,QAAQ,KAAK,IAChC,QAAQ,MAAM;AAClB;AAAA;AAAA,QAEEH,2BAAAA;AAAAA,UAAC;AAAA,UAAA;AAAA,YAEC,MAAK;AAAA,YACL,mBAAiB;AAAA,YACjB,WAAWI,GAAAA;AAAAA,cACT,eAAe,iBAAiB,KAAK;AAAA,YAAA;AAAA,YAGvC,UAAA;AAAA,cAAAL,2BAAAA,IAAC,OAAA,EAAI,MAAK,gBAAe,IAAI,WAC1B,+BACC,cAEAA,2BAAAA,IAAC,qBAAA,EAAqB,UAAA,YAAA,CAAY,GAEtC;AAAA,cACC,QAAQ,QAAQ,IAAI,eAAe;AAAA,YAAA;AAAA,UAAA;AAAA,UAd/B,QAAQ,MAAM;AAAA,QAAA;AAAA;AAAA,IAiBzB,CAAC;AAAA,IACA,gBAAgB,gBAAgB,YAAY;AAAA,IAC5C,oBACCA,2BAAAA,IAAC,OAAA,EAAI,WAAU,iFACZ,UAAA,UAAA,CACH;AAAA,EAAA,GAEJ;AAEJ;;"}
|
|
@@ -9,7 +9,39 @@ function defaultFilter(option, query) {
|
|
|
9
9
|
function getLabel(option) {
|
|
10
10
|
return option.label ?? option.value;
|
|
11
11
|
}
|
|
12
|
+
function getVisibleSections(options, groups, filter, query) {
|
|
13
|
+
const pinned = [];
|
|
14
|
+
const ungrouped = [];
|
|
15
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
16
|
+
const knownGroupIds = new Set(groups?.map((g) => g.id));
|
|
17
|
+
const lowerQuery = query.toLowerCase();
|
|
18
|
+
const groupMatchedByHeading = new Set(
|
|
19
|
+
query ? (groups ?? []).filter((g) => g.label.toLowerCase().includes(lowerQuery)).map((g) => g.id) : []
|
|
20
|
+
);
|
|
21
|
+
for (const option of options) {
|
|
22
|
+
if (option.pinned) {
|
|
23
|
+
pinned.push(option);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const inMatchedGroup = !!option.groupId && groupMatchedByHeading.has(option.groupId);
|
|
27
|
+
if (query && !inMatchedGroup && !filter(option, query)) continue;
|
|
28
|
+
if (option.groupId && knownGroupIds.has(option.groupId)) {
|
|
29
|
+
const bucket = byGroup.get(option.groupId) ?? [];
|
|
30
|
+
bucket.push(option);
|
|
31
|
+
byGroup.set(option.groupId, bucket);
|
|
32
|
+
} else {
|
|
33
|
+
ungrouped.push(option);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const visibleGroups = (groups ?? []).map((group) => ({ group, options: byGroup.get(group.id) ?? [] })).filter((section) => section.options.length > 0);
|
|
37
|
+
return { pinned, ungrouped, groups: visibleGroups };
|
|
38
|
+
}
|
|
39
|
+
function flattenVisibleSections(sections) {
|
|
40
|
+
return [...sections.pinned, ...sections.ungrouped, ...sections.groups.flatMap((s) => s.options)];
|
|
41
|
+
}
|
|
12
42
|
exports.CREATE_PREFIX = CREATE_PREFIX;
|
|
13
43
|
exports.defaultFilter = defaultFilter;
|
|
44
|
+
exports.flattenVisibleSections = flattenVisibleSections;
|
|
14
45
|
exports.getLabel = getLabel;
|
|
46
|
+
exports.getVisibleSections = getVisibleSections;
|
|
15
47
|
//# sourceMappingURL=constants.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.cjs","sources":["../../../../src/components/Autocomplete/constants.ts"],"sourcesContent":["import type { AutocompleteOption } from \"./Autocomplete\";\n\nexport const CREATE_PREFIX = \"__create__\";\n\nexport function defaultFilter(option: AutocompleteOption, query: string): boolean {\n const label = option.label ?? option.value;\n return label.toLowerCase().includes(query.toLowerCase());\n}\n\nexport function getLabel(option: AutocompleteOption): string {\n return option.label ?? option.value;\n}\n"],"names":[],"mappings":";;;AAEO,MAAM,gBAAgB;AAEtB,SAAS,cAAc,QAA4B,OAAwB;AAChF,QAAM,QAAQ,OAAO,SAAS,OAAO;AACrC,SAAO,MAAM,YAAA,EAAc,SAAS,MAAM,aAAa;AACzD;AAEO,SAAS,SAAS,QAAoC;AAC3D,SAAO,OAAO,SAAS,OAAO;AAChC
|
|
1
|
+
{"version":3,"file":"constants.cjs","sources":["../../../../src/components/Autocomplete/constants.ts"],"sourcesContent":["import type { AutocompleteGroup, AutocompleteOption } from \"./Autocomplete\";\n\nexport const CREATE_PREFIX = \"__create__\";\n\nexport function defaultFilter(option: AutocompleteOption, query: string): boolean {\n const label = option.label ?? option.value;\n return label.toLowerCase().includes(query.toLowerCase());\n}\n\nexport function getLabel(option: AutocompleteOption): string {\n return option.label ?? option.value;\n}\n\n/**\n * Section structure consumed by `AutocompleteDropdownContent` to render the\n * grouped layout. `pinned` options bypass the filter; `ungrouped` options\n * have no `groupId`; `groups` preserves the order declared by the consumer's\n * `groups` prop and omits any group with zero visible options.\n *\n * The flat `visibleOptions` list (built in `useAutocomplete`) keeps the same\n * order as this structure (pinned → ungrouped → groups in order, options in\n * declaration order within each section) so `aria-activedescendant` and\n * keyboard navigation work against a single flat index.\n */\nexport interface AutocompleteVisibleSection {\n group: AutocompleteGroup;\n options: AutocompleteOption[];\n}\n\nexport interface AutocompleteVisibleSections {\n pinned: AutocompleteOption[];\n ungrouped: AutocompleteOption[];\n groups: AutocompleteVisibleSection[];\n}\n\n/**\n * Splits `options` into pinned / ungrouped / per-group buckets, applies the\n * provided filter to non-pinned options only, and drops empty groups.\n *\n * Group-heading matching: when the query matches a group's `label`\n * (case-insensitive substring, same as the default option filter), every\n * option under that group is kept regardless of whether it individually\n * matches the per-option `filter`. This supports the common \"group heading\n * is the searchable label, items are sub-rows\" shape (e.g. group =\n * product name, items = prices). Pinned options always bypass filtering.\n */\nexport function getVisibleSections(\n options: AutocompleteOption[],\n groups: AutocompleteGroup[] | undefined,\n filter: (option: AutocompleteOption, query: string) => boolean,\n query: string,\n): AutocompleteVisibleSections {\n const pinned: AutocompleteOption[] = [];\n const ungrouped: AutocompleteOption[] = [];\n const byGroup = new Map<string, AutocompleteOption[]>();\n const knownGroupIds = new Set(groups?.map((g) => g.id));\n\n const lowerQuery = query.toLowerCase();\n const groupMatchedByHeading = new Set<string>(\n query\n ? (groups ?? []).filter((g) => g.label.toLowerCase().includes(lowerQuery)).map((g) => g.id)\n : [],\n );\n\n for (const option of options) {\n if (option.pinned) {\n pinned.push(option);\n continue;\n }\n const inMatchedGroup = !!option.groupId && groupMatchedByHeading.has(option.groupId);\n if (query && !inMatchedGroup && !filter(option, query)) continue;\n if (option.groupId && knownGroupIds.has(option.groupId)) {\n const bucket = byGroup.get(option.groupId) ?? [];\n bucket.push(option);\n byGroup.set(option.groupId, bucket);\n } else {\n ungrouped.push(option);\n }\n }\n\n const visibleGroups: AutocompleteVisibleSection[] = (groups ?? [])\n .map((group) => ({ group, options: byGroup.get(group.id) ?? [] }))\n .filter((section) => section.options.length > 0);\n\n return { pinned, ungrouped, groups: visibleGroups };\n}\n\n/** Flattens visible sections into the cursor-indexed list. */\nexport function flattenVisibleSections(\n sections: AutocompleteVisibleSections,\n): AutocompleteOption[] {\n return [...sections.pinned, ...sections.ungrouped, ...sections.groups.flatMap((s) => s.options)];\n}\n"],"names":[],"mappings":";;;AAEO,MAAM,gBAAgB;AAEtB,SAAS,cAAc,QAA4B,OAAwB;AAChF,QAAM,QAAQ,OAAO,SAAS,OAAO;AACrC,SAAO,MAAM,YAAA,EAAc,SAAS,MAAM,aAAa;AACzD;AAEO,SAAS,SAAS,QAAoC;AAC3D,SAAO,OAAO,SAAS,OAAO;AAChC;AAmCO,SAAS,mBACd,SACA,QACA,QACA,OAC6B;AAC7B,QAAM,SAA+B,CAAA;AACrC,QAAM,YAAkC,CAAA;AACxC,QAAM,8BAAc,IAAA;AACpB,QAAM,gBAAgB,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AAEtD,QAAM,aAAa,MAAM,YAAA;AACzB,QAAM,wBAAwB,IAAI;AAAA,IAChC,SACK,UAAU,CAAA,GAAI,OAAO,CAAC,MAAM,EAAE,MAAM,YAAA,EAAc,SAAS,UAAU,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,IACxF,CAAA;AAAA,EAAC;AAGP,aAAW,UAAU,SAAS;AAC5B,QAAI,OAAO,QAAQ;AACjB,aAAO,KAAK,MAAM;AAClB;AAAA,IACF;AACA,UAAM,iBAAiB,CAAC,CAAC,OAAO,WAAW,sBAAsB,IAAI,OAAO,OAAO;AACnF,QAAI,SAAS,CAAC,kBAAkB,CAAC,OAAO,QAAQ,KAAK,EAAG;AACxD,QAAI,OAAO,WAAW,cAAc,IAAI,OAAO,OAAO,GAAG;AACvD,YAAM,SAAS,QAAQ,IAAI,OAAO,OAAO,KAAK,CAAA;AAC9C,aAAO,KAAK,MAAM;AAClB,cAAQ,IAAI,OAAO,SAAS,MAAM;AAAA,IACpC,OAAO;AACL,gBAAU,KAAK,MAAM;AAAA,IACvB;AAAA,EACF;AAEA,QAAM,iBAA+C,UAAU,CAAA,GAC5D,IAAI,CAAC,WAAW,EAAE,OAAO,SAAS,QAAQ,IAAI,MAAM,EAAE,KAAK,CAAA,IAAK,EAChE,OAAO,CAAC,YAAY,QAAQ,QAAQ,SAAS,CAAC;AAEjD,SAAO,EAAE,QAAQ,WAAW,QAAQ,cAAA;AACtC;AAGO,SAAS,uBACd,UACsB;AACtB,SAAO,CAAC,GAAG,SAAS,QAAQ,GAAG,SAAS,WAAW,GAAG,SAAS,OAAO,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;AACjG;;;;;;"}
|
|
@@ -24,6 +24,7 @@ function useAutocomplete(props) {
|
|
|
24
24
|
const {
|
|
25
25
|
id,
|
|
26
26
|
options,
|
|
27
|
+
groups,
|
|
27
28
|
disabled = false,
|
|
28
29
|
filterFn,
|
|
29
30
|
creatable = false,
|
|
@@ -63,19 +64,22 @@ function useAutocomplete(props) {
|
|
|
63
64
|
);
|
|
64
65
|
const [activeIndex, setActiveIndex] = React__namespace.useState(-1);
|
|
65
66
|
const filter = filterFn ?? constants.defaultFilter;
|
|
66
|
-
const filteredOptions = React__namespace.useMemo(() => {
|
|
67
|
-
if (!searchText) return options;
|
|
68
|
-
return options.filter((o) => filter(o, searchText));
|
|
69
|
-
}, [options, searchText, filter]);
|
|
70
67
|
const showCreate = creatable && searchText.length > 0 && !options.some((o) => (o.label ?? o.value).toLowerCase() === searchText.toLowerCase());
|
|
71
|
-
const
|
|
72
|
-
if (!showCreate) return
|
|
73
|
-
|
|
68
|
+
const createOption = React__namespace.useMemo(() => {
|
|
69
|
+
if (!showCreate) return null;
|
|
70
|
+
return {
|
|
74
71
|
value: `${constants.CREATE_PREFIX}${searchText}`,
|
|
75
72
|
label: creatableLabel ? creatableLabel(searchText) : searchText
|
|
76
73
|
};
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
}, [showCreate, searchText, creatableLabel]);
|
|
75
|
+
const visibleSections = React__namespace.useMemo(
|
|
76
|
+
() => constants.getVisibleSections(options, groups, filter, searchText),
|
|
77
|
+
[options, groups, filter, searchText]
|
|
78
|
+
);
|
|
79
|
+
const visibleOptions = React__namespace.useMemo(() => {
|
|
80
|
+
const flat = constants.flattenVisibleSections(visibleSections);
|
|
81
|
+
return createOption ? [...flat, createOption] : flat;
|
|
82
|
+
}, [visibleSections, createOption]);
|
|
79
83
|
const prevOptionsLengthRef = React__namespace.useRef(visibleOptions.length);
|
|
80
84
|
const prevSearchTextRef = React__namespace.useRef(searchText);
|
|
81
85
|
React__namespace.useEffect(() => {
|
|
@@ -264,6 +268,8 @@ function useAutocomplete(props) {
|
|
|
264
268
|
selectedMultiOptions,
|
|
265
269
|
searchText,
|
|
266
270
|
visibleOptions,
|
|
271
|
+
visibleSections,
|
|
272
|
+
createOption,
|
|
267
273
|
activeIndex,
|
|
268
274
|
activeDescendantId,
|
|
269
275
|
hasClearableValue,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAutocomplete.cjs","sources":["../../../../src/components/Autocomplete/useAutocomplete.ts"],"sourcesContent":["import * as React from \"react\";\nimport type { AutocompleteOption, AutocompleteProps } from \"./Autocomplete\";\nimport { CREATE_PREFIX, defaultFilter, getLabel } from \"./constants\";\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Combobox hook managing interconnected controlled/uncontrolled state\nexport function useAutocomplete(props: AutocompleteProps) {\n const {\n id,\n options,\n disabled = false,\n filterFn,\n creatable = false,\n creatableLabel,\n onCreate,\n inputValue: inputValueProp,\n onInputChange,\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n } = props;\n\n const generatedId = React.useId();\n const inputId = id ?? generatedId;\n const helperTextId = `${inputId}-helper`;\n const listboxId = `${inputId}-listbox`;\n\n const inputRef = React.useRef<HTMLInputElement>(null);\n const listRef = React.useRef<HTMLDivElement>(null);\n\n const isMulti = props.multiple === true;\n\n // --- single-select state ---\n const [internalValue, setInternalValue] = React.useState<string | null>(\n (!isMulti && props.defaultValue) || null,\n );\n const selectedValue: string | null = !isMulti\n ? props.value !== undefined\n ? props.value\n : internalValue\n : null;\n\n // --- multi-select state ---\n const [internalMultiValue, setInternalMultiValue] = React.useState<string[]>(\n isMulti && props.defaultValue ? props.defaultValue : [],\n );\n const selectedMultiValues: string[] = isMulti\n ? props.value !== undefined\n ? props.value\n : internalMultiValue\n : [];\n\n // --- input text ---\n const [internalInputValue, setInternalInputValue] = React.useState(\"\");\n const searchText = inputValueProp !== undefined ? inputValueProp : internalInputValue;\n\n // --- open state ---\n const [internalOpen, setInternalOpen] = React.useState(defaultOpen);\n const isOpen = openProp !== undefined ? openProp : internalOpen;\n\n const setOpen = React.useCallback(\n (next: boolean) => {\n if (openProp === undefined) setInternalOpen(next);\n onOpenChange?.(next);\n },\n [openProp, onOpenChange],\n );\n\n // --- active index ---\n const [activeIndex, setActiveIndex] = React.useState(-1);\n\n // --- filtering ---\n const filter = filterFn ?? defaultFilter;\n\n const filteredOptions = React.useMemo(() => {\n if (!searchText) return options;\n return options.filter((o) => filter(o, searchText));\n }, [options, searchText, filter]);\n\n const showCreate =\n creatable &&\n searchText.length > 0 &&\n !options.some((o) => (o.label ?? o.value).toLowerCase() === searchText.toLowerCase());\n\n const visibleOptions = React.useMemo(() => {\n if (!showCreate) return filteredOptions;\n const createOption: AutocompleteOption = {\n value: `${CREATE_PREFIX}${searchText}`,\n label: creatableLabel ? creatableLabel(searchText) : searchText,\n };\n return [...filteredOptions, createOption];\n }, [filteredOptions, showCreate, searchText, creatableLabel]);\n\n const prevOptionsLengthRef = React.useRef(visibleOptions.length);\n const prevSearchTextRef = React.useRef(searchText);\n\n React.useEffect(() => {\n if (\n searchText !== prevSearchTextRef.current ||\n visibleOptions.length !== prevOptionsLengthRef.current\n ) {\n setActiveIndex(-1);\n prevSearchTextRef.current = searchText;\n prevOptionsLengthRef.current = visibleOptions.length;\n }\n }, [searchText, visibleOptions.length]);\n\n // --- scroll active option into view ---\n React.useEffect(() => {\n if (activeIndex < 0 || !listRef.current) return;\n const el = listRef.current.querySelector(`[data-option-index=\"${activeIndex}\"]`);\n if (typeof el?.scrollIntoView === \"function\") {\n el.scrollIntoView({ block: \"nearest\" });\n }\n }, [activeIndex]);\n\n // --- helpers ---\n function clearInputText() {\n if (inputValueProp === undefined) setInternalInputValue(\"\");\n onInputChange?.(\"\");\n }\n\n function selectSingle(val: string | null) {\n if (!isMulti && props.value === undefined) setInternalValue(val);\n if (!isMulti) (props as { onChange?: (v: string | null) => void }).onChange?.(val);\n }\n\n function toggleMulti(val: string) {\n const next = selectedMultiValues.includes(val)\n ? selectedMultiValues.filter((v) => v !== val)\n : [...selectedMultiValues, val];\n if (isMulti && props.value === undefined) setInternalMultiValue(next);\n if (isMulti) (props as { onChange?: (v: string[]) => void }).onChange?.(next);\n }\n\n function handleSelect(option: AutocompleteOption) {\n if (option.value.startsWith(CREATE_PREFIX)) {\n const raw = option.value.slice(CREATE_PREFIX.length);\n onCreate?.(raw);\n if (isMulti) {\n toggleMulti(raw);\n } else {\n selectSingle(raw);\n setOpen(false);\n }\n clearInputText();\n return;\n }\n\n if (isMulti) {\n toggleMulti(option.value);\n clearInputText();\n } else {\n selectSingle(option.value);\n clearInputText();\n setOpen(false);\n }\n }\n\n function handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {\n const val = e.target.value;\n if (inputValueProp === undefined) setInternalInputValue(val);\n onInputChange?.(val);\n if (!isOpen) setOpen(true);\n }\n\n function findNextEnabled(start: number, direction: 1 | -1): number | null {\n let idx = start;\n while (idx >= 0 && idx < visibleOptions.length && visibleOptions[idx]?.disabled) {\n idx += direction;\n }\n return idx >= 0 && idx < visibleOptions.length ? idx : null;\n }\n\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Flat switch is clearer than splitting into separate handler functions\n function handleKeyDown(e: React.KeyboardEvent) {\n if (disabled) return;\n\n switch (e.key) {\n case \"ArrowDown\": {\n e.preventDefault();\n if (!isOpen) {\n setOpen(true);\n return;\n }\n setActiveIndex((prev) => findNextEnabled(prev + 1, 1) ?? prev);\n break;\n }\n case \"ArrowUp\": {\n e.preventDefault();\n if (!isOpen) {\n setOpen(true);\n return;\n }\n setActiveIndex((prev) => findNextEnabled(prev - 1, -1) ?? prev);\n break;\n }\n case \"Enter\": {\n if (isOpen && activeIndex >= 0 && activeIndex < visibleOptions.length) {\n e.preventDefault();\n const opt = visibleOptions[activeIndex];\n if (opt && !opt.disabled) handleSelect(opt);\n }\n break;\n }\n case \"Escape\": {\n e.preventDefault();\n if (isOpen) setOpen(false);\n break;\n }\n case \"Backspace\": {\n if (isMulti && searchText === \"\" && selectedMultiValues.length > 0) {\n const lastVal = selectedMultiValues[selectedMultiValues.length - 1];\n if (lastVal !== undefined) toggleMulti(lastVal);\n }\n break;\n }\n case \"Home\": {\n if (isOpen) {\n e.preventDefault();\n const idx = findNextEnabled(0, 1);\n if (idx !== null) setActiveIndex(idx);\n }\n break;\n }\n case \"End\": {\n if (isOpen) {\n e.preventDefault();\n const idx = findNextEnabled(visibleOptions.length - 1, -1);\n if (idx !== null) setActiveIndex(idx);\n }\n break;\n }\n }\n }\n\n function handleClear(e: React.MouseEvent) {\n e.stopPropagation();\n if (isMulti) {\n if (props.value === undefined) setInternalMultiValue([]);\n if (isMulti) (props as { onChange?: (v: string[]) => void }).onChange?.([]);\n } else {\n selectSingle(null);\n }\n clearInputText();\n inputRef.current?.focus();\n }\n\n function handleBlur(e: React.FocusEvent<HTMLInputElement>) {\n const container = e.currentTarget.closest(\"[data-autocomplete-root]\");\n if (container && e.relatedTarget instanceof Node && container.contains(e.relatedTarget)) {\n return;\n }\n if (\n e.relatedTarget instanceof Node &&\n (e.relatedTarget as Element).closest?.(\"[data-radix-popper-content-wrapper]\")\n ) {\n return;\n }\n if (isOpen) setOpen(false);\n }\n\n function handleFocus() {\n if (!disabled && !isOpen) setOpen(true);\n }\n\n function handleContainerClick() {\n if (!disabled) {\n inputRef.current?.focus();\n if (!isOpen) setOpen(true);\n }\n }\n\n function handleOpenChange(next: boolean) {\n setOpen(next);\n if (!next) {\n clearInputText();\n setActiveIndex(-1);\n }\n }\n\n const selectedOption = React.useMemo(\n () => options.find((o) => o.value === selectedValue),\n [options, selectedValue],\n );\n\n const selectedMultiOptions = React.useMemo(\n () =>\n selectedMultiValues\n .map((v) => options.find((o) => o.value === v))\n .filter(Boolean) as AutocompleteOption[],\n [options, selectedMultiValues],\n );\n\n const hasClearableValue = isMulti ? selectedMultiValues.length > 0 : selectedValue != null;\n\n const displayInputValue = React.useMemo(() => {\n if (searchText) return searchText;\n if (isMulti || isOpen) return \"\";\n return selectedOption ? getLabel(selectedOption) : \"\";\n }, [searchText, isMulti, isOpen, selectedOption]);\n\n const activeDescendantId = activeIndex >= 0 ? `${listboxId}-option-${activeIndex}` : undefined;\n\n return {\n inputId,\n helperTextId,\n listboxId,\n inputRef,\n listRef,\n isMulti,\n isOpen,\n selectedValue,\n selectedMultiValues,\n selectedMultiOptions,\n searchText,\n visibleOptions,\n activeIndex,\n activeDescendantId,\n hasClearableValue,\n displayInputValue,\n setActiveIndex,\n handleSelect,\n handleInputChange,\n handleKeyDown,\n handleClear,\n handleBlur,\n handleFocus,\n handleContainerClick,\n handleOpenChange,\n toggleMulti,\n };\n}\n"],"names":["React","defaultFilter","CREATE_PREFIX","getLabel"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useAutocomplete.cjs","sources":["../../../../src/components/Autocomplete/useAutocomplete.ts"],"sourcesContent":["import * as React from \"react\";\nimport type { AutocompleteOption, AutocompleteProps } from \"./Autocomplete\";\nimport {\n type AutocompleteVisibleSections,\n CREATE_PREFIX,\n defaultFilter,\n flattenVisibleSections,\n getLabel,\n getVisibleSections,\n} from \"./constants\";\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Combobox hook managing interconnected controlled/uncontrolled state\nexport function useAutocomplete(props: AutocompleteProps) {\n const {\n id,\n options,\n groups,\n disabled = false,\n filterFn,\n creatable = false,\n creatableLabel,\n onCreate,\n inputValue: inputValueProp,\n onInputChange,\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n } = props;\n\n const generatedId = React.useId();\n const inputId = id ?? generatedId;\n const helperTextId = `${inputId}-helper`;\n const listboxId = `${inputId}-listbox`;\n\n const inputRef = React.useRef<HTMLInputElement>(null);\n const listRef = React.useRef<HTMLDivElement>(null);\n\n const isMulti = props.multiple === true;\n\n // --- single-select state ---\n const [internalValue, setInternalValue] = React.useState<string | null>(\n (!isMulti && props.defaultValue) || null,\n );\n const selectedValue: string | null = !isMulti\n ? props.value !== undefined\n ? props.value\n : internalValue\n : null;\n\n // --- multi-select state ---\n const [internalMultiValue, setInternalMultiValue] = React.useState<string[]>(\n isMulti && props.defaultValue ? props.defaultValue : [],\n );\n const selectedMultiValues: string[] = isMulti\n ? props.value !== undefined\n ? props.value\n : internalMultiValue\n : [];\n\n // --- input text ---\n const [internalInputValue, setInternalInputValue] = React.useState(\"\");\n const searchText = inputValueProp !== undefined ? inputValueProp : internalInputValue;\n\n // --- open state ---\n const [internalOpen, setInternalOpen] = React.useState(defaultOpen);\n const isOpen = openProp !== undefined ? openProp : internalOpen;\n\n const setOpen = React.useCallback(\n (next: boolean) => {\n if (openProp === undefined) setInternalOpen(next);\n onOpenChange?.(next);\n },\n [openProp, onOpenChange],\n );\n\n // --- active index ---\n const [activeIndex, setActiveIndex] = React.useState(-1);\n\n // --- filtering ---\n const filter = filterFn ?? defaultFilter;\n\n const showCreate =\n creatable &&\n searchText.length > 0 &&\n !options.some((o) => (o.label ?? o.value).toLowerCase() === searchText.toLowerCase());\n\n const createOption: AutocompleteOption | null = React.useMemo(() => {\n if (!showCreate) return null;\n return {\n value: `${CREATE_PREFIX}${searchText}`,\n label: creatableLabel ? creatableLabel(searchText) : searchText,\n };\n }, [showCreate, searchText, creatableLabel]);\n\n const visibleSections: AutocompleteVisibleSections = React.useMemo(\n () => getVisibleSections(options, groups, filter, searchText),\n [options, groups, filter, searchText],\n );\n\n const visibleOptions = React.useMemo(() => {\n const flat = flattenVisibleSections(visibleSections);\n return createOption ? [...flat, createOption] : flat;\n }, [visibleSections, createOption]);\n\n const prevOptionsLengthRef = React.useRef(visibleOptions.length);\n const prevSearchTextRef = React.useRef(searchText);\n\n React.useEffect(() => {\n if (\n searchText !== prevSearchTextRef.current ||\n visibleOptions.length !== prevOptionsLengthRef.current\n ) {\n setActiveIndex(-1);\n prevSearchTextRef.current = searchText;\n prevOptionsLengthRef.current = visibleOptions.length;\n }\n }, [searchText, visibleOptions.length]);\n\n // --- scroll active option into view ---\n React.useEffect(() => {\n if (activeIndex < 0 || !listRef.current) return;\n const el = listRef.current.querySelector(`[data-option-index=\"${activeIndex}\"]`);\n if (typeof el?.scrollIntoView === \"function\") {\n el.scrollIntoView({ block: \"nearest\" });\n }\n }, [activeIndex]);\n\n // --- helpers ---\n function clearInputText() {\n if (inputValueProp === undefined) setInternalInputValue(\"\");\n onInputChange?.(\"\");\n }\n\n function selectSingle(val: string | null) {\n if (!isMulti && props.value === undefined) setInternalValue(val);\n if (!isMulti) (props as { onChange?: (v: string | null) => void }).onChange?.(val);\n }\n\n function toggleMulti(val: string) {\n const next = selectedMultiValues.includes(val)\n ? selectedMultiValues.filter((v) => v !== val)\n : [...selectedMultiValues, val];\n if (isMulti && props.value === undefined) setInternalMultiValue(next);\n if (isMulti) (props as { onChange?: (v: string[]) => void }).onChange?.(next);\n }\n\n function handleSelect(option: AutocompleteOption) {\n if (option.value.startsWith(CREATE_PREFIX)) {\n const raw = option.value.slice(CREATE_PREFIX.length);\n onCreate?.(raw);\n if (isMulti) {\n toggleMulti(raw);\n } else {\n selectSingle(raw);\n setOpen(false);\n }\n clearInputText();\n return;\n }\n\n if (isMulti) {\n toggleMulti(option.value);\n clearInputText();\n } else {\n selectSingle(option.value);\n clearInputText();\n setOpen(false);\n }\n }\n\n function handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {\n const val = e.target.value;\n if (inputValueProp === undefined) setInternalInputValue(val);\n onInputChange?.(val);\n if (!isOpen) setOpen(true);\n }\n\n function findNextEnabled(start: number, direction: 1 | -1): number | null {\n let idx = start;\n while (idx >= 0 && idx < visibleOptions.length && visibleOptions[idx]?.disabled) {\n idx += direction;\n }\n return idx >= 0 && idx < visibleOptions.length ? idx : null;\n }\n\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Flat switch is clearer than splitting into separate handler functions\n function handleKeyDown(e: React.KeyboardEvent) {\n if (disabled) return;\n\n switch (e.key) {\n case \"ArrowDown\": {\n e.preventDefault();\n if (!isOpen) {\n setOpen(true);\n return;\n }\n setActiveIndex((prev) => findNextEnabled(prev + 1, 1) ?? prev);\n break;\n }\n case \"ArrowUp\": {\n e.preventDefault();\n if (!isOpen) {\n setOpen(true);\n return;\n }\n setActiveIndex((prev) => findNextEnabled(prev - 1, -1) ?? prev);\n break;\n }\n case \"Enter\": {\n if (isOpen && activeIndex >= 0 && activeIndex < visibleOptions.length) {\n e.preventDefault();\n const opt = visibleOptions[activeIndex];\n if (opt && !opt.disabled) handleSelect(opt);\n }\n break;\n }\n case \"Escape\": {\n e.preventDefault();\n if (isOpen) setOpen(false);\n break;\n }\n case \"Backspace\": {\n if (isMulti && searchText === \"\" && selectedMultiValues.length > 0) {\n const lastVal = selectedMultiValues[selectedMultiValues.length - 1];\n if (lastVal !== undefined) toggleMulti(lastVal);\n }\n break;\n }\n case \"Home\": {\n if (isOpen) {\n e.preventDefault();\n const idx = findNextEnabled(0, 1);\n if (idx !== null) setActiveIndex(idx);\n }\n break;\n }\n case \"End\": {\n if (isOpen) {\n e.preventDefault();\n const idx = findNextEnabled(visibleOptions.length - 1, -1);\n if (idx !== null) setActiveIndex(idx);\n }\n break;\n }\n }\n }\n\n function handleClear(e: React.MouseEvent) {\n e.stopPropagation();\n if (isMulti) {\n if (props.value === undefined) setInternalMultiValue([]);\n if (isMulti) (props as { onChange?: (v: string[]) => void }).onChange?.([]);\n } else {\n selectSingle(null);\n }\n clearInputText();\n inputRef.current?.focus();\n }\n\n function handleBlur(e: React.FocusEvent<HTMLInputElement>) {\n const container = e.currentTarget.closest(\"[data-autocomplete-root]\");\n if (container && e.relatedTarget instanceof Node && container.contains(e.relatedTarget)) {\n return;\n }\n if (\n e.relatedTarget instanceof Node &&\n (e.relatedTarget as Element).closest?.(\"[data-radix-popper-content-wrapper]\")\n ) {\n return;\n }\n if (isOpen) setOpen(false);\n }\n\n function handleFocus() {\n if (!disabled && !isOpen) setOpen(true);\n }\n\n function handleContainerClick() {\n if (!disabled) {\n inputRef.current?.focus();\n if (!isOpen) setOpen(true);\n }\n }\n\n function handleOpenChange(next: boolean) {\n setOpen(next);\n if (!next) {\n clearInputText();\n setActiveIndex(-1);\n }\n }\n\n const selectedOption = React.useMemo(\n () => options.find((o) => o.value === selectedValue),\n [options, selectedValue],\n );\n\n const selectedMultiOptions = React.useMemo(\n () =>\n selectedMultiValues\n .map((v) => options.find((o) => o.value === v))\n .filter(Boolean) as AutocompleteOption[],\n [options, selectedMultiValues],\n );\n\n const hasClearableValue = isMulti ? selectedMultiValues.length > 0 : selectedValue != null;\n\n const displayInputValue = React.useMemo(() => {\n if (searchText) return searchText;\n if (isMulti || isOpen) return \"\";\n return selectedOption ? getLabel(selectedOption) : \"\";\n }, [searchText, isMulti, isOpen, selectedOption]);\n\n const activeDescendantId = activeIndex >= 0 ? `${listboxId}-option-${activeIndex}` : undefined;\n\n return {\n inputId,\n helperTextId,\n listboxId,\n inputRef,\n listRef,\n isMulti,\n isOpen,\n selectedValue,\n selectedMultiValues,\n selectedMultiOptions,\n searchText,\n visibleOptions,\n visibleSections,\n createOption,\n activeIndex,\n activeDescendantId,\n hasClearableValue,\n displayInputValue,\n setActiveIndex,\n handleSelect,\n handleInputChange,\n handleKeyDown,\n handleClear,\n handleBlur,\n handleFocus,\n handleContainerClick,\n handleOpenChange,\n toggleMulti,\n };\n}\n"],"names":["React","defaultFilter","CREATE_PREFIX","getVisibleSections","flattenVisibleSections","getLabel"],"mappings":"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"}
|
|
@@ -54,7 +54,8 @@ const Autocomplete = React.forwardRef((props, ref) => {
|
|
|
54
54
|
loadingText,
|
|
55
55
|
emptyText = "No results",
|
|
56
56
|
renderOption,
|
|
57
|
-
renderTag
|
|
57
|
+
renderTag,
|
|
58
|
+
renderGroupHeading
|
|
58
59
|
} = props;
|
|
59
60
|
const ac = useAutocomplete(props);
|
|
60
61
|
React.useImperativeHandle(ref, () => ac.inputRef.current);
|
|
@@ -186,6 +187,8 @@ const Autocomplete = React.forwardRef((props, ref) => {
|
|
|
186
187
|
loadingText,
|
|
187
188
|
emptyText,
|
|
188
189
|
visibleOptions: ac.visibleOptions,
|
|
190
|
+
visibleSections: ac.visibleSections,
|
|
191
|
+
createOption: ac.createOption,
|
|
189
192
|
listboxId: ac.listboxId,
|
|
190
193
|
activeIndex: ac.activeIndex,
|
|
191
194
|
isMulti: ac.isMulti,
|
|
@@ -193,7 +196,8 @@ const Autocomplete = React.forwardRef((props, ref) => {
|
|
|
193
196
|
selectedValue: ac.selectedValue,
|
|
194
197
|
onSelect: ac.handleSelect,
|
|
195
198
|
onMouseEnter: ac.setActiveIndex,
|
|
196
|
-
renderOption
|
|
199
|
+
renderOption,
|
|
200
|
+
renderGroupHeading
|
|
197
201
|
}
|
|
198
202
|
)
|
|
199
203
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Autocomplete.mjs","sources":["../../../src/components/Autocomplete/Autocomplete.tsx"],"sourcesContent":["import * as Popover from \"@radix-ui/react-popover\";\nimport * as React from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { FLOATING_CONTENT_COLLISION_PADDING } from \"@/utils/floatingContentCollisionPadding\";\nimport { ChevronDownIcon } from \"../Icons/ChevronDownIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\nimport { SpinnerIcon } from \"../Icons/SpinnerIcon\";\nimport { AutocompleteDropdownContent } from \"./AutocompleteDropdownContent\";\nimport { AutocompleteTag } from \"./AutocompleteTag\";\nimport { useAutocomplete } from \"./useAutocomplete\";\n\nexport type AutocompleteSize = \"48\" | \"40\" | \"32\";\n\nexport interface AutocompleteOption {\n value: string;\n label?: string;\n disabled?: boolean;\n}\n\ninterface AutocompleteBaseProps {\n label?: string;\n \"aria-label\"?: string;\n \"aria-labelledby\"?: string;\n helperText?: string;\n /** @default \"48\" */\n size?: AutocompleteSize;\n /** @default false */\n error?: boolean;\n errorMessage?: string;\n placeholder?: string;\n leftIcon?: React.ReactNode;\n /** @default false */\n fullWidth?: boolean;\n /** @default false */\n disabled?: boolean;\n /** @default false */\n clearable?: boolean;\n clearAriaLabel?: string;\n id?: string;\n className?: string;\n options: AutocompleteOption[];\n inputValue?: string;\n onInputChange?: (value: string) => void;\n filterFn?: (option: AutocompleteOption, query: string) => boolean;\n /** @default false */\n creatable?: boolean;\n creatableLabel?: (inputValue: string) => string;\n onCreate?: (inputValue: string) => void;\n /** @default false */\n loading?: boolean;\n loadingText?: string;\n emptyText?: string;\n renderOption?: (\n option: AutocompleteOption,\n state: { selected: boolean; active: boolean },\n ) => React.ReactNode;\n renderTag?: (option: AutocompleteOption, onRemove: () => void) => React.ReactNode;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n}\n\ninterface AutocompleteSingleProps extends AutocompleteBaseProps {\n multiple?: false;\n value?: string | null;\n defaultValue?: string | null;\n onChange?: (value: string | null) => void;\n}\n\ninterface AutocompleteMultiProps extends AutocompleteBaseProps {\n multiple: true;\n value?: string[];\n defaultValue?: string[];\n onChange?: (values: string[]) => void;\n}\n\nexport type AutocompleteProps = AutocompleteSingleProps | AutocompleteMultiProps;\n\nconst CONTAINER_HEIGHT: Record<AutocompleteSize, string> = {\n \"48\": \"min-h-12\",\n \"40\": \"min-h-10\",\n \"32\": \"min-h-8\",\n};\n\nconst INPUT_SIZE_CLASSES: Record<AutocompleteSize, string> = {\n \"48\": \"typography-regular-body-lg\",\n \"40\": \"typography-regular-body-lg\",\n \"32\": \"typography-regular-body-md\",\n};\n\nconst PADDING_CLASSES: Record<AutocompleteSize, string> = {\n \"48\": \"px-4 py-1.5 gap-3\",\n \"40\": \"px-4 py-1 gap-3\",\n \"32\": \"px-3 py-1 gap-2\",\n};\n\nfunction warnMissingAccessibleName(label?: string, ariaLabel?: string, ariaLabelledBy?: string) {\n if (process.env.NODE_ENV !== \"production\") {\n if (!label && !ariaLabel && !ariaLabelledBy) {\n console.warn(\n \"Autocomplete: no accessible name provided. Pass a `label`, `aria-label`, or `aria-labelledby` prop.\",\n );\n }\n }\n}\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Conditional JSX branches in the render template\nexport const Autocomplete = React.forwardRef<HTMLInputElement, AutocompleteProps>((props, ref) => {\n const {\n label,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledby,\n helperText,\n size = \"48\",\n error = false,\n errorMessage,\n placeholder,\n leftIcon,\n fullWidth = false,\n disabled = false,\n clearable = false,\n clearAriaLabel = \"Clear\",\n className,\n loading = false,\n loadingText,\n emptyText = \"No results\",\n renderOption,\n renderTag,\n } = props;\n\n const ac = useAutocomplete(props);\n\n React.useImperativeHandle(ref, () => ac.inputRef.current as HTMLInputElement);\n\n warnMissingAccessibleName(label, ariaLabel, ariaLabelledby);\n\n const bottomText = error && errorMessage ? errorMessage : helperText;\n\n return (\n <Popover.Root open={ac.isOpen && !disabled} onOpenChange={ac.handleOpenChange}>\n <div\n className={cn(\"flex flex-col\", fullWidth && \"w-full\", className)}\n data-autocomplete-root=\"\"\n data-disabled={disabled ? \"\" : undefined}\n data-error={error ? \"\" : undefined}\n >\n {label && (\n <label\n htmlFor={ac.inputId}\n className=\"typography-semibold-body-sm px-1 pt-1 pb-2 text-content-primary\"\n >\n {label}\n </label>\n )}\n\n <Popover.Anchor asChild>\n {/* biome-ignore lint/a11y/noStaticElementInteractions: Container delegates click to the inner input */}\n {/* biome-ignore lint/a11y/useKeyWithClickEvents: Keyboard interaction is handled by the inner combobox input */}\n <div\n className={cn(\n \"flex flex-wrap items-center overflow-hidden rounded-sm border bg-neutral-alphas-100 has-focus-visible:shadow-focus-ring has-focus-visible:outline-none motion-safe:transition-colors\",\n error ? \"border-error-content\" : \"border-transparent\",\n !disabled && !error && \"hover:border-neutral-alphas-400\",\n ac.isOpen && !error && !disabled && \"border-neutral-alphas-400\",\n CONTAINER_HEIGHT[size],\n PADDING_CLASSES[size],\n disabled && \"opacity-50\",\n )}\n onClick={ac.handleContainerClick}\n >\n {leftIcon && (\n <div className=\"flex size-5 shrink-0 items-center justify-center text-content-secondary\">\n {leftIcon}\n </div>\n )}\n\n <div className=\"flex min-w-0 flex-1 flex-wrap items-center gap-1.5\">\n {ac.isMulti &&\n ac.selectedMultiOptions.map((opt) => (\n <AutocompleteTag\n key={opt.value}\n option={opt}\n disabled={disabled}\n onRemove={() => ac.toggleMulti(opt.value)}\n renderTag={renderTag}\n />\n ))}\n\n <input\n ref={ac.inputRef}\n id={ac.inputId}\n role=\"combobox\"\n type=\"text\"\n disabled={disabled}\n aria-expanded={ac.isOpen}\n aria-controls={ac.isOpen ? ac.listboxId : undefined}\n aria-activedescendant={ac.activeDescendantId}\n aria-autocomplete=\"list\"\n aria-describedby={bottomText ? ac.helperTextId : undefined}\n aria-invalid={error || undefined}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n autoComplete=\"off\"\n placeholder={\n ac.isMulti && ac.selectedMultiValues.length > 0 ? undefined : placeholder\n }\n value={ac.displayInputValue}\n onChange={ac.handleInputChange}\n onKeyDown={ac.handleKeyDown}\n onFocus={ac.handleFocus}\n onBlur={ac.handleBlur}\n className={cn(\n \"min-w-[40px] flex-1 truncate bg-transparent text-content-primary no-underline placeholder:text-content-secondary placeholder:opacity-40 focus:outline-none disabled:cursor-not-allowed\",\n INPUT_SIZE_CLASSES[size],\n )}\n />\n </div>\n\n <div className=\"flex shrink-0 items-center gap-1\">\n {loading && <SpinnerIcon className=\"size-4 animate-spin text-content-secondary\" />}\n {clearable && ac.hasClearableValue && !disabled && (\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={clearAriaLabel}\n className=\"flex size-5 shrink-0 cursor-pointer items-center justify-center text-content-secondary hover:text-content-primary active:scale-95\"\n onClick={ac.handleClear}\n >\n <CloseIcon className=\"size-4\" />\n </button>\n )}\n <div className=\"flex size-5 shrink-0 items-center justify-center text-content-secondary\">\n <ChevronDownIcon\n className={cn(\"size-5 transition-transform\", ac.isOpen && \"rotate-180\")}\n />\n </div>\n </div>\n </div>\n </Popover.Anchor>\n\n <Popover.Portal>\n <Popover.Content\n sideOffset={4}\n collisionPadding={FLOATING_CONTENT_COLLISION_PADDING}\n onOpenAutoFocus={(e) => e.preventDefault()}\n onCloseAutoFocus={(e) => e.preventDefault()}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\" }}\n className={cn(\n \"w-max min-w-(--radix-popper-anchor-width) max-w-(--radix-popover-content-available-width) overflow-hidden rounded-sm border border-neutral-alphas-200 bg-bg-primary text-content-primary shadow-[0_4px_16px_rgba(0,0,0,0.10)]\",\n \"data-[state=closed]:animate-out data-[state=open]:animate-in\",\n \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n \"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\",\n \"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2\",\n )}\n >\n <div\n ref={ac.listRef}\n id={ac.listboxId}\n role=\"listbox\"\n aria-label={label ?? ariaLabel ?? \"Options\"}\n aria-multiselectable={ac.isMulti || undefined}\n className=\"max-h-60 overflow-y-auto p-1\"\n >\n <AutocompleteDropdownContent\n loading={loading}\n loadingText={loadingText}\n emptyText={emptyText}\n visibleOptions={ac.visibleOptions}\n listboxId={ac.listboxId}\n activeIndex={ac.activeIndex}\n isMulti={ac.isMulti}\n selectedMultiValues={ac.selectedMultiValues}\n selectedValue={ac.selectedValue}\n onSelect={ac.handleSelect}\n onMouseEnter={ac.setActiveIndex}\n renderOption={renderOption}\n />\n </div>\n </Popover.Content>\n </Popover.Portal>\n\n {bottomText && (\n <p\n id={ac.helperTextId}\n className={cn(\n \"typography-regular-body-sm px-2 pt-1 pb-0.5\",\n error ? \"text-error-content\" : \"text-content-secondary\",\n )}\n >\n {bottomText}\n </p>\n )}\n </div>\n </Popover.Root>\n );\n});\n\nAutocomplete.displayName = \"Autocomplete\";\n"],"names":["Popover"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Autocomplete.mjs","sources":["../../../src/components/Autocomplete/Autocomplete.tsx"],"sourcesContent":["import * as Popover from \"@radix-ui/react-popover\";\nimport * as React from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { FLOATING_CONTENT_COLLISION_PADDING } from \"@/utils/floatingContentCollisionPadding\";\nimport { ChevronDownIcon } from \"../Icons/ChevronDownIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\nimport { SpinnerIcon } from \"../Icons/SpinnerIcon\";\nimport { AutocompleteDropdownContent } from \"./AutocompleteDropdownContent\";\nimport { AutocompleteTag } from \"./AutocompleteTag\";\nimport { useAutocomplete } from \"./useAutocomplete\";\n\nexport type AutocompleteSize = \"48\" | \"40\" | \"32\";\n\nexport interface AutocompleteOption {\n /** Unique value identifying the option. Returned via `onChange`. */\n value: string;\n /** Visible label. Falls back to `value` when omitted. */\n label?: string;\n /** When `true`, the option renders but cannot be selected. @default false */\n disabled?: boolean;\n /**\n * ID of the group this option belongs to. Must match an entry in the\n * component's `groups` prop. Options without a `groupId` render in an\n * implicit \"ungrouped\" bucket above the first declared group.\n */\n groupId?: string;\n /**\n * Pinned options bypass the search filter and stay visible at the top of\n * the list regardless of the current query. Useful for \"+ Create new\"\n * affordances. Pinned options ignore `groupId` and render before any\n * grouped or ungrouped content. @default false\n */\n pinned?: boolean;\n}\n\n/**\n * Describes a single group rendered above its matching options.\n * Indentation of nested rows (e.g. price under product) is not built in —\n * consumers control it via `renderOption` styling.\n */\nexport interface AutocompleteGroup {\n /** Stable identifier referenced by `option.groupId`. */\n id: string;\n /**\n * Group heading text. Used as the group's accessible name and matched\n * against the search query: when the query matches a group's `label`,\n * every option under that group is kept regardless of whether it\n * individually matches the per-option filter. This supports the common\n * \"heading is the searchable label, items are sub-rows\" shape (e.g.\n * heading = product name, items = prices).\n */\n label: string;\n}\n\ninterface AutocompleteBaseProps {\n label?: string;\n \"aria-label\"?: string;\n \"aria-labelledby\"?: string;\n helperText?: string;\n /** @default \"48\" */\n size?: AutocompleteSize;\n /** @default false */\n error?: boolean;\n errorMessage?: string;\n placeholder?: string;\n leftIcon?: React.ReactNode;\n /** @default false */\n fullWidth?: boolean;\n /** @default false */\n disabled?: boolean;\n /** @default false */\n clearable?: boolean;\n clearAriaLabel?: string;\n id?: string;\n className?: string;\n options: AutocompleteOption[];\n inputValue?: string;\n onInputChange?: (value: string) => void;\n filterFn?: (option: AutocompleteOption, query: string) => boolean;\n /** @default false */\n creatable?: boolean;\n creatableLabel?: (inputValue: string) => string;\n onCreate?: (inputValue: string) => void;\n /** @default false */\n loading?: boolean;\n loadingText?: string;\n emptyText?: string;\n renderOption?: (\n option: AutocompleteOption,\n state: { selected: boolean; active: boolean },\n ) => React.ReactNode;\n renderTag?: (option: AutocompleteOption, onRemove: () => void) => React.ReactNode;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n /**\n * Ordered list of groups. When provided, options whose `groupId` matches\n * an entry render under the corresponding heading. Groups with no visible\n * (post-filter) options collapse silently. Pinned options render above\n * everything; ungrouped options render between pinned and the first group.\n * Only one level of grouping is supported.\n */\n groups?: AutocompleteGroup[];\n /**\n * Custom renderer for group headings. The returned node is wrapped by the\n * component in an element carrying the `id` referenced by the surrounding\n * `role=\"group\"` wrapper's `aria-labelledby`, so consumers only need to\n * return visual content.\n */\n renderGroupHeading?: (group: AutocompleteGroup) => React.ReactNode;\n}\n\ninterface AutocompleteSingleProps extends AutocompleteBaseProps {\n multiple?: false;\n value?: string | null;\n defaultValue?: string | null;\n onChange?: (value: string | null) => void;\n}\n\ninterface AutocompleteMultiProps extends AutocompleteBaseProps {\n multiple: true;\n value?: string[];\n defaultValue?: string[];\n onChange?: (values: string[]) => void;\n}\n\nexport type AutocompleteProps = AutocompleteSingleProps | AutocompleteMultiProps;\n\nconst CONTAINER_HEIGHT: Record<AutocompleteSize, string> = {\n \"48\": \"min-h-12\",\n \"40\": \"min-h-10\",\n \"32\": \"min-h-8\",\n};\n\nconst INPUT_SIZE_CLASSES: Record<AutocompleteSize, string> = {\n \"48\": \"typography-regular-body-lg\",\n \"40\": \"typography-regular-body-lg\",\n \"32\": \"typography-regular-body-md\",\n};\n\nconst PADDING_CLASSES: Record<AutocompleteSize, string> = {\n \"48\": \"px-4 py-1.5 gap-3\",\n \"40\": \"px-4 py-1 gap-3\",\n \"32\": \"px-3 py-1 gap-2\",\n};\n\nfunction warnMissingAccessibleName(label?: string, ariaLabel?: string, ariaLabelledBy?: string) {\n if (process.env.NODE_ENV !== \"production\") {\n if (!label && !ariaLabel && !ariaLabelledBy) {\n console.warn(\n \"Autocomplete: no accessible name provided. Pass a `label`, `aria-label`, or `aria-labelledby` prop.\",\n );\n }\n }\n}\n\n/**\n * A combobox input with single- or multi-select, optional async loading, and\n * native support for grouped + pinned options.\n *\n * - Pass `groups` plus an `options` array whose entries reference each group\n * via `groupId` to render hierarchical lists with proper `role=\"group\"` +\n * `aria-labelledby` semantics. Options without a `groupId` render above\n * the first group; options marked `pinned` render above everything and\n * bypass the search filter.\n * - Indentation of nested rows (e.g. price under product) is controlled by\n * the consumer via `renderOption` styling — there is no built-in indent.\n *\n * @example\n * ```tsx\n * <Autocomplete\n * aria-label=\"Choose a product\"\n * options={[\n * { value: \"__new__\", label: \"+ Create new product\", pinned: true },\n * { value: \"product:abc\", label: \"Demo Product\", groupId: \"recent\" },\n * ]}\n * groups={[{ id: \"recent\", label: \"Recent products\" }]}\n * onChange={handleSelect}\n * />\n * ```\n */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Conditional JSX branches in the render template\nexport const Autocomplete = React.forwardRef<HTMLInputElement, AutocompleteProps>((props, ref) => {\n const {\n label,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledby,\n helperText,\n size = \"48\",\n error = false,\n errorMessage,\n placeholder,\n leftIcon,\n fullWidth = false,\n disabled = false,\n clearable = false,\n clearAriaLabel = \"Clear\",\n className,\n loading = false,\n loadingText,\n emptyText = \"No results\",\n renderOption,\n renderTag,\n renderGroupHeading,\n } = props;\n\n const ac = useAutocomplete(props);\n\n React.useImperativeHandle(ref, () => ac.inputRef.current as HTMLInputElement);\n\n warnMissingAccessibleName(label, ariaLabel, ariaLabelledby);\n\n const bottomText = error && errorMessage ? errorMessage : helperText;\n\n return (\n <Popover.Root open={ac.isOpen && !disabled} onOpenChange={ac.handleOpenChange}>\n <div\n className={cn(\"flex flex-col\", fullWidth && \"w-full\", className)}\n data-autocomplete-root=\"\"\n data-disabled={disabled ? \"\" : undefined}\n data-error={error ? \"\" : undefined}\n >\n {label && (\n <label\n htmlFor={ac.inputId}\n className=\"typography-semibold-body-sm px-1 pt-1 pb-2 text-content-primary\"\n >\n {label}\n </label>\n )}\n\n <Popover.Anchor asChild>\n {/* biome-ignore lint/a11y/noStaticElementInteractions: Container delegates click to the inner input */}\n {/* biome-ignore lint/a11y/useKeyWithClickEvents: Keyboard interaction is handled by the inner combobox input */}\n <div\n className={cn(\n \"flex flex-wrap items-center overflow-hidden rounded-sm border bg-neutral-alphas-100 has-focus-visible:shadow-focus-ring has-focus-visible:outline-none motion-safe:transition-colors\",\n error ? \"border-error-content\" : \"border-transparent\",\n !disabled && !error && \"hover:border-neutral-alphas-400\",\n ac.isOpen && !error && !disabled && \"border-neutral-alphas-400\",\n CONTAINER_HEIGHT[size],\n PADDING_CLASSES[size],\n disabled && \"opacity-50\",\n )}\n onClick={ac.handleContainerClick}\n >\n {leftIcon && (\n <div className=\"flex size-5 shrink-0 items-center justify-center text-content-secondary\">\n {leftIcon}\n </div>\n )}\n\n <div className=\"flex min-w-0 flex-1 flex-wrap items-center gap-1.5\">\n {ac.isMulti &&\n ac.selectedMultiOptions.map((opt) => (\n <AutocompleteTag\n key={opt.value}\n option={opt}\n disabled={disabled}\n onRemove={() => ac.toggleMulti(opt.value)}\n renderTag={renderTag}\n />\n ))}\n\n <input\n ref={ac.inputRef}\n id={ac.inputId}\n role=\"combobox\"\n type=\"text\"\n disabled={disabled}\n aria-expanded={ac.isOpen}\n aria-controls={ac.isOpen ? ac.listboxId : undefined}\n aria-activedescendant={ac.activeDescendantId}\n aria-autocomplete=\"list\"\n aria-describedby={bottomText ? ac.helperTextId : undefined}\n aria-invalid={error || undefined}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n autoComplete=\"off\"\n placeholder={\n ac.isMulti && ac.selectedMultiValues.length > 0 ? undefined : placeholder\n }\n value={ac.displayInputValue}\n onChange={ac.handleInputChange}\n onKeyDown={ac.handleKeyDown}\n onFocus={ac.handleFocus}\n onBlur={ac.handleBlur}\n className={cn(\n \"min-w-[40px] flex-1 truncate bg-transparent text-content-primary no-underline placeholder:text-content-secondary placeholder:opacity-40 focus:outline-none disabled:cursor-not-allowed\",\n INPUT_SIZE_CLASSES[size],\n )}\n />\n </div>\n\n <div className=\"flex shrink-0 items-center gap-1\">\n {loading && <SpinnerIcon className=\"size-4 animate-spin text-content-secondary\" />}\n {clearable && ac.hasClearableValue && !disabled && (\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={clearAriaLabel}\n className=\"flex size-5 shrink-0 cursor-pointer items-center justify-center text-content-secondary hover:text-content-primary active:scale-95\"\n onClick={ac.handleClear}\n >\n <CloseIcon className=\"size-4\" />\n </button>\n )}\n <div className=\"flex size-5 shrink-0 items-center justify-center text-content-secondary\">\n <ChevronDownIcon\n className={cn(\"size-5 transition-transform\", ac.isOpen && \"rotate-180\")}\n />\n </div>\n </div>\n </div>\n </Popover.Anchor>\n\n <Popover.Portal>\n <Popover.Content\n sideOffset={4}\n collisionPadding={FLOATING_CONTENT_COLLISION_PADDING}\n onOpenAutoFocus={(e) => e.preventDefault()}\n onCloseAutoFocus={(e) => e.preventDefault()}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\" }}\n className={cn(\n \"w-max min-w-(--radix-popper-anchor-width) max-w-(--radix-popover-content-available-width) overflow-hidden rounded-sm border border-neutral-alphas-200 bg-bg-primary text-content-primary shadow-[0_4px_16px_rgba(0,0,0,0.10)]\",\n \"data-[state=closed]:animate-out data-[state=open]:animate-in\",\n \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n \"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\",\n \"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2\",\n )}\n >\n <div\n ref={ac.listRef}\n id={ac.listboxId}\n role=\"listbox\"\n aria-label={label ?? ariaLabel ?? \"Options\"}\n aria-multiselectable={ac.isMulti || undefined}\n className=\"max-h-60 overflow-y-auto p-1\"\n >\n <AutocompleteDropdownContent\n loading={loading}\n loadingText={loadingText}\n emptyText={emptyText}\n visibleOptions={ac.visibleOptions}\n visibleSections={ac.visibleSections}\n createOption={ac.createOption}\n listboxId={ac.listboxId}\n activeIndex={ac.activeIndex}\n isMulti={ac.isMulti}\n selectedMultiValues={ac.selectedMultiValues}\n selectedValue={ac.selectedValue}\n onSelect={ac.handleSelect}\n onMouseEnter={ac.setActiveIndex}\n renderOption={renderOption}\n renderGroupHeading={renderGroupHeading}\n />\n </div>\n </Popover.Content>\n </Popover.Portal>\n\n {bottomText && (\n <p\n id={ac.helperTextId}\n className={cn(\n \"typography-regular-body-sm px-2 pt-1 pb-0.5\",\n error ? \"text-error-content\" : \"text-content-secondary\",\n )}\n >\n {bottomText}\n </p>\n )}\n </div>\n </Popover.Root>\n );\n});\n\nAutocomplete.displayName = \"Autocomplete\";\n"],"names":["Popover"],"mappings":";;;;;;;;;;;;AAgIA,MAAM,mBAAqD;AAAA,EACzD,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AACR;AAEA,MAAM,qBAAuD;AAAA,EAC3D,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AACR;AAEA,MAAM,kBAAoD;AAAA,EACxD,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AACR;AAEA,SAAS,0BAA0B,OAAgB,WAAoB,gBAAyB;AAC9F,MAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,QAAI,CAAC,SAAS,CAAC,aAAa,CAAC,gBAAgB;AAC3C,cAAQ;AAAA,QACN;AAAA,MAAA;AAAA,IAEJ;AAAA,EACF;AACF;AA4BO,MAAM,eAAe,MAAM,WAAgD,CAAC,OAAO,QAAQ;AAChG,QAAM;AAAA,IACJ;AAAA,IACA,cAAc;AAAA,IACd,mBAAmB;AAAA,IACnB;AAAA,IACA,OAAO;AAAA,IACP,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,YAAY;AAAA,IACZ,iBAAiB;AAAA,IACjB;AAAA,IACA,UAAU;AAAA,IACV;AAAA,IACA,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,EAAA,IACE;AAEJ,QAAM,KAAK,gBAAgB,KAAK;AAEhC,QAAM,oBAAoB,KAAK,MAAM,GAAG,SAAS,OAA2B;AAE5E,4BAA0B,OAAO,WAAW,cAAc;AAE1D,QAAM,aAAa,SAAS,eAAe,eAAe;AAE1D,SACE,oBAACA,iBAAQ,MAAR,EAAa,MAAM,GAAG,UAAU,CAAC,UAAU,cAAc,GAAG,kBAC3D,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,WAAW,GAAG,iBAAiB,aAAa,UAAU,SAAS;AAAA,MAC/D,0BAAuB;AAAA,MACvB,iBAAe,WAAW,KAAK;AAAA,MAC/B,cAAY,QAAQ,KAAK;AAAA,MAExB,UAAA;AAAA,QAAA,SACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,SAAS,GAAG;AAAA,YACZ,WAAU;AAAA,YAET,UAAA;AAAA,UAAA;AAAA,QAAA;AAAA,QAIL,oBAACA,iBAAQ,QAAR,EAAe,SAAO,MAGrB,UAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,QAAQ,yBAAyB;AAAA,cACjC,CAAC,YAAY,CAAC,SAAS;AAAA,cACvB,GAAG,UAAU,CAAC,SAAS,CAAC,YAAY;AAAA,cACpC,iBAAiB,IAAI;AAAA,cACrB,gBAAgB,IAAI;AAAA,cACpB,YAAY;AAAA,YAAA;AAAA,YAEd,SAAS,GAAG;AAAA,YAEX,UAAA;AAAA,cAAA,YACC,oBAAC,OAAA,EAAI,WAAU,2EACZ,UAAA,UACH;AAAA,cAGF,qBAAC,OAAA,EAAI,WAAU,sDACZ,UAAA;AAAA,gBAAA,GAAG,WACF,GAAG,qBAAqB,IAAI,CAAC,QAC3B;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBAEC,QAAQ;AAAA,oBACR;AAAA,oBACA,UAAU,MAAM,GAAG,YAAY,IAAI,KAAK;AAAA,oBACxC;AAAA,kBAAA;AAAA,kBAJK,IAAI;AAAA,gBAAA,CAMZ;AAAA,gBAEH;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,KAAK,GAAG;AAAA,oBACR,IAAI,GAAG;AAAA,oBACP,MAAK;AAAA,oBACL,MAAK;AAAA,oBACL;AAAA,oBACA,iBAAe,GAAG;AAAA,oBAClB,iBAAe,GAAG,SAAS,GAAG,YAAY;AAAA,oBAC1C,yBAAuB,GAAG;AAAA,oBAC1B,qBAAkB;AAAA,oBAClB,oBAAkB,aAAa,GAAG,eAAe;AAAA,oBACjD,gBAAc,SAAS;AAAA,oBACvB,cAAY;AAAA,oBACZ,mBAAiB;AAAA,oBACjB,cAAa;AAAA,oBACb,aACE,GAAG,WAAW,GAAG,oBAAoB,SAAS,IAAI,SAAY;AAAA,oBAEhE,OAAO,GAAG;AAAA,oBACV,UAAU,GAAG;AAAA,oBACb,WAAW,GAAG;AAAA,oBACd,SAAS,GAAG;AAAA,oBACZ,QAAQ,GAAG;AAAA,oBACX,WAAW;AAAA,sBACT;AAAA,sBACA,mBAAmB,IAAI;AAAA,oBAAA;AAAA,kBACzB;AAAA,gBAAA;AAAA,cACF,GACF;AAAA,cAEA,qBAAC,OAAA,EAAI,WAAU,oCACZ,UAAA;AAAA,gBAAA,WAAW,oBAAC,aAAA,EAAY,WAAU,6CAAA,CAA6C;AAAA,gBAC/E,aAAa,GAAG,qBAAqB,CAAC,YACrC;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,MAAK;AAAA,oBACL,UAAU;AAAA,oBACV,cAAY;AAAA,oBACZ,WAAU;AAAA,oBACV,SAAS,GAAG;AAAA,oBAEZ,UAAA,oBAAC,WAAA,EAAU,WAAU,SAAA,CAAS;AAAA,kBAAA;AAAA,gBAAA;AAAA,gBAGlC,oBAAC,OAAA,EAAI,WAAU,2EACb,UAAA;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,WAAW,GAAG,+BAA+B,GAAG,UAAU,YAAY;AAAA,kBAAA;AAAA,gBAAA,EACxE,CACF;AAAA,cAAA,EAAA,CACF;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA,GAEJ;AAAA,QAEA,oBAACA,iBAAQ,QAAR,EACC,UAAA;AAAA,UAACA,iBAAQ;AAAA,UAAR;AAAA,YACC,YAAY;AAAA,YACZ,kBAAkB;AAAA,YAClB,iBAAiB,CAAC,MAAM,EAAE,eAAA;AAAA,YAC1B,kBAAkB,CAAC,MAAM,EAAE,eAAA;AAAA,YAC3B,OAAO,EAAE,QAAQ,sCAAA;AAAA,YACjB,WAAW;AAAA,cACT;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,YAAA;AAAA,YAGF,UAAA;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,KAAK,GAAG;AAAA,gBACR,IAAI,GAAG;AAAA,gBACP,MAAK;AAAA,gBACL,cAAY,SAAS,aAAa;AAAA,gBAClC,wBAAsB,GAAG,WAAW;AAAA,gBACpC,WAAU;AAAA,gBAEV,UAAA;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC;AAAA,oBACA;AAAA,oBACA;AAAA,oBACA,gBAAgB,GAAG;AAAA,oBACnB,iBAAiB,GAAG;AAAA,oBACpB,cAAc,GAAG;AAAA,oBACjB,WAAW,GAAG;AAAA,oBACd,aAAa,GAAG;AAAA,oBAChB,SAAS,GAAG;AAAA,oBACZ,qBAAqB,GAAG;AAAA,oBACxB,eAAe,GAAG;AAAA,oBAClB,UAAU,GAAG;AAAA,oBACb,cAAc,GAAG;AAAA,oBACjB;AAAA,oBACA;AAAA,kBAAA;AAAA,gBAAA;AAAA,cACF;AAAA,YAAA;AAAA,UACF;AAAA,QAAA,GAEJ;AAAA,QAEC,cACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,IAAI,GAAG;AAAA,YACP,WAAW;AAAA,cACT;AAAA,cACA,QAAQ,uBAAuB;AAAA,YAAA;AAAA,YAGhC,UAAA;AAAA,UAAA;AAAA,QAAA;AAAA,MACH;AAAA,IAAA;AAAA,EAAA,GAGN;AAEJ,CAAC;AAED,aAAa,cAAc;"}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from "../../utils/cn.mjs";
|
|
3
4
|
import { SpinnerIcon } from "../Icons/SpinnerIcon.mjs";
|
|
4
5
|
import { AutocompleteOptionItem } from "./AutocompleteOptionItem.mjs";
|
|
6
|
+
function DefaultGroupHeading({ children }) {
|
|
7
|
+
return /* @__PURE__ */ jsx("span", { className: "typography-semibold-body-sm block px-3 pt-2 pb-1 text-content-secondary uppercase tracking-wide", children });
|
|
8
|
+
}
|
|
5
9
|
function AutocompleteDropdownContent({
|
|
6
10
|
loading,
|
|
7
11
|
loadingText,
|
|
8
12
|
emptyText,
|
|
9
13
|
visibleOptions,
|
|
14
|
+
visibleSections,
|
|
15
|
+
createOption,
|
|
10
16
|
listboxId,
|
|
11
17
|
activeIndex,
|
|
12
18
|
isMulti,
|
|
@@ -14,7 +20,8 @@ function AutocompleteDropdownContent({
|
|
|
14
20
|
selectedValue,
|
|
15
21
|
onSelect,
|
|
16
22
|
onMouseEnter,
|
|
17
|
-
renderOption
|
|
23
|
+
renderOption,
|
|
24
|
+
renderGroupHeading
|
|
18
25
|
}) {
|
|
19
26
|
if (loading) {
|
|
20
27
|
return (
|
|
@@ -25,10 +32,16 @@ function AutocompleteDropdownContent({
|
|
|
25
32
|
] })
|
|
26
33
|
);
|
|
27
34
|
}
|
|
35
|
+
const { pinned, ungrouped, groups } = visibleSections;
|
|
36
|
+
const hasTopBlock = pinned.length > 0 || ungrouped.length > 0;
|
|
37
|
+
const hasGroups = groups.length > 0;
|
|
38
|
+
const nonPinnedVisibleCount = ungrouped.length + groups.reduce((n, g) => n + g.options.length, 0) + (createOption ? 1 : 0);
|
|
28
39
|
if (visibleOptions.length === 0) {
|
|
29
40
|
return /* @__PURE__ */ jsx("div", { className: "typography-regular-body-md block px-3 py-4 text-center text-content-secondary", children: emptyText });
|
|
30
41
|
}
|
|
31
|
-
|
|
42
|
+
let cursor = 0;
|
|
43
|
+
const renderOptionRow = (option) => {
|
|
44
|
+
const index = cursor++;
|
|
32
45
|
const isSelected = isMulti ? selectedMultiValues.includes(option.value) : option.value === selectedValue;
|
|
33
46
|
return /* @__PURE__ */ jsx(
|
|
34
47
|
AutocompleteOptionItem,
|
|
@@ -44,7 +57,37 @@ function AutocompleteDropdownContent({
|
|
|
44
57
|
},
|
|
45
58
|
option.value
|
|
46
59
|
);
|
|
47
|
-
}
|
|
60
|
+
};
|
|
61
|
+
const showDivider = hasTopBlock && hasGroups;
|
|
62
|
+
const emptyAfterPinned = nonPinnedVisibleCount === 0 && pinned.length > 0;
|
|
63
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
64
|
+
pinned.map(renderOptionRow),
|
|
65
|
+
ungrouped.map(renderOptionRow),
|
|
66
|
+
groups.map((section, sectionIndex) => {
|
|
67
|
+
const headingId = `${listboxId}-group-${section.group.id}`;
|
|
68
|
+
const headingNode = renderGroupHeading ? renderGroupHeading(section.group) : section.group.label;
|
|
69
|
+
return (
|
|
70
|
+
// biome-ignore lint/a11y/useSemanticElements: <fieldset> carries form semantics and default styling we don't want inside a listbox; role="group" is the correct ARIA pattern here
|
|
71
|
+
/* @__PURE__ */ jsxs(
|
|
72
|
+
"div",
|
|
73
|
+
{
|
|
74
|
+
role: "group",
|
|
75
|
+
"aria-labelledby": headingId,
|
|
76
|
+
className: cn(
|
|
77
|
+
showDivider && sectionIndex === 0 && "mt-1 border-neutral-alphas-200 border-t pt-1"
|
|
78
|
+
),
|
|
79
|
+
children: [
|
|
80
|
+
/* @__PURE__ */ jsx("div", { role: "presentation", id: headingId, children: renderGroupHeading ? headingNode : /* @__PURE__ */ jsx(DefaultGroupHeading, { children: headingNode }) }),
|
|
81
|
+
section.options.map(renderOptionRow)
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
section.group.id
|
|
85
|
+
)
|
|
86
|
+
);
|
|
87
|
+
}),
|
|
88
|
+
createOption && renderOptionRow(createOption),
|
|
89
|
+
emptyAfterPinned && /* @__PURE__ */ jsx("div", { className: "typography-regular-body-md block px-3 py-4 text-center text-content-secondary", children: emptyText })
|
|
90
|
+
] });
|
|
48
91
|
}
|
|
49
92
|
export {
|
|
50
93
|
AutocompleteDropdownContent
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AutocompleteDropdownContent.mjs","sources":["../../../src/components/Autocomplete/AutocompleteDropdownContent.tsx"],"sourcesContent":["import type * as React from \"react\";\nimport { SpinnerIcon } from \"../Icons/SpinnerIcon\";\nimport type { AutocompleteOption } from \"./Autocomplete\";\nimport { AutocompleteOptionItem } from \"./AutocompleteOptionItem\";\
|
|
1
|
+
{"version":3,"file":"AutocompleteDropdownContent.mjs","sources":["../../../src/components/Autocomplete/AutocompleteDropdownContent.tsx"],"sourcesContent":["import type * as React from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { SpinnerIcon } from \"../Icons/SpinnerIcon\";\nimport type { AutocompleteGroup, AutocompleteOption } from \"./Autocomplete\";\nimport { AutocompleteOptionItem } from \"./AutocompleteOptionItem\";\nimport type { AutocompleteVisibleSections } from \"./constants\";\n\ninterface AutocompleteDropdownContentProps {\n loading: boolean;\n loadingText?: string;\n emptyText?: string;\n visibleOptions: AutocompleteOption[];\n visibleSections: AutocompleteVisibleSections;\n /**\n * Synthetic \"Create new …\" row, when `creatable` is on and the search\n * doesn't already match an existing option. Rendered as a trailing row,\n * always shown regardless of filter, and indexed last in\n * `visibleOptions` so keyboard navigation works.\n */\n createOption?: AutocompleteOption | null;\n listboxId: string;\n activeIndex: number;\n isMulti: boolean;\n selectedMultiValues: string[];\n selectedValue: string | null;\n onSelect: (option: AutocompleteOption) => void;\n onMouseEnter: (index: number) => void;\n renderOption?: (\n option: AutocompleteOption,\n state: { selected: boolean; active: boolean },\n ) => React.ReactNode;\n renderGroupHeading?: (group: AutocompleteGroup) => React.ReactNode;\n}\n\nfunction DefaultGroupHeading({ children }: { children: React.ReactNode }) {\n return (\n <span className=\"typography-semibold-body-sm block px-3 pt-2 pb-1 text-content-secondary uppercase tracking-wide\">\n {children}\n </span>\n );\n}\n\nexport function AutocompleteDropdownContent({\n loading,\n loadingText,\n emptyText,\n visibleOptions,\n visibleSections,\n createOption,\n listboxId,\n activeIndex,\n isMulti,\n selectedMultiValues,\n selectedValue,\n onSelect,\n onMouseEnter,\n renderOption,\n renderGroupHeading,\n}: AutocompleteDropdownContentProps) {\n if (loading) {\n return (\n // biome-ignore lint/a11y/useSemanticElements: <output> is not appropriate here; using role=\"status\" for live region announcements\n <div role=\"status\" className=\"flex items-center justify-center py-4\">\n <SpinnerIcon className=\"size-5 animate-spin text-content-secondary\" />\n {loadingText && (\n <span className=\"typography-regular-body-md ml-2 text-content-secondary\">\n {loadingText}\n </span>\n )}\n </div>\n );\n }\n\n const { pinned, ungrouped, groups } = visibleSections;\n const hasTopBlock = pinned.length > 0 || ungrouped.length > 0;\n const hasGroups = groups.length > 0;\n const nonPinnedVisibleCount =\n ungrouped.length + groups.reduce((n, g) => n + g.options.length, 0) + (createOption ? 1 : 0);\n\n if (visibleOptions.length === 0) {\n return (\n <div className=\"typography-regular-body-md block px-3 py-4 text-center text-content-secondary\">\n {emptyText}\n </div>\n );\n }\n\n let cursor = 0;\n const renderOptionRow = (option: AutocompleteOption) => {\n const index = cursor++;\n const isSelected = isMulti\n ? selectedMultiValues.includes(option.value)\n : option.value === selectedValue;\n return (\n <AutocompleteOptionItem\n key={option.value}\n option={option}\n optionId={`${listboxId}-option-${index}`}\n index={index}\n isSelected={isSelected}\n isActive={index === activeIndex}\n onSelect={() => onSelect(option)}\n onMouseEnter={() => onMouseEnter(index)}\n renderOption={renderOption}\n />\n );\n };\n\n const showDivider = hasTopBlock && hasGroups;\n const emptyAfterPinned = nonPinnedVisibleCount === 0 && pinned.length > 0;\n\n return (\n <>\n {pinned.map(renderOptionRow)}\n {ungrouped.map(renderOptionRow)}\n {groups.map((section, sectionIndex) => {\n const headingId = `${listboxId}-group-${section.group.id}`;\n const headingNode = renderGroupHeading\n ? renderGroupHeading(section.group)\n : section.group.label;\n return (\n // biome-ignore lint/a11y/useSemanticElements: <fieldset> carries form semantics and default styling we don't want inside a listbox; role=\"group\" is the correct ARIA pattern here\n <div\n key={section.group.id}\n role=\"group\"\n aria-labelledby={headingId}\n className={cn(\n showDivider && sectionIndex === 0 && \"mt-1 border-neutral-alphas-200 border-t pt-1\",\n )}\n >\n <div role=\"presentation\" id={headingId}>\n {renderGroupHeading ? (\n headingNode\n ) : (\n <DefaultGroupHeading>{headingNode}</DefaultGroupHeading>\n )}\n </div>\n {section.options.map(renderOptionRow)}\n </div>\n );\n })}\n {createOption && renderOptionRow(createOption)}\n {emptyAfterPinned && (\n <div className=\"typography-regular-body-md block px-3 py-4 text-center text-content-secondary\">\n {emptyText}\n </div>\n )}\n </>\n );\n}\n"],"names":[],"mappings":";;;;;AAkCA,SAAS,oBAAoB,EAAE,YAA2C;AACxE,SACE,oBAAC,QAAA,EAAK,WAAU,mGACb,SAAA,CACH;AAEJ;AAEO,SAAS,4BAA4B;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAqC;AACnC,MAAI,SAAS;AACX;AAAA;AAAA,MAEE,qBAAC,OAAA,EAAI,MAAK,UAAS,WAAU,yCAC3B,UAAA;AAAA,QAAA,oBAAC,aAAA,EAAY,WAAU,6CAAA,CAA6C;AAAA,QACnE,eACC,oBAAC,QAAA,EAAK,WAAU,0DACb,UAAA,YAAA,CACH;AAAA,MAAA,EAAA,CAEJ;AAAA;AAAA,EAEJ;AAEA,QAAM,EAAE,QAAQ,WAAW,OAAA,IAAW;AACtC,QAAM,cAAc,OAAO,SAAS,KAAK,UAAU,SAAS;AAC5D,QAAM,YAAY,OAAO,SAAS;AAClC,QAAM,wBACJ,UAAU,SAAS,OAAO,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,QAAQ,QAAQ,CAAC,KAAK,eAAe,IAAI;AAE5F,MAAI,eAAe,WAAW,GAAG;AAC/B,WACE,oBAAC,OAAA,EAAI,WAAU,iFACZ,UAAA,WACH;AAAA,EAEJ;AAEA,MAAI,SAAS;AACb,QAAM,kBAAkB,CAAC,WAA+B;AACtD,UAAM,QAAQ;AACd,UAAM,aAAa,UACf,oBAAoB,SAAS,OAAO,KAAK,IACzC,OAAO,UAAU;AACrB,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QAEC;AAAA,QACA,UAAU,GAAG,SAAS,WAAW,KAAK;AAAA,QACtC;AAAA,QACA;AAAA,QACA,UAAU,UAAU;AAAA,QACpB,UAAU,MAAM,SAAS,MAAM;AAAA,QAC/B,cAAc,MAAM,aAAa,KAAK;AAAA,QACtC;AAAA,MAAA;AAAA,MARK,OAAO;AAAA,IAAA;AAAA,EAWlB;AAEA,QAAM,cAAc,eAAe;AACnC,QAAM,mBAAmB,0BAA0B,KAAK,OAAO,SAAS;AAExE,SACE,qBAAA,UAAA,EACG,UAAA;AAAA,IAAA,OAAO,IAAI,eAAe;AAAA,IAC1B,UAAU,IAAI,eAAe;AAAA,IAC7B,OAAO,IAAI,CAAC,SAAS,iBAAiB;AACrC,YAAM,YAAY,GAAG,SAAS,UAAU,QAAQ,MAAM,EAAE;AACxD,YAAM,cAAc,qBAChB,mBAAmB,QAAQ,KAAK,IAChC,QAAQ,MAAM;AAClB;AAAA;AAAA,QAEE;AAAA,UAAC;AAAA,UAAA;AAAA,YAEC,MAAK;AAAA,YACL,mBAAiB;AAAA,YACjB,WAAW;AAAA,cACT,eAAe,iBAAiB,KAAK;AAAA,YAAA;AAAA,YAGvC,UAAA;AAAA,cAAA,oBAAC,OAAA,EAAI,MAAK,gBAAe,IAAI,WAC1B,+BACC,cAEA,oBAAC,qBAAA,EAAqB,UAAA,YAAA,CAAY,GAEtC;AAAA,cACC,QAAQ,QAAQ,IAAI,eAAe;AAAA,YAAA;AAAA,UAAA;AAAA,UAd/B,QAAQ,MAAM;AAAA,QAAA;AAAA;AAAA,IAiBzB,CAAC;AAAA,IACA,gBAAgB,gBAAgB,YAAY;AAAA,IAC5C,oBACC,oBAAC,OAAA,EAAI,WAAU,iFACZ,UAAA,UAAA,CACH;AAAA,EAAA,GAEJ;AAEJ;"}
|
|
@@ -7,9 +7,41 @@ function defaultFilter(option, query) {
|
|
|
7
7
|
function getLabel(option) {
|
|
8
8
|
return option.label ?? option.value;
|
|
9
9
|
}
|
|
10
|
+
function getVisibleSections(options, groups, filter, query) {
|
|
11
|
+
const pinned = [];
|
|
12
|
+
const ungrouped = [];
|
|
13
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
14
|
+
const knownGroupIds = new Set(groups?.map((g) => g.id));
|
|
15
|
+
const lowerQuery = query.toLowerCase();
|
|
16
|
+
const groupMatchedByHeading = new Set(
|
|
17
|
+
query ? (groups ?? []).filter((g) => g.label.toLowerCase().includes(lowerQuery)).map((g) => g.id) : []
|
|
18
|
+
);
|
|
19
|
+
for (const option of options) {
|
|
20
|
+
if (option.pinned) {
|
|
21
|
+
pinned.push(option);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const inMatchedGroup = !!option.groupId && groupMatchedByHeading.has(option.groupId);
|
|
25
|
+
if (query && !inMatchedGroup && !filter(option, query)) continue;
|
|
26
|
+
if (option.groupId && knownGroupIds.has(option.groupId)) {
|
|
27
|
+
const bucket = byGroup.get(option.groupId) ?? [];
|
|
28
|
+
bucket.push(option);
|
|
29
|
+
byGroup.set(option.groupId, bucket);
|
|
30
|
+
} else {
|
|
31
|
+
ungrouped.push(option);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const visibleGroups = (groups ?? []).map((group) => ({ group, options: byGroup.get(group.id) ?? [] })).filter((section) => section.options.length > 0);
|
|
35
|
+
return { pinned, ungrouped, groups: visibleGroups };
|
|
36
|
+
}
|
|
37
|
+
function flattenVisibleSections(sections) {
|
|
38
|
+
return [...sections.pinned, ...sections.ungrouped, ...sections.groups.flatMap((s) => s.options)];
|
|
39
|
+
}
|
|
10
40
|
export {
|
|
11
41
|
CREATE_PREFIX,
|
|
12
42
|
defaultFilter,
|
|
13
|
-
|
|
43
|
+
flattenVisibleSections,
|
|
44
|
+
getLabel,
|
|
45
|
+
getVisibleSections
|
|
14
46
|
};
|
|
15
47
|
//# sourceMappingURL=constants.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.mjs","sources":["../../../src/components/Autocomplete/constants.ts"],"sourcesContent":["import type { AutocompleteOption } from \"./Autocomplete\";\n\nexport const CREATE_PREFIX = \"__create__\";\n\nexport function defaultFilter(option: AutocompleteOption, query: string): boolean {\n const label = option.label ?? option.value;\n return label.toLowerCase().includes(query.toLowerCase());\n}\n\nexport function getLabel(option: AutocompleteOption): string {\n return option.label ?? option.value;\n}\n"],"names":[],"mappings":";AAEO,MAAM,gBAAgB;AAEtB,SAAS,cAAc,QAA4B,OAAwB;AAChF,QAAM,QAAQ,OAAO,SAAS,OAAO;AACrC,SAAO,MAAM,YAAA,EAAc,SAAS,MAAM,aAAa;AACzD;AAEO,SAAS,SAAS,QAAoC;AAC3D,SAAO,OAAO,SAAS,OAAO;AAChC;"}
|
|
1
|
+
{"version":3,"file":"constants.mjs","sources":["../../../src/components/Autocomplete/constants.ts"],"sourcesContent":["import type { AutocompleteGroup, AutocompleteOption } from \"./Autocomplete\";\n\nexport const CREATE_PREFIX = \"__create__\";\n\nexport function defaultFilter(option: AutocompleteOption, query: string): boolean {\n const label = option.label ?? option.value;\n return label.toLowerCase().includes(query.toLowerCase());\n}\n\nexport function getLabel(option: AutocompleteOption): string {\n return option.label ?? option.value;\n}\n\n/**\n * Section structure consumed by `AutocompleteDropdownContent` to render the\n * grouped layout. `pinned` options bypass the filter; `ungrouped` options\n * have no `groupId`; `groups` preserves the order declared by the consumer's\n * `groups` prop and omits any group with zero visible options.\n *\n * The flat `visibleOptions` list (built in `useAutocomplete`) keeps the same\n * order as this structure (pinned → ungrouped → groups in order, options in\n * declaration order within each section) so `aria-activedescendant` and\n * keyboard navigation work against a single flat index.\n */\nexport interface AutocompleteVisibleSection {\n group: AutocompleteGroup;\n options: AutocompleteOption[];\n}\n\nexport interface AutocompleteVisibleSections {\n pinned: AutocompleteOption[];\n ungrouped: AutocompleteOption[];\n groups: AutocompleteVisibleSection[];\n}\n\n/**\n * Splits `options` into pinned / ungrouped / per-group buckets, applies the\n * provided filter to non-pinned options only, and drops empty groups.\n *\n * Group-heading matching: when the query matches a group's `label`\n * (case-insensitive substring, same as the default option filter), every\n * option under that group is kept regardless of whether it individually\n * matches the per-option `filter`. This supports the common \"group heading\n * is the searchable label, items are sub-rows\" shape (e.g. group =\n * product name, items = prices). Pinned options always bypass filtering.\n */\nexport function getVisibleSections(\n options: AutocompleteOption[],\n groups: AutocompleteGroup[] | undefined,\n filter: (option: AutocompleteOption, query: string) => boolean,\n query: string,\n): AutocompleteVisibleSections {\n const pinned: AutocompleteOption[] = [];\n const ungrouped: AutocompleteOption[] = [];\n const byGroup = new Map<string, AutocompleteOption[]>();\n const knownGroupIds = new Set(groups?.map((g) => g.id));\n\n const lowerQuery = query.toLowerCase();\n const groupMatchedByHeading = new Set<string>(\n query\n ? (groups ?? []).filter((g) => g.label.toLowerCase().includes(lowerQuery)).map((g) => g.id)\n : [],\n );\n\n for (const option of options) {\n if (option.pinned) {\n pinned.push(option);\n continue;\n }\n const inMatchedGroup = !!option.groupId && groupMatchedByHeading.has(option.groupId);\n if (query && !inMatchedGroup && !filter(option, query)) continue;\n if (option.groupId && knownGroupIds.has(option.groupId)) {\n const bucket = byGroup.get(option.groupId) ?? [];\n bucket.push(option);\n byGroup.set(option.groupId, bucket);\n } else {\n ungrouped.push(option);\n }\n }\n\n const visibleGroups: AutocompleteVisibleSection[] = (groups ?? [])\n .map((group) => ({ group, options: byGroup.get(group.id) ?? [] }))\n .filter((section) => section.options.length > 0);\n\n return { pinned, ungrouped, groups: visibleGroups };\n}\n\n/** Flattens visible sections into the cursor-indexed list. */\nexport function flattenVisibleSections(\n sections: AutocompleteVisibleSections,\n): AutocompleteOption[] {\n return [...sections.pinned, ...sections.ungrouped, ...sections.groups.flatMap((s) => s.options)];\n}\n"],"names":[],"mappings":";AAEO,MAAM,gBAAgB;AAEtB,SAAS,cAAc,QAA4B,OAAwB;AAChF,QAAM,QAAQ,OAAO,SAAS,OAAO;AACrC,SAAO,MAAM,YAAA,EAAc,SAAS,MAAM,aAAa;AACzD;AAEO,SAAS,SAAS,QAAoC;AAC3D,SAAO,OAAO,SAAS,OAAO;AAChC;AAmCO,SAAS,mBACd,SACA,QACA,QACA,OAC6B;AAC7B,QAAM,SAA+B,CAAA;AACrC,QAAM,YAAkC,CAAA;AACxC,QAAM,8BAAc,IAAA;AACpB,QAAM,gBAAgB,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AAEtD,QAAM,aAAa,MAAM,YAAA;AACzB,QAAM,wBAAwB,IAAI;AAAA,IAChC,SACK,UAAU,CAAA,GAAI,OAAO,CAAC,MAAM,EAAE,MAAM,YAAA,EAAc,SAAS,UAAU,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,IACxF,CAAA;AAAA,EAAC;AAGP,aAAW,UAAU,SAAS;AAC5B,QAAI,OAAO,QAAQ;AACjB,aAAO,KAAK,MAAM;AAClB;AAAA,IACF;AACA,UAAM,iBAAiB,CAAC,CAAC,OAAO,WAAW,sBAAsB,IAAI,OAAO,OAAO;AACnF,QAAI,SAAS,CAAC,kBAAkB,CAAC,OAAO,QAAQ,KAAK,EAAG;AACxD,QAAI,OAAO,WAAW,cAAc,IAAI,OAAO,OAAO,GAAG;AACvD,YAAM,SAAS,QAAQ,IAAI,OAAO,OAAO,KAAK,CAAA;AAC9C,aAAO,KAAK,MAAM;AAClB,cAAQ,IAAI,OAAO,SAAS,MAAM;AAAA,IACpC,OAAO;AACL,gBAAU,KAAK,MAAM;AAAA,IACvB;AAAA,EACF;AAEA,QAAM,iBAA+C,UAAU,CAAA,GAC5D,IAAI,CAAC,WAAW,EAAE,OAAO,SAAS,QAAQ,IAAI,MAAM,EAAE,KAAK,CAAA,IAAK,EAChE,OAAO,CAAC,YAAY,QAAQ,QAAQ,SAAS,CAAC;AAEjD,SAAO,EAAE,QAAQ,WAAW,QAAQ,cAAA;AACtC;AAGO,SAAS,uBACd,UACsB;AACtB,SAAO,CAAC,GAAG,SAAS,QAAQ,GAAG,SAAS,WAAW,GAAG,SAAS,OAAO,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;AACjG;"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import { defaultFilter,
|
|
3
|
+
import { CREATE_PREFIX, getVisibleSections, defaultFilter, flattenVisibleSections, getLabel } from "./constants.mjs";
|
|
4
4
|
function useAutocomplete(props) {
|
|
5
5
|
const {
|
|
6
6
|
id,
|
|
7
7
|
options,
|
|
8
|
+
groups,
|
|
8
9
|
disabled = false,
|
|
9
10
|
filterFn,
|
|
10
11
|
creatable = false,
|
|
@@ -44,19 +45,22 @@ function useAutocomplete(props) {
|
|
|
44
45
|
);
|
|
45
46
|
const [activeIndex, setActiveIndex] = React.useState(-1);
|
|
46
47
|
const filter = filterFn ?? defaultFilter;
|
|
47
|
-
const filteredOptions = React.useMemo(() => {
|
|
48
|
-
if (!searchText) return options;
|
|
49
|
-
return options.filter((o) => filter(o, searchText));
|
|
50
|
-
}, [options, searchText, filter]);
|
|
51
48
|
const showCreate = creatable && searchText.length > 0 && !options.some((o) => (o.label ?? o.value).toLowerCase() === searchText.toLowerCase());
|
|
52
|
-
const
|
|
53
|
-
if (!showCreate) return
|
|
54
|
-
|
|
49
|
+
const createOption = React.useMemo(() => {
|
|
50
|
+
if (!showCreate) return null;
|
|
51
|
+
return {
|
|
55
52
|
value: `${CREATE_PREFIX}${searchText}`,
|
|
56
53
|
label: creatableLabel ? creatableLabel(searchText) : searchText
|
|
57
54
|
};
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
}, [showCreate, searchText, creatableLabel]);
|
|
56
|
+
const visibleSections = React.useMemo(
|
|
57
|
+
() => getVisibleSections(options, groups, filter, searchText),
|
|
58
|
+
[options, groups, filter, searchText]
|
|
59
|
+
);
|
|
60
|
+
const visibleOptions = React.useMemo(() => {
|
|
61
|
+
const flat = flattenVisibleSections(visibleSections);
|
|
62
|
+
return createOption ? [...flat, createOption] : flat;
|
|
63
|
+
}, [visibleSections, createOption]);
|
|
60
64
|
const prevOptionsLengthRef = React.useRef(visibleOptions.length);
|
|
61
65
|
const prevSearchTextRef = React.useRef(searchText);
|
|
62
66
|
React.useEffect(() => {
|
|
@@ -245,6 +249,8 @@ function useAutocomplete(props) {
|
|
|
245
249
|
selectedMultiOptions,
|
|
246
250
|
searchText,
|
|
247
251
|
visibleOptions,
|
|
252
|
+
visibleSections,
|
|
253
|
+
createOption,
|
|
248
254
|
activeIndex,
|
|
249
255
|
activeDescendantId,
|
|
250
256
|
hasClearableValue,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAutocomplete.mjs","sources":["../../../src/components/Autocomplete/useAutocomplete.ts"],"sourcesContent":["import * as React from \"react\";\nimport type { AutocompleteOption, AutocompleteProps } from \"./Autocomplete\";\nimport { CREATE_PREFIX, defaultFilter, getLabel } from \"./constants\";\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Combobox hook managing interconnected controlled/uncontrolled state\nexport function useAutocomplete(props: AutocompleteProps) {\n const {\n id,\n options,\n disabled = false,\n filterFn,\n creatable = false,\n creatableLabel,\n onCreate,\n inputValue: inputValueProp,\n onInputChange,\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n } = props;\n\n const generatedId = React.useId();\n const inputId = id ?? generatedId;\n const helperTextId = `${inputId}-helper`;\n const listboxId = `${inputId}-listbox`;\n\n const inputRef = React.useRef<HTMLInputElement>(null);\n const listRef = React.useRef<HTMLDivElement>(null);\n\n const isMulti = props.multiple === true;\n\n // --- single-select state ---\n const [internalValue, setInternalValue] = React.useState<string | null>(\n (!isMulti && props.defaultValue) || null,\n );\n const selectedValue: string | null = !isMulti\n ? props.value !== undefined\n ? props.value\n : internalValue\n : null;\n\n // --- multi-select state ---\n const [internalMultiValue, setInternalMultiValue] = React.useState<string[]>(\n isMulti && props.defaultValue ? props.defaultValue : [],\n );\n const selectedMultiValues: string[] = isMulti\n ? props.value !== undefined\n ? props.value\n : internalMultiValue\n : [];\n\n // --- input text ---\n const [internalInputValue, setInternalInputValue] = React.useState(\"\");\n const searchText = inputValueProp !== undefined ? inputValueProp : internalInputValue;\n\n // --- open state ---\n const [internalOpen, setInternalOpen] = React.useState(defaultOpen);\n const isOpen = openProp !== undefined ? openProp : internalOpen;\n\n const setOpen = React.useCallback(\n (next: boolean) => {\n if (openProp === undefined) setInternalOpen(next);\n onOpenChange?.(next);\n },\n [openProp, onOpenChange],\n );\n\n // --- active index ---\n const [activeIndex, setActiveIndex] = React.useState(-1);\n\n // --- filtering ---\n const filter = filterFn ?? defaultFilter;\n\n const filteredOptions = React.useMemo(() => {\n if (!searchText) return options;\n return options.filter((o) => filter(o, searchText));\n }, [options, searchText, filter]);\n\n const showCreate =\n creatable &&\n searchText.length > 0 &&\n !options.some((o) => (o.label ?? o.value).toLowerCase() === searchText.toLowerCase());\n\n const visibleOptions = React.useMemo(() => {\n if (!showCreate) return filteredOptions;\n const createOption: AutocompleteOption = {\n value: `${CREATE_PREFIX}${searchText}`,\n label: creatableLabel ? creatableLabel(searchText) : searchText,\n };\n return [...filteredOptions, createOption];\n }, [filteredOptions, showCreate, searchText, creatableLabel]);\n\n const prevOptionsLengthRef = React.useRef(visibleOptions.length);\n const prevSearchTextRef = React.useRef(searchText);\n\n React.useEffect(() => {\n if (\n searchText !== prevSearchTextRef.current ||\n visibleOptions.length !== prevOptionsLengthRef.current\n ) {\n setActiveIndex(-1);\n prevSearchTextRef.current = searchText;\n prevOptionsLengthRef.current = visibleOptions.length;\n }\n }, [searchText, visibleOptions.length]);\n\n // --- scroll active option into view ---\n React.useEffect(() => {\n if (activeIndex < 0 || !listRef.current) return;\n const el = listRef.current.querySelector(`[data-option-index=\"${activeIndex}\"]`);\n if (typeof el?.scrollIntoView === \"function\") {\n el.scrollIntoView({ block: \"nearest\" });\n }\n }, [activeIndex]);\n\n // --- helpers ---\n function clearInputText() {\n if (inputValueProp === undefined) setInternalInputValue(\"\");\n onInputChange?.(\"\");\n }\n\n function selectSingle(val: string | null) {\n if (!isMulti && props.value === undefined) setInternalValue(val);\n if (!isMulti) (props as { onChange?: (v: string | null) => void }).onChange?.(val);\n }\n\n function toggleMulti(val: string) {\n const next = selectedMultiValues.includes(val)\n ? selectedMultiValues.filter((v) => v !== val)\n : [...selectedMultiValues, val];\n if (isMulti && props.value === undefined) setInternalMultiValue(next);\n if (isMulti) (props as { onChange?: (v: string[]) => void }).onChange?.(next);\n }\n\n function handleSelect(option: AutocompleteOption) {\n if (option.value.startsWith(CREATE_PREFIX)) {\n const raw = option.value.slice(CREATE_PREFIX.length);\n onCreate?.(raw);\n if (isMulti) {\n toggleMulti(raw);\n } else {\n selectSingle(raw);\n setOpen(false);\n }\n clearInputText();\n return;\n }\n\n if (isMulti) {\n toggleMulti(option.value);\n clearInputText();\n } else {\n selectSingle(option.value);\n clearInputText();\n setOpen(false);\n }\n }\n\n function handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {\n const val = e.target.value;\n if (inputValueProp === undefined) setInternalInputValue(val);\n onInputChange?.(val);\n if (!isOpen) setOpen(true);\n }\n\n function findNextEnabled(start: number, direction: 1 | -1): number | null {\n let idx = start;\n while (idx >= 0 && idx < visibleOptions.length && visibleOptions[idx]?.disabled) {\n idx += direction;\n }\n return idx >= 0 && idx < visibleOptions.length ? idx : null;\n }\n\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Flat switch is clearer than splitting into separate handler functions\n function handleKeyDown(e: React.KeyboardEvent) {\n if (disabled) return;\n\n switch (e.key) {\n case \"ArrowDown\": {\n e.preventDefault();\n if (!isOpen) {\n setOpen(true);\n return;\n }\n setActiveIndex((prev) => findNextEnabled(prev + 1, 1) ?? prev);\n break;\n }\n case \"ArrowUp\": {\n e.preventDefault();\n if (!isOpen) {\n setOpen(true);\n return;\n }\n setActiveIndex((prev) => findNextEnabled(prev - 1, -1) ?? prev);\n break;\n }\n case \"Enter\": {\n if (isOpen && activeIndex >= 0 && activeIndex < visibleOptions.length) {\n e.preventDefault();\n const opt = visibleOptions[activeIndex];\n if (opt && !opt.disabled) handleSelect(opt);\n }\n break;\n }\n case \"Escape\": {\n e.preventDefault();\n if (isOpen) setOpen(false);\n break;\n }\n case \"Backspace\": {\n if (isMulti && searchText === \"\" && selectedMultiValues.length > 0) {\n const lastVal = selectedMultiValues[selectedMultiValues.length - 1];\n if (lastVal !== undefined) toggleMulti(lastVal);\n }\n break;\n }\n case \"Home\": {\n if (isOpen) {\n e.preventDefault();\n const idx = findNextEnabled(0, 1);\n if (idx !== null) setActiveIndex(idx);\n }\n break;\n }\n case \"End\": {\n if (isOpen) {\n e.preventDefault();\n const idx = findNextEnabled(visibleOptions.length - 1, -1);\n if (idx !== null) setActiveIndex(idx);\n }\n break;\n }\n }\n }\n\n function handleClear(e: React.MouseEvent) {\n e.stopPropagation();\n if (isMulti) {\n if (props.value === undefined) setInternalMultiValue([]);\n if (isMulti) (props as { onChange?: (v: string[]) => void }).onChange?.([]);\n } else {\n selectSingle(null);\n }\n clearInputText();\n inputRef.current?.focus();\n }\n\n function handleBlur(e: React.FocusEvent<HTMLInputElement>) {\n const container = e.currentTarget.closest(\"[data-autocomplete-root]\");\n if (container && e.relatedTarget instanceof Node && container.contains(e.relatedTarget)) {\n return;\n }\n if (\n e.relatedTarget instanceof Node &&\n (e.relatedTarget as Element).closest?.(\"[data-radix-popper-content-wrapper]\")\n ) {\n return;\n }\n if (isOpen) setOpen(false);\n }\n\n function handleFocus() {\n if (!disabled && !isOpen) setOpen(true);\n }\n\n function handleContainerClick() {\n if (!disabled) {\n inputRef.current?.focus();\n if (!isOpen) setOpen(true);\n }\n }\n\n function handleOpenChange(next: boolean) {\n setOpen(next);\n if (!next) {\n clearInputText();\n setActiveIndex(-1);\n }\n }\n\n const selectedOption = React.useMemo(\n () => options.find((o) => o.value === selectedValue),\n [options, selectedValue],\n );\n\n const selectedMultiOptions = React.useMemo(\n () =>\n selectedMultiValues\n .map((v) => options.find((o) => o.value === v))\n .filter(Boolean) as AutocompleteOption[],\n [options, selectedMultiValues],\n );\n\n const hasClearableValue = isMulti ? selectedMultiValues.length > 0 : selectedValue != null;\n\n const displayInputValue = React.useMemo(() => {\n if (searchText) return searchText;\n if (isMulti || isOpen) return \"\";\n return selectedOption ? getLabel(selectedOption) : \"\";\n }, [searchText, isMulti, isOpen, selectedOption]);\n\n const activeDescendantId = activeIndex >= 0 ? `${listboxId}-option-${activeIndex}` : undefined;\n\n return {\n inputId,\n helperTextId,\n listboxId,\n inputRef,\n listRef,\n isMulti,\n isOpen,\n selectedValue,\n selectedMultiValues,\n selectedMultiOptions,\n searchText,\n visibleOptions,\n activeIndex,\n activeDescendantId,\n hasClearableValue,\n displayInputValue,\n setActiveIndex,\n handleSelect,\n handleInputChange,\n handleKeyDown,\n handleClear,\n handleBlur,\n handleFocus,\n handleContainerClick,\n handleOpenChange,\n toggleMulti,\n };\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useAutocomplete.mjs","sources":["../../../src/components/Autocomplete/useAutocomplete.ts"],"sourcesContent":["import * as React from \"react\";\nimport type { AutocompleteOption, AutocompleteProps } from \"./Autocomplete\";\nimport {\n type AutocompleteVisibleSections,\n CREATE_PREFIX,\n defaultFilter,\n flattenVisibleSections,\n getLabel,\n getVisibleSections,\n} from \"./constants\";\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Combobox hook managing interconnected controlled/uncontrolled state\nexport function useAutocomplete(props: AutocompleteProps) {\n const {\n id,\n options,\n groups,\n disabled = false,\n filterFn,\n creatable = false,\n creatableLabel,\n onCreate,\n inputValue: inputValueProp,\n onInputChange,\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n } = props;\n\n const generatedId = React.useId();\n const inputId = id ?? generatedId;\n const helperTextId = `${inputId}-helper`;\n const listboxId = `${inputId}-listbox`;\n\n const inputRef = React.useRef<HTMLInputElement>(null);\n const listRef = React.useRef<HTMLDivElement>(null);\n\n const isMulti = props.multiple === true;\n\n // --- single-select state ---\n const [internalValue, setInternalValue] = React.useState<string | null>(\n (!isMulti && props.defaultValue) || null,\n );\n const selectedValue: string | null = !isMulti\n ? props.value !== undefined\n ? props.value\n : internalValue\n : null;\n\n // --- multi-select state ---\n const [internalMultiValue, setInternalMultiValue] = React.useState<string[]>(\n isMulti && props.defaultValue ? props.defaultValue : [],\n );\n const selectedMultiValues: string[] = isMulti\n ? props.value !== undefined\n ? props.value\n : internalMultiValue\n : [];\n\n // --- input text ---\n const [internalInputValue, setInternalInputValue] = React.useState(\"\");\n const searchText = inputValueProp !== undefined ? inputValueProp : internalInputValue;\n\n // --- open state ---\n const [internalOpen, setInternalOpen] = React.useState(defaultOpen);\n const isOpen = openProp !== undefined ? openProp : internalOpen;\n\n const setOpen = React.useCallback(\n (next: boolean) => {\n if (openProp === undefined) setInternalOpen(next);\n onOpenChange?.(next);\n },\n [openProp, onOpenChange],\n );\n\n // --- active index ---\n const [activeIndex, setActiveIndex] = React.useState(-1);\n\n // --- filtering ---\n const filter = filterFn ?? defaultFilter;\n\n const showCreate =\n creatable &&\n searchText.length > 0 &&\n !options.some((o) => (o.label ?? o.value).toLowerCase() === searchText.toLowerCase());\n\n const createOption: AutocompleteOption | null = React.useMemo(() => {\n if (!showCreate) return null;\n return {\n value: `${CREATE_PREFIX}${searchText}`,\n label: creatableLabel ? creatableLabel(searchText) : searchText,\n };\n }, [showCreate, searchText, creatableLabel]);\n\n const visibleSections: AutocompleteVisibleSections = React.useMemo(\n () => getVisibleSections(options, groups, filter, searchText),\n [options, groups, filter, searchText],\n );\n\n const visibleOptions = React.useMemo(() => {\n const flat = flattenVisibleSections(visibleSections);\n return createOption ? [...flat, createOption] : flat;\n }, [visibleSections, createOption]);\n\n const prevOptionsLengthRef = React.useRef(visibleOptions.length);\n const prevSearchTextRef = React.useRef(searchText);\n\n React.useEffect(() => {\n if (\n searchText !== prevSearchTextRef.current ||\n visibleOptions.length !== prevOptionsLengthRef.current\n ) {\n setActiveIndex(-1);\n prevSearchTextRef.current = searchText;\n prevOptionsLengthRef.current = visibleOptions.length;\n }\n }, [searchText, visibleOptions.length]);\n\n // --- scroll active option into view ---\n React.useEffect(() => {\n if (activeIndex < 0 || !listRef.current) return;\n const el = listRef.current.querySelector(`[data-option-index=\"${activeIndex}\"]`);\n if (typeof el?.scrollIntoView === \"function\") {\n el.scrollIntoView({ block: \"nearest\" });\n }\n }, [activeIndex]);\n\n // --- helpers ---\n function clearInputText() {\n if (inputValueProp === undefined) setInternalInputValue(\"\");\n onInputChange?.(\"\");\n }\n\n function selectSingle(val: string | null) {\n if (!isMulti && props.value === undefined) setInternalValue(val);\n if (!isMulti) (props as { onChange?: (v: string | null) => void }).onChange?.(val);\n }\n\n function toggleMulti(val: string) {\n const next = selectedMultiValues.includes(val)\n ? selectedMultiValues.filter((v) => v !== val)\n : [...selectedMultiValues, val];\n if (isMulti && props.value === undefined) setInternalMultiValue(next);\n if (isMulti) (props as { onChange?: (v: string[]) => void }).onChange?.(next);\n }\n\n function handleSelect(option: AutocompleteOption) {\n if (option.value.startsWith(CREATE_PREFIX)) {\n const raw = option.value.slice(CREATE_PREFIX.length);\n onCreate?.(raw);\n if (isMulti) {\n toggleMulti(raw);\n } else {\n selectSingle(raw);\n setOpen(false);\n }\n clearInputText();\n return;\n }\n\n if (isMulti) {\n toggleMulti(option.value);\n clearInputText();\n } else {\n selectSingle(option.value);\n clearInputText();\n setOpen(false);\n }\n }\n\n function handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {\n const val = e.target.value;\n if (inputValueProp === undefined) setInternalInputValue(val);\n onInputChange?.(val);\n if (!isOpen) setOpen(true);\n }\n\n function findNextEnabled(start: number, direction: 1 | -1): number | null {\n let idx = start;\n while (idx >= 0 && idx < visibleOptions.length && visibleOptions[idx]?.disabled) {\n idx += direction;\n }\n return idx >= 0 && idx < visibleOptions.length ? idx : null;\n }\n\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Flat switch is clearer than splitting into separate handler functions\n function handleKeyDown(e: React.KeyboardEvent) {\n if (disabled) return;\n\n switch (e.key) {\n case \"ArrowDown\": {\n e.preventDefault();\n if (!isOpen) {\n setOpen(true);\n return;\n }\n setActiveIndex((prev) => findNextEnabled(prev + 1, 1) ?? prev);\n break;\n }\n case \"ArrowUp\": {\n e.preventDefault();\n if (!isOpen) {\n setOpen(true);\n return;\n }\n setActiveIndex((prev) => findNextEnabled(prev - 1, -1) ?? prev);\n break;\n }\n case \"Enter\": {\n if (isOpen && activeIndex >= 0 && activeIndex < visibleOptions.length) {\n e.preventDefault();\n const opt = visibleOptions[activeIndex];\n if (opt && !opt.disabled) handleSelect(opt);\n }\n break;\n }\n case \"Escape\": {\n e.preventDefault();\n if (isOpen) setOpen(false);\n break;\n }\n case \"Backspace\": {\n if (isMulti && searchText === \"\" && selectedMultiValues.length > 0) {\n const lastVal = selectedMultiValues[selectedMultiValues.length - 1];\n if (lastVal !== undefined) toggleMulti(lastVal);\n }\n break;\n }\n case \"Home\": {\n if (isOpen) {\n e.preventDefault();\n const idx = findNextEnabled(0, 1);\n if (idx !== null) setActiveIndex(idx);\n }\n break;\n }\n case \"End\": {\n if (isOpen) {\n e.preventDefault();\n const idx = findNextEnabled(visibleOptions.length - 1, -1);\n if (idx !== null) setActiveIndex(idx);\n }\n break;\n }\n }\n }\n\n function handleClear(e: React.MouseEvent) {\n e.stopPropagation();\n if (isMulti) {\n if (props.value === undefined) setInternalMultiValue([]);\n if (isMulti) (props as { onChange?: (v: string[]) => void }).onChange?.([]);\n } else {\n selectSingle(null);\n }\n clearInputText();\n inputRef.current?.focus();\n }\n\n function handleBlur(e: React.FocusEvent<HTMLInputElement>) {\n const container = e.currentTarget.closest(\"[data-autocomplete-root]\");\n if (container && e.relatedTarget instanceof Node && container.contains(e.relatedTarget)) {\n return;\n }\n if (\n e.relatedTarget instanceof Node &&\n (e.relatedTarget as Element).closest?.(\"[data-radix-popper-content-wrapper]\")\n ) {\n return;\n }\n if (isOpen) setOpen(false);\n }\n\n function handleFocus() {\n if (!disabled && !isOpen) setOpen(true);\n }\n\n function handleContainerClick() {\n if (!disabled) {\n inputRef.current?.focus();\n if (!isOpen) setOpen(true);\n }\n }\n\n function handleOpenChange(next: boolean) {\n setOpen(next);\n if (!next) {\n clearInputText();\n setActiveIndex(-1);\n }\n }\n\n const selectedOption = React.useMemo(\n () => options.find((o) => o.value === selectedValue),\n [options, selectedValue],\n );\n\n const selectedMultiOptions = React.useMemo(\n () =>\n selectedMultiValues\n .map((v) => options.find((o) => o.value === v))\n .filter(Boolean) as AutocompleteOption[],\n [options, selectedMultiValues],\n );\n\n const hasClearableValue = isMulti ? selectedMultiValues.length > 0 : selectedValue != null;\n\n const displayInputValue = React.useMemo(() => {\n if (searchText) return searchText;\n if (isMulti || isOpen) return \"\";\n return selectedOption ? getLabel(selectedOption) : \"\";\n }, [searchText, isMulti, isOpen, selectedOption]);\n\n const activeDescendantId = activeIndex >= 0 ? `${listboxId}-option-${activeIndex}` : undefined;\n\n return {\n inputId,\n helperTextId,\n listboxId,\n inputRef,\n listRef,\n isMulti,\n isOpen,\n selectedValue,\n selectedMultiValues,\n selectedMultiOptions,\n searchText,\n visibleOptions,\n visibleSections,\n createOption,\n activeIndex,\n activeDescendantId,\n hasClearableValue,\n displayInputValue,\n setActiveIndex,\n handleSelect,\n handleInputChange,\n handleKeyDown,\n handleClear,\n handleBlur,\n handleFocus,\n handleContainerClick,\n handleOpenChange,\n toggleMulti,\n };\n}\n"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -340,6 +340,31 @@ export declare interface AudioValidationError {
|
|
|
340
340
|
message: string;
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
/**
|
|
344
|
+
* A combobox input with single- or multi-select, optional async loading, and
|
|
345
|
+
* native support for grouped + pinned options.
|
|
346
|
+
*
|
|
347
|
+
* - Pass `groups` plus an `options` array whose entries reference each group
|
|
348
|
+
* via `groupId` to render hierarchical lists with proper `role="group"` +
|
|
349
|
+
* `aria-labelledby` semantics. Options without a `groupId` render above
|
|
350
|
+
* the first group; options marked `pinned` render above everything and
|
|
351
|
+
* bypass the search filter.
|
|
352
|
+
* - Indentation of nested rows (e.g. price under product) is controlled by
|
|
353
|
+
* the consumer via `renderOption` styling — there is no built-in indent.
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* ```tsx
|
|
357
|
+
* <Autocomplete
|
|
358
|
+
* aria-label="Choose a product"
|
|
359
|
+
* options={[
|
|
360
|
+
* { value: "__new__", label: "+ Create new product", pinned: true },
|
|
361
|
+
* { value: "product:abc", label: "Demo Product", groupId: "recent" },
|
|
362
|
+
* ]}
|
|
363
|
+
* groups={[{ id: "recent", label: "Recent products" }]}
|
|
364
|
+
* onChange={handleSelect}
|
|
365
|
+
* />
|
|
366
|
+
* ```
|
|
367
|
+
*/
|
|
343
368
|
export declare const Autocomplete: React_2.ForwardRefExoticComponent<AutocompleteProps & React_2.RefAttributes<HTMLInputElement>>;
|
|
344
369
|
|
|
345
370
|
declare interface AutocompleteBaseProps {
|
|
@@ -383,6 +408,40 @@ declare interface AutocompleteBaseProps {
|
|
|
383
408
|
open?: boolean;
|
|
384
409
|
defaultOpen?: boolean;
|
|
385
410
|
onOpenChange?: (open: boolean) => void;
|
|
411
|
+
/**
|
|
412
|
+
* Ordered list of groups. When provided, options whose `groupId` matches
|
|
413
|
+
* an entry render under the corresponding heading. Groups with no visible
|
|
414
|
+
* (post-filter) options collapse silently. Pinned options render above
|
|
415
|
+
* everything; ungrouped options render between pinned and the first group.
|
|
416
|
+
* Only one level of grouping is supported.
|
|
417
|
+
*/
|
|
418
|
+
groups?: AutocompleteGroup[];
|
|
419
|
+
/**
|
|
420
|
+
* Custom renderer for group headings. The returned node is wrapped by the
|
|
421
|
+
* component in an element carrying the `id` referenced by the surrounding
|
|
422
|
+
* `role="group"` wrapper's `aria-labelledby`, so consumers only need to
|
|
423
|
+
* return visual content.
|
|
424
|
+
*/
|
|
425
|
+
renderGroupHeading?: (group: AutocompleteGroup) => React_2.ReactNode;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Describes a single group rendered above its matching options.
|
|
430
|
+
* Indentation of nested rows (e.g. price under product) is not built in —
|
|
431
|
+
* consumers control it via `renderOption` styling.
|
|
432
|
+
*/
|
|
433
|
+
export declare interface AutocompleteGroup {
|
|
434
|
+
/** Stable identifier referenced by `option.groupId`. */
|
|
435
|
+
id: string;
|
|
436
|
+
/**
|
|
437
|
+
* Group heading text. Used as the group's accessible name and matched
|
|
438
|
+
* against the search query: when the query matches a group's `label`,
|
|
439
|
+
* every option under that group is kept regardless of whether it
|
|
440
|
+
* individually matches the per-option filter. This supports the common
|
|
441
|
+
* "heading is the searchable label, items are sub-rows" shape (e.g.
|
|
442
|
+
* heading = product name, items = prices).
|
|
443
|
+
*/
|
|
444
|
+
label: string;
|
|
386
445
|
}
|
|
387
446
|
|
|
388
447
|
declare interface AutocompleteMultiProps extends AutocompleteBaseProps {
|
|
@@ -393,9 +452,25 @@ declare interface AutocompleteMultiProps extends AutocompleteBaseProps {
|
|
|
393
452
|
}
|
|
394
453
|
|
|
395
454
|
export declare interface AutocompleteOption {
|
|
455
|
+
/** Unique value identifying the option. Returned via `onChange`. */
|
|
396
456
|
value: string;
|
|
457
|
+
/** Visible label. Falls back to `value` when omitted. */
|
|
397
458
|
label?: string;
|
|
459
|
+
/** When `true`, the option renders but cannot be selected. @default false */
|
|
398
460
|
disabled?: boolean;
|
|
461
|
+
/**
|
|
462
|
+
* ID of the group this option belongs to. Must match an entry in the
|
|
463
|
+
* component's `groups` prop. Options without a `groupId` render in an
|
|
464
|
+
* implicit "ungrouped" bucket above the first declared group.
|
|
465
|
+
*/
|
|
466
|
+
groupId?: string;
|
|
467
|
+
/**
|
|
468
|
+
* Pinned options bypass the search filter and stay visible at the top of
|
|
469
|
+
* the list regardless of the current query. Useful for "+ Create new"
|
|
470
|
+
* affordances. Pinned options ignore `groupId` and render before any
|
|
471
|
+
* grouped or ungrouped content. @default false
|
|
472
|
+
*/
|
|
473
|
+
pinned?: boolean;
|
|
399
474
|
}
|
|
400
475
|
|
|
401
476
|
export declare type AutocompleteProps = AutocompleteSingleProps | AutocompleteMultiProps;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fanvue/ui",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.0",
|
|
4
4
|
"description": "React component library built with Tailwind CSS for Fanvue ecosystem",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
|
@@ -166,7 +166,10 @@
|
|
|
166
166
|
"ajv": ">=8.18.0",
|
|
167
167
|
"brace-expansion": ">=5.0.5",
|
|
168
168
|
"yaml": ">=2.8.3",
|
|
169
|
-
"postcss": ">=8.5.10"
|
|
169
|
+
"postcss": ">=8.5.10",
|
|
170
|
+
"tmp": ">=0.2.6",
|
|
171
|
+
"ws": ">=8.20.1",
|
|
172
|
+
"qs": ">=6.15.2"
|
|
170
173
|
}
|
|
171
174
|
},
|
|
172
175
|
"size-limit": [
|