@shadow-js/ui 0.0.0-stage → 1.0.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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +598 -2
- package/dist/cli.mjs +2293 -0
- package/dist/dev/cli/schema.d.ts +73 -0
- package/dist/dev/index.d.ts +6 -0
- package/dist/dev/index.js +5 -0
- package/dist/dev/locales/ar.d.ts +141 -0
- package/dist/dev/locales/ar.js +128 -0
- package/dist/dev/locales/ar.js.map +1 -0
- package/dist/dev/locales/en.d.ts +141 -0
- package/dist/dev/locales/en.js +128 -0
- package/dist/dev/locales/en.js.map +1 -0
- package/dist/dev/registry/utils/utils.d.ts +29 -0
- package/dist/dev/registry/utils/utils.js +27 -0
- package/dist/dev/registry/utils/utils.js.map +1 -0
- package/dist/dev/theme/theme-logic.d.ts +187 -0
- package/dist/dev/theme/theme-logic.js +184 -0
- package/dist/dev/theme/theme-logic.js.map +1 -0
- package/dist/dev/theme/theme-provider.d.ts +90 -0
- package/dist/dev/theme/theme-provider.js +150 -0
- package/dist/dev/theme/theme-provider.js.map +1 -0
- package/dist/prod/index.js +1 -0
- package/dist/prod/locales/ar.js +2 -0
- package/dist/prod/locales/ar.js.map +1 -0
- package/dist/prod/locales/en.js +2 -0
- package/dist/prod/locales/en.js.map +1 -0
- package/dist/prod/registry/utils/utils.js +2 -0
- package/dist/prod/registry/utils/utils.js.map +1 -0
- package/dist/prod/theme/theme-logic.js +2 -0
- package/dist/prod/theme/theme-logic.js.map +1 -0
- package/dist/prod/theme/theme-provider.js +2 -0
- package/dist/prod/theme/theme-provider.js.map +1 -0
- package/package.json +102 -4
- package/registry/accordion/accordion.tsx +210 -0
- package/registry/accordion/item.json +4 -0
- package/registry/alert/alert.tsx +200 -0
- package/registry/alert/item.json +4 -0
- package/registry/alert-dialog/alert-dialog.tsx +530 -0
- package/registry/alert-dialog/item.json +4 -0
- package/registry/aspect-ratio/aspect-ratio.tsx +71 -0
- package/registry/aspect-ratio/item.json +4 -0
- package/registry/attachment/attachment.tsx +459 -0
- package/registry/attachment/item.json +4 -0
- package/registry/avatar/avatar.tsx +385 -0
- package/registry/avatar/item.json +4 -0
- package/registry/badge/badge.tsx +108 -0
- package/registry/badge/item.json +4 -0
- package/registry/breadcrumb/breadcrumb.tsx +354 -0
- package/registry/breadcrumb/item.json +4 -0
- package/registry/bubble/bubble.tsx +260 -0
- package/registry/bubble/item.json +4 -0
- package/registry/button/button.tsx +125 -0
- package/registry/button/item.json +4 -0
- package/registry/button-group/button-group.tsx +189 -0
- package/registry/button-group/item.json +4 -0
- package/registry/calendar/calendar.tsx +348 -0
- package/registry/calendar/item.json +4 -0
- package/registry/card/card.tsx +311 -0
- package/registry/card/item.json +4 -0
- package/registry/carousel/carousel.tsx +851 -0
- package/registry/carousel/item.json +4 -0
- package/registry/chart/chart.tsx +1956 -0
- package/registry/chart/item.json +4 -0
- package/registry/chart-scale/chart-scale.ts +670 -0
- package/registry/chart-scale/item.json +5 -0
- package/registry/checkbox/checkbox.tsx +79 -0
- package/registry/checkbox/item.json +4 -0
- package/registry/code-block/code-block.tsx +470 -0
- package/registry/code-block/item.json +4 -0
- package/registry/collapsible/collapsible.tsx +117 -0
- package/registry/collapsible/item.json +4 -0
- package/registry/combobox/combobox.tsx +977 -0
- package/registry/combobox/item.json +4 -0
- package/registry/command/command.tsx +757 -0
- package/registry/command/item.json +4 -0
- package/registry/context-menu/context-menu.tsx +697 -0
- package/registry/context-menu/item.json +4 -0
- package/registry/data-table/data-table.tsx +437 -0
- package/registry/data-table/item.json +4 -0
- package/registry/date-picker/date-picker.tsx +349 -0
- package/registry/date-picker/item.json +4 -0
- package/registry/dialog/dialog.tsx +412 -0
- package/registry/dialog/item.json +4 -0
- package/registry/direction/direction.tsx +88 -0
- package/registry/direction/item.json +4 -0
- package/registry/drawer/drawer.tsx +787 -0
- package/registry/drawer/item.json +4 -0
- package/registry/dropdown-menu/dropdown-menu.tsx +692 -0
- package/registry/dropdown-menu/item.json +4 -0
- package/registry/empty/empty.tsx +240 -0
- package/registry/empty/item.json +4 -0
- package/registry/field/field.tsx +624 -0
- package/registry/field/item.json +4 -0
- package/registry/form/form.tsx +1112 -0
- package/registry/form/item.json +4 -0
- package/registry/hover-card/hover-card.tsx +144 -0
- package/registry/hover-card/item.json +4 -0
- package/registry/icons/icons.tsx +1691 -0
- package/registry/icons/item.json +4 -0
- package/registry/input/input.tsx +43 -0
- package/registry/input/item.json +4 -0
- package/registry/input-group/input-group.tsx +315 -0
- package/registry/input-group/item.json +4 -0
- package/registry/input-otp/input-otp.tsx +329 -0
- package/registry/input-otp/item.json +4 -0
- package/registry/item/item.json +4 -0
- package/registry/item/item.tsx +439 -0
- package/registry/kbd/item.json +4 -0
- package/registry/kbd/kbd.tsx +65 -0
- package/registry/label/item.json +4 -0
- package/registry/label/label.tsx +60 -0
- package/registry/marker/item.json +4 -0
- package/registry/marker/marker.tsx +175 -0
- package/registry/menubar/item.json +4 -0
- package/registry/menubar/menubar.tsx +694 -0
- package/registry/message/item.json +4 -0
- package/registry/message/message.tsx +280 -0
- package/registry/message-scroller/item.json +4 -0
- package/registry/message-scroller/message-scroller.tsx +262 -0
- package/registry/native-select/item.json +4 -0
- package/registry/native-select/native-select.tsx +113 -0
- package/registry/navigation-menu/item.json +4 -0
- package/registry/navigation-menu/navigation-menu.tsx +362 -0
- package/registry/pagination/item.json +4 -0
- package/registry/pagination/pagination.tsx +460 -0
- package/registry/password-input/item.json +5 -0
- package/registry/password-input/password-input.tsx +494 -0
- package/registry/popover/item.json +4 -0
- package/registry/popover/popover.tsx +284 -0
- package/registry/progress/item.json +4 -0
- package/registry/progress/progress.tsx +190 -0
- package/registry/questionnaire/item.json +4 -0
- package/registry/questionnaire/questionnaire.tsx +569 -0
- package/registry/radio-group/item.json +4 -0
- package/registry/radio-group/radio-group.tsx +101 -0
- package/registry/resizable/item.json +4 -0
- package/registry/resizable/resizable.tsx +455 -0
- package/registry/scroll-area/item.json +4 -0
- package/registry/scroll-area/scroll-area.tsx +149 -0
- package/registry/select/item.json +4 -0
- package/registry/select/select.tsx +571 -0
- package/registry/separator/item.json +4 -0
- package/registry/separator/separator.tsx +52 -0
- package/registry/sheet/item.json +4 -0
- package/registry/sheet/sheet.tsx +476 -0
- package/registry/sidebar/item.json +4 -0
- package/registry/sidebar/sidebar.tsx +1870 -0
- package/registry/skeleton/item.json +4 -0
- package/registry/skeleton/skeleton.tsx +47 -0
- package/registry/slider/item.json +4 -0
- package/registry/slider/slider.tsx +149 -0
- package/registry/spinner/item.json +4 -0
- package/registry/spinner/spinner.tsx +51 -0
- package/registry/switch/item.json +4 -0
- package/registry/switch/switch.tsx +93 -0
- package/registry/table/item.json +4 -0
- package/registry/table/table.tsx +373 -0
- package/registry/tabs/item.json +4 -0
- package/registry/tabs/tabs.tsx +209 -0
- package/registry/textarea/item.json +4 -0
- package/registry/textarea/textarea.tsx +51 -0
- package/registry/toast/item.json +4 -0
- package/registry/toast/toast.tsx +949 -0
- package/registry/toggle/item.json +4 -0
- package/registry/toggle/toggle.tsx +110 -0
- package/registry/toggle-group/item.json +4 -0
- package/registry/toggle-group/toggle-group.tsx +189 -0
- package/registry/tooltip/item.json +4 -0
- package/registry/tooltip/tooltip.tsx +169 -0
- package/registry/trace/item.json +4 -0
- package/registry/trace/trace.tsx +356 -0
- package/registry/ui-strings/item.json +5 -0
- package/registry/ui-strings/ui-strings.tsx +454 -0
- package/registry/use-highlighted-code/item.json +5 -0
- package/registry/use-highlighted-code/use-highlighted-code.tsx +145 -0
- package/registry/use-mobile/item.json +5 -0
- package/registry/use-mobile/use-mobile.ts +46 -0
- package/registry/utils/item.json +5 -0
- package/registry/utils/utils.ts +38 -0
- package/registry.json +3274 -0
- package/styles/animations.css +321 -0
- package/styles/theme-core.css +55 -0
- package/styles/theme.css +12 -0
- package/styles/themes/gray.css +69 -0
- package/styles/themes/neutral.css +69 -0
- package/styles/themes/slate.css +69 -0
- package/styles/themes/stone.css +69 -0
- package/styles/themes/zinc.css +69 -0
- package/styles/typeset.css +251 -0
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
import { createContext, splitProps, useContext } from "@shadow-js/core";
|
|
2
|
+
import {
|
|
3
|
+
type ComboboxChipProps as ComboboxChipPrimitiveProps,
|
|
4
|
+
type ComboboxChipRemoveProps,
|
|
5
|
+
type ComboboxClearProps,
|
|
6
|
+
type ComboboxContentProps as ComboboxContentPrimitiveProps,
|
|
7
|
+
type ComboboxEmptyProps,
|
|
8
|
+
type ComboboxGroupProps,
|
|
9
|
+
type ComboboxInputProps as ComboboxInputPrimitiveProps,
|
|
10
|
+
type ComboboxItemProps as ComboboxItemPrimitiveProps,
|
|
11
|
+
type ComboboxLabelProps,
|
|
12
|
+
type ComboboxLoadingProps,
|
|
13
|
+
Combobox as ComboboxPrimitive,
|
|
14
|
+
type ComboboxProps,
|
|
15
|
+
type ComboboxSeparatorProps,
|
|
16
|
+
type ComboboxTriggerProps,
|
|
17
|
+
type ComboboxChipsProps as ComboboxValueProps,
|
|
18
|
+
type PolymorphicTarget,
|
|
19
|
+
} from "@shadow-js/primitives";
|
|
20
|
+
import { type ComponentProps, For, type JSX } from "@shadow-js/web";
|
|
21
|
+
import { CheckIcon, ChevronDownIcon, XIcon } from "@/components/ui/icons";
|
|
22
|
+
import { useUiStrings } from "@/lib/ui-strings";
|
|
23
|
+
import { type ClassValue, cn } from "@/lib/utils";
|
|
24
|
+
|
|
25
|
+
/** The items a combobox lists, read by {@link ComboboxList}. */
|
|
26
|
+
const itemsContext = createContext<() => readonly unknown[] | undefined>(() => undefined, {
|
|
27
|
+
name: "ComboboxItems",
|
|
28
|
+
});
|
|
29
|
+
/** The items of the group around a {@link ComboboxCollection}. */
|
|
30
|
+
const groupItemsContext = createContext<() => readonly unknown[] | undefined>(() => undefined, {
|
|
31
|
+
name: "ComboboxGroupItems",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The `items` prop {@link Combobox} adds to the primitive's props: the data that a
|
|
36
|
+
* {@link ComboboxList} with a render function turns into {@link ComboboxItem} parts.
|
|
37
|
+
* @since 1.0.0
|
|
38
|
+
*/
|
|
39
|
+
export interface ComboboxItemsProps<T = unknown> {
|
|
40
|
+
/**
|
|
41
|
+
* The data behind the options. A `ComboboxList` whose children are a function renders it once
|
|
42
|
+
* per item. The options still filter themselves from the typed text.
|
|
43
|
+
* @default none
|
|
44
|
+
*/
|
|
45
|
+
readonly items?: readonly T[] | undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The props as the implementation sees them. */
|
|
49
|
+
type Loose = {
|
|
50
|
+
readonly class?: ClassValue;
|
|
51
|
+
readonly children?: JSX.Element;
|
|
52
|
+
readonly [prop: string]: unknown;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** The look shared by the single-line field and the chips field. */
|
|
56
|
+
const FIELD =
|
|
57
|
+
"relative flex w-full min-w-0 rounded-md border border-input outline-none bg-transparent shadow-xs transition-[color,box-shadow] motion-reduce:transition-none dark:bg-input/30 has-[input:focus-visible]:border-ring has-[input:focus-visible]:ring-[3px] has-[input:focus-visible]:ring-ring/50 has-[input:focus-visible]:outline-solid has-[input:focus-visible]:outline-2 has-[input:focus-visible]:outline-offset-2 has-[input:focus-visible]:outline-transparent has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50 has-[input[aria-invalid=true]]:border-destructive has-[input[aria-invalid=true]]:ring-destructive/20 dark:has-[input[aria-invalid=true]]:ring-destructive/40";
|
|
58
|
+
|
|
59
|
+
/** The look of the small icon buttons at the end of a field. */
|
|
60
|
+
const ICON_BUTTON =
|
|
61
|
+
"inline-flex size-7 shrink-0 items-center justify-center rounded-sm text-muted-foreground opacity-50 outline-none hover:opacity-100 disabled:pointer-events-none disabled:opacity-30 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A text field with a filtered list of options. The user types to narrow the options and picks one;
|
|
65
|
+
* with `multiple`, the picks show as removable chips. Use it for a searchable choice, such as a
|
|
66
|
+
* framework or tag picker.
|
|
67
|
+
*
|
|
68
|
+
* It is controlled with `value` / `onValueChange`, `inputValue` / `onInputValueChange` and `open` /
|
|
69
|
+
* `onOpenChange`, or uncontrolled with the `default*` props. With a `name` it takes part in native
|
|
70
|
+
* forms. Set `shouldFilter={false}` to filter on the server and render exactly the items you want
|
|
71
|
+
* shown. While the list is closed its items are read once without being shown, so a `defaultValue`
|
|
72
|
+
* already shows its label; pass `itemToString` for server rendering and for items that arrive later.
|
|
73
|
+
*
|
|
74
|
+
* Keyboard and ARIA: the input keeps focus and is the only tab stop. Typing opens and filters the
|
|
75
|
+
* list. ArrowDown opens it and highlights the first option, or moves to the next one; ArrowUp does
|
|
76
|
+
* the same from the end. Alt+ArrowDown opens without moving the highlight and Alt+ArrowUp closes.
|
|
77
|
+
* Home and End jump to the first and last option while one is highlighted. Enter picks the
|
|
78
|
+
* highlighted option and never submits a surrounding form while the list is open. Escape closes the
|
|
79
|
+
* list, and a second Escape clears the field. Tab closes the list. With `multiple`, Backspace in an
|
|
80
|
+
* empty input removes the last chip, and ArrowLeft with the caret at the start moves focus to the
|
|
81
|
+
* chips. The input is `role="combobox"` with `aria-autocomplete="list"`, `aria-haspopup="listbox"`,
|
|
82
|
+
* `aria-expanded`, `aria-controls` (while the list is open) and `aria-activedescendant` (the
|
|
83
|
+
* highlighted option). The list is a `role="listbox"` of `role="option"` items with `aria-selected`.
|
|
84
|
+
* Name the input with a `ComboboxLabel` placed outside a group, or with `aria-label`.
|
|
85
|
+
*
|
|
86
|
+
* shadcn/ui differences: this is one set of parts (`ComboboxInput`, `ComboboxContent`,
|
|
87
|
+
* `ComboboxItem`, ...) rather than a `Popover` wrapped around a `Command`. `as` replaces `asChild` on the parts that accept it, and every part has a `data-slot`. The prop is `class`, not `className`.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* import {
|
|
91
|
+
* Combobox,
|
|
92
|
+
* ComboboxContent,
|
|
93
|
+
* ComboboxEmpty,
|
|
94
|
+
* ComboboxInput,
|
|
95
|
+
* ComboboxItem,
|
|
96
|
+
* } from "@/components/ui/combobox";
|
|
97
|
+
*
|
|
98
|
+
* export function FrameworkPicker() {
|
|
99
|
+
* return (
|
|
100
|
+
* <Combobox>
|
|
101
|
+
* <ComboboxInput placeholder="Select a framework" />
|
|
102
|
+
* <ComboboxContent>
|
|
103
|
+
* <ComboboxEmpty>No framework found.</ComboboxEmpty>
|
|
104
|
+
* <ComboboxItem value="next">Next.js</ComboboxItem>
|
|
105
|
+
* <ComboboxItem value="astro">Astro</ComboboxItem>
|
|
106
|
+
* </ComboboxContent>
|
|
107
|
+
* </Combobox>
|
|
108
|
+
* );
|
|
109
|
+
* }
|
|
110
|
+
* @see {@link ComboboxInput}
|
|
111
|
+
* @see {@link ComboboxContent}
|
|
112
|
+
* @since 1.0.0
|
|
113
|
+
*/
|
|
114
|
+
function Combobox<T = unknown>(props: ComboboxProps & ComboboxItemsProps<T>): JSX.Element {
|
|
115
|
+
const [local, rest] = splitProps(props as ComboboxProps & ComboboxItemsProps<T>, ["items"]);
|
|
116
|
+
return (
|
|
117
|
+
<itemsContext.Provider value={() => local.items}>
|
|
118
|
+
<ComboboxPrimitive {...(rest as ComboboxProps)} />
|
|
119
|
+
</itemsContext.Provider>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Own props of {@link ComboboxInput}: the parts the field adds around the `input`.
|
|
125
|
+
* @see {@link ComboboxInput}
|
|
126
|
+
* @since 1.0.0
|
|
127
|
+
*/
|
|
128
|
+
export interface ComboboxInputExtraProps {
|
|
129
|
+
/** Shows the chevron button that toggles the list. @default true */
|
|
130
|
+
readonly showTrigger?: boolean | undefined;
|
|
131
|
+
/** Shows the button that clears the text and the selection. @default false */
|
|
132
|
+
readonly showClear?: boolean | undefined;
|
|
133
|
+
/** Rendered after the buttons, inside the field: an icon, or a button of your own. @default none */
|
|
134
|
+
readonly children?: JSX.Element | undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Props of {@link ComboboxInput}: the attributes of the `input` and {@link ComboboxInputExtraProps}.
|
|
139
|
+
* `class` goes to the field around the input, every other attribute to the `input`.
|
|
140
|
+
* @see {@link ComboboxInput}
|
|
141
|
+
* @since 1.0.0
|
|
142
|
+
*/
|
|
143
|
+
export type ComboboxInputProps = ComboboxInputPrimitiveProps & ComboboxInputExtraProps;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The text field of a combobox: the input that keeps focus, the chevron that toggles the list and,
|
|
147
|
+
* with `showClear`, a clear button. The list is placed
|
|
148
|
+
* against the whole field. Name the input with a `ComboboxLabel` or an `aria-label`. Set
|
|
149
|
+
* `aria-invalid="true"` on the input to draw the field with the destructive border.
|
|
150
|
+
*
|
|
151
|
+
* @param props The input props: see {@link ComboboxInputProps}.
|
|
152
|
+
* @returns The field, with the input inside it.
|
|
153
|
+
* @throws Error when rendered outside a {@link Combobox}.
|
|
154
|
+
* @example
|
|
155
|
+
* import { Combobox, ComboboxContent, ComboboxInput, ComboboxItem } from "@/components/ui/combobox";
|
|
156
|
+
*
|
|
157
|
+
* export function Search() {
|
|
158
|
+
* return (
|
|
159
|
+
* <Combobox>
|
|
160
|
+
* <ComboboxInput placeholder="Search..." showClear aria-label="Search" />
|
|
161
|
+
* <ComboboxContent>
|
|
162
|
+
* <ComboboxItem value="next">Next.js</ComboboxItem>
|
|
163
|
+
* </ComboboxContent>
|
|
164
|
+
* </Combobox>
|
|
165
|
+
* );
|
|
166
|
+
* }
|
|
167
|
+
* @see {@link Combobox}
|
|
168
|
+
* @see {@link ComboboxInputProps}
|
|
169
|
+
* @since 1.0.0
|
|
170
|
+
*/
|
|
171
|
+
function ComboboxInput(props: ComboboxInputProps): JSX.Element {
|
|
172
|
+
const [local, rest] = splitProps(props as unknown as Loose & ComboboxInputExtraProps, [
|
|
173
|
+
"class",
|
|
174
|
+
"children",
|
|
175
|
+
"showTrigger",
|
|
176
|
+
"showClear",
|
|
177
|
+
]);
|
|
178
|
+
return (
|
|
179
|
+
<ComboboxPrimitive.Anchor
|
|
180
|
+
data-slot="combobox-input-group"
|
|
181
|
+
class={cn(FIELD, "h-9 items-center", local.class)}
|
|
182
|
+
>
|
|
183
|
+
<ComboboxPrimitive.Input
|
|
184
|
+
data-slot="combobox-input"
|
|
185
|
+
class="h-full min-w-0 flex-1 bg-transparent px-3 py-1 text-base outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:cursor-not-allowed md:text-sm"
|
|
186
|
+
{...rest}
|
|
187
|
+
/>
|
|
188
|
+
{local.showClear === true && <ComboboxClear />}
|
|
189
|
+
{local.showTrigger !== false && <ComboboxTrigger />}
|
|
190
|
+
{local.children}
|
|
191
|
+
</ComboboxPrimitive.Anchor>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The chevron button that toggles the list. It is not a tab stop (the input is) and pressing it keeps
|
|
197
|
+
* focus in the input. Its default `aria-label` is `"Toggle suggestions"`; pass your own to translate
|
|
198
|
+
* it. `children` replaces the chevron icon.
|
|
199
|
+
*
|
|
200
|
+
* @param props The button props: the attributes of the rendered element, and `children`.
|
|
201
|
+
* @returns The rendered trigger button.
|
|
202
|
+
* @throws Error when rendered outside a {@link Combobox}.
|
|
203
|
+
* @example
|
|
204
|
+
* import { Combobox, ComboboxContent, ComboboxInput, ComboboxItem, ComboboxTrigger } from "@/components/ui/combobox";
|
|
205
|
+
*
|
|
206
|
+
* export function FruitPicker() {
|
|
207
|
+
* return (
|
|
208
|
+
* <Combobox>
|
|
209
|
+
* <ComboboxInput showTrigger={false} aria-label="Fruit" />
|
|
210
|
+
* <ComboboxTrigger aria-label="Show fruits" />
|
|
211
|
+
* <ComboboxContent>
|
|
212
|
+
* <ComboboxItem value="apple">Apple</ComboboxItem>
|
|
213
|
+
* </ComboboxContent>
|
|
214
|
+
* </Combobox>
|
|
215
|
+
* );
|
|
216
|
+
* }
|
|
217
|
+
* @see {@link ComboboxInput}
|
|
218
|
+
* @since 1.0.0
|
|
219
|
+
*/
|
|
220
|
+
function ComboboxTrigger<T extends PolymorphicTarget = "button">(
|
|
221
|
+
props: ComboboxTriggerProps<T>,
|
|
222
|
+
): JSX.Element {
|
|
223
|
+
const [local, rest] = splitProps(props as unknown as Loose, ["class", "children"]);
|
|
224
|
+
const t = useUiStrings();
|
|
225
|
+
return (
|
|
226
|
+
<ComboboxPrimitive.Trigger
|
|
227
|
+
data-slot="combobox-trigger"
|
|
228
|
+
aria-label={t("combobox.toggle", "Toggle suggestions")}
|
|
229
|
+
class={cn(ICON_BUTTON, "me-0.5", local.class)}
|
|
230
|
+
{...rest}
|
|
231
|
+
>
|
|
232
|
+
{local.children ?? <ChevronDownIcon />}
|
|
233
|
+
</ComboboxPrimitive.Trigger>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The button that empties the text and the selection and keeps focus in the input. It is hidden
|
|
239
|
+
* while there is nothing to clear. Its default `aria-label` is `"Clear"`; pass your own to translate
|
|
240
|
+
* it. `children` replaces the X icon.
|
|
241
|
+
*
|
|
242
|
+
* @param props The button props: the attributes of the rendered element, and `children`.
|
|
243
|
+
* @returns The rendered clear button, or nothing while it is hidden.
|
|
244
|
+
* @throws Error when rendered outside a {@link Combobox}.
|
|
245
|
+
* @example
|
|
246
|
+
* import { Combobox, ComboboxClear, ComboboxInput } from "@/components/ui/combobox";
|
|
247
|
+
*
|
|
248
|
+
* export function Cleared() {
|
|
249
|
+
* return (
|
|
250
|
+
* <Combobox>
|
|
251
|
+
* <ComboboxInput showTrigger={false} aria-label="Fruit" />
|
|
252
|
+
* <ComboboxClear aria-label="Clear selection" />
|
|
253
|
+
* </Combobox>
|
|
254
|
+
* );
|
|
255
|
+
* }
|
|
256
|
+
* @see {@link ComboboxInput}
|
|
257
|
+
* @since 1.0.0
|
|
258
|
+
*/
|
|
259
|
+
function ComboboxClear<T extends PolymorphicTarget = "button">(
|
|
260
|
+
props: ComboboxClearProps<T>,
|
|
261
|
+
): JSX.Element {
|
|
262
|
+
const [local, rest] = splitProps(props as unknown as Loose, ["class", "children"]);
|
|
263
|
+
const t = useUiStrings();
|
|
264
|
+
return (
|
|
265
|
+
<ComboboxPrimitive.Clear
|
|
266
|
+
data-slot="combobox-clear"
|
|
267
|
+
aria-label={t("combobox.clear", "Clear")}
|
|
268
|
+
class={cn(ICON_BUTTON, local.class)}
|
|
269
|
+
{...rest}
|
|
270
|
+
>
|
|
271
|
+
{local.children ?? <XIcon />}
|
|
272
|
+
</ComboboxPrimitive.Clear>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Props of {@link ComboboxContent}: the placement options of the popup (such as `side`, `align` and
|
|
278
|
+
* `sideOffset`) and the attributes of the rendered `div`.
|
|
279
|
+
* @see {@link ComboboxContent}
|
|
280
|
+
* @since 1.0.0
|
|
281
|
+
*/
|
|
282
|
+
export type ComboboxContentProps<T extends PolymorphicTarget = "div"> =
|
|
283
|
+
ComboboxContentPrimitiveProps<T>;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The popup list of options. It is portaled to `document.body`, placed against the field and as wide
|
|
287
|
+
* as the field, with a 4 px gap unless you pass `sideOffset`. It never takes focus: the input keeps
|
|
288
|
+
* it and points at the highlighted option with `aria-activedescendant`. Pressing or focusing outside
|
|
289
|
+
* the list, or pressing Escape, closes it.
|
|
290
|
+
*
|
|
291
|
+
* Keyboard and ARIA: the list is a `role="listbox"`, named by the `ComboboxLabel` or else by the
|
|
292
|
+
* input. It is `aria-busy` while {@link ComboboxLoading} is shown and carries `data-empty` when no
|
|
293
|
+
* option matches. Put {@link ComboboxItem}, {@link ComboboxGroup} and {@link ComboboxEmpty} inside it.
|
|
294
|
+
*
|
|
295
|
+
* @param props The list props: see {@link ComboboxContentProps}.
|
|
296
|
+
* @returns The popup list, rendered in a portal while it is open.
|
|
297
|
+
* @throws Error when rendered outside a {@link Combobox}.
|
|
298
|
+
* @example
|
|
299
|
+
* import { Combobox, ComboboxContent, ComboboxInput, ComboboxItem } from "@/components/ui/combobox";
|
|
300
|
+
*
|
|
301
|
+
* export function FruitList() {
|
|
302
|
+
* return (
|
|
303
|
+
* <Combobox>
|
|
304
|
+
* <ComboboxInput aria-label="Fruit" />
|
|
305
|
+
* <ComboboxContent>
|
|
306
|
+
* <ComboboxItem value="apple">Apple</ComboboxItem>
|
|
307
|
+
* </ComboboxContent>
|
|
308
|
+
* </Combobox>
|
|
309
|
+
* );
|
|
310
|
+
* }
|
|
311
|
+
* @see {@link ComboboxItem}
|
|
312
|
+
* @since 1.0.0
|
|
313
|
+
*/
|
|
314
|
+
function ComboboxContent<T extends PolymorphicTarget = "div">(
|
|
315
|
+
props: ComboboxContentProps<T>,
|
|
316
|
+
): JSX.Element {
|
|
317
|
+
const [local, rest] = splitProps(props as unknown as Loose, ["class", "children"]);
|
|
318
|
+
return (
|
|
319
|
+
<ComboboxPrimitive.Portal>
|
|
320
|
+
<ComboboxPrimitive.Content
|
|
321
|
+
data-slot="combobox-content"
|
|
322
|
+
sideOffset={4}
|
|
323
|
+
class={cn(
|
|
324
|
+
"relative z-50 max-h-[min(24rem,var(--primitives-available-height))] w-(--primitives-anchor-width) origin-(--primitives-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
325
|
+
local.class,
|
|
326
|
+
)}
|
|
327
|
+
{...rest}
|
|
328
|
+
>
|
|
329
|
+
{local.children}
|
|
330
|
+
</ComboboxPrimitive.Content>
|
|
331
|
+
</ComboboxPrimitive.Portal>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Props of {@link ComboboxItem}: `value` (required, unique in the list), `label`, `disabled` and the
|
|
337
|
+
* attributes of the rendered `div`.
|
|
338
|
+
* @see {@link ComboboxItem}
|
|
339
|
+
* @since 1.0.0
|
|
340
|
+
*/
|
|
341
|
+
export type ComboboxItemProps<T extends PolymorphicTarget = "div"> = ComboboxItemPrimitiveProps<T>;
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* One option of the list. Its text is what the filter matches and what the field shows once the
|
|
345
|
+
* option is picked; give `label` when the children are not plain text. A check marks the picked
|
|
346
|
+
* options. A disabled option cannot be highlighted or picked. Filtered-out options are not rendered.
|
|
347
|
+
*
|
|
348
|
+
* Keyboard and ARIA: the option is a `role="option"` with `aria-selected`, and `aria-disabled` when
|
|
349
|
+
* disabled. The input keeps focus and highlights it with the arrow keys; Enter picks the highlighted
|
|
350
|
+
* option. Picking a single-select option closes the list and shows its label in the field; picking
|
|
351
|
+
* an option of a `multiple` combobox toggles it and keeps the list open.
|
|
352
|
+
*
|
|
353
|
+
* @param props The option props: see {@link ComboboxItemProps}.
|
|
354
|
+
* @returns The option, or nothing while the filter rejects it.
|
|
355
|
+
* @throws Error when rendered outside a {@link Combobox}.
|
|
356
|
+
* @example
|
|
357
|
+
* import { Combobox, ComboboxContent, ComboboxInput, ComboboxItem } from "@/components/ui/combobox";
|
|
358
|
+
*
|
|
359
|
+
* export function Frameworks() {
|
|
360
|
+
* return (
|
|
361
|
+
* <Combobox>
|
|
362
|
+
* <ComboboxInput aria-label="Framework" />
|
|
363
|
+
* <ComboboxContent>
|
|
364
|
+
* <ComboboxItem value="next">Next.js</ComboboxItem>
|
|
365
|
+
* <ComboboxItem value="astro" label="Astro">
|
|
366
|
+
* Astro
|
|
367
|
+
* </ComboboxItem>
|
|
368
|
+
* </ComboboxContent>
|
|
369
|
+
* </Combobox>
|
|
370
|
+
* );
|
|
371
|
+
* }
|
|
372
|
+
* @see {@link ComboboxContent}
|
|
373
|
+
* @since 1.0.0
|
|
374
|
+
*/
|
|
375
|
+
function ComboboxItem<T extends PolymorphicTarget = "div">(
|
|
376
|
+
props: ComboboxItemProps<T>,
|
|
377
|
+
): JSX.Element {
|
|
378
|
+
const [local, rest] = splitProps(props as unknown as Loose & { readonly value: string }, [
|
|
379
|
+
"class",
|
|
380
|
+
"children",
|
|
381
|
+
"value",
|
|
382
|
+
]);
|
|
383
|
+
return (
|
|
384
|
+
<ComboboxPrimitive.Item
|
|
385
|
+
data-slot="combobox-item"
|
|
386
|
+
value={local.value}
|
|
387
|
+
class={cn(
|
|
388
|
+
"group/combobox-item relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 ps-2 pe-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground forced-colors:data-[highlighted]:bg-[Highlight] forced-colors:data-[highlighted]:text-[HighlightText] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
|
389
|
+
local.class,
|
|
390
|
+
)}
|
|
391
|
+
{...rest}
|
|
392
|
+
>
|
|
393
|
+
{local.children}
|
|
394
|
+
<span class="pointer-events-none absolute end-2 flex size-3.5 items-center justify-center">
|
|
395
|
+
<CheckIcon class="hidden size-4 group-data-[state=checked]/combobox-item:block" />
|
|
396
|
+
</span>
|
|
397
|
+
</ComboboxPrimitive.Item>
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* The message shown inside the list while it is open and no option matches. It is hidden while
|
|
403
|
+
* {@link ComboboxLoading} is shown. The list itself announces the result count to screen readers.
|
|
404
|
+
*
|
|
405
|
+
* @param props The message props: the attributes of the rendered `div`.
|
|
406
|
+
* @returns The message, or nothing while the list has matches.
|
|
407
|
+
* @throws Error when rendered outside a {@link Combobox}.
|
|
408
|
+
* @example
|
|
409
|
+
* import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput } from "@/components/ui/combobox";
|
|
410
|
+
*
|
|
411
|
+
* export function Empty() {
|
|
412
|
+
* return (
|
|
413
|
+
* <Combobox>
|
|
414
|
+
* <ComboboxInput aria-label="Framework" />
|
|
415
|
+
* <ComboboxContent>
|
|
416
|
+
* <ComboboxEmpty>No framework found.</ComboboxEmpty>
|
|
417
|
+
* </ComboboxContent>
|
|
418
|
+
* </Combobox>
|
|
419
|
+
* );
|
|
420
|
+
* }
|
|
421
|
+
* @see {@link ComboboxContent}
|
|
422
|
+
* @since 1.0.0
|
|
423
|
+
*/
|
|
424
|
+
function ComboboxEmpty<T extends PolymorphicTarget = "div">(
|
|
425
|
+
props: ComboboxEmptyProps<T>,
|
|
426
|
+
): JSX.Element {
|
|
427
|
+
const [local, rest] = splitProps(props as unknown as Loose, ["class"]);
|
|
428
|
+
return (
|
|
429
|
+
<ComboboxPrimitive.Empty
|
|
430
|
+
data-slot="combobox-empty"
|
|
431
|
+
class={cn("py-6 text-center text-sm", local.class)}
|
|
432
|
+
{...rest}
|
|
433
|
+
/>
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* The status row shown while your results are loading. The combobox does not know about loading, so
|
|
439
|
+
* render it from your own state (for example with `Show`). While it is mounted the list is
|
|
440
|
+
* `aria-busy`.
|
|
441
|
+
*
|
|
442
|
+
* @param props The row props: the attributes of the rendered `div`.
|
|
443
|
+
* @returns The status row.
|
|
444
|
+
* @throws Error when rendered outside a {@link Combobox}.
|
|
445
|
+
* @example
|
|
446
|
+
* import { useSignal } from "@shadow-js/core";
|
|
447
|
+
* import { Show } from "@shadow-js/web";
|
|
448
|
+
* import { Combobox, ComboboxContent, ComboboxInput, ComboboxLoading } from "@/components/ui/combobox";
|
|
449
|
+
*
|
|
450
|
+
* export function Results() {
|
|
451
|
+
* const [loading] = useSignal(true);
|
|
452
|
+
* return (
|
|
453
|
+
* <Combobox>
|
|
454
|
+
* <ComboboxInput aria-label="Search" />
|
|
455
|
+
* <ComboboxContent>
|
|
456
|
+
* <Show when={loading()}>
|
|
457
|
+
* <ComboboxLoading>Searching...</ComboboxLoading>
|
|
458
|
+
* </Show>
|
|
459
|
+
* </ComboboxContent>
|
|
460
|
+
* </Combobox>
|
|
461
|
+
* );
|
|
462
|
+
* }
|
|
463
|
+
* @see {@link ComboboxContent}
|
|
464
|
+
* @since 1.0.0
|
|
465
|
+
*/
|
|
466
|
+
function ComboboxLoading<T extends PolymorphicTarget = "div">(
|
|
467
|
+
props: ComboboxLoadingProps<T>,
|
|
468
|
+
): JSX.Element {
|
|
469
|
+
const [local, rest] = splitProps(props as unknown as Loose, ["class"]);
|
|
470
|
+
return (
|
|
471
|
+
<ComboboxPrimitive.Loading
|
|
472
|
+
data-slot="combobox-loading"
|
|
473
|
+
class={cn("py-6 text-center text-sm text-muted-foreground", local.class)}
|
|
474
|
+
{...rest}
|
|
475
|
+
/>
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* A group of options under a {@link ComboboxLabel} heading, which names the group. The group is
|
|
481
|
+
* hidden while the filter rejects all of its options.
|
|
482
|
+
*
|
|
483
|
+
* @param props The group props: the attributes of the rendered `div`.
|
|
484
|
+
* @returns The group of options.
|
|
485
|
+
* @throws Error when rendered outside a {@link Combobox}.
|
|
486
|
+
* @example
|
|
487
|
+
* import { Combobox, ComboboxContent, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxLabel } from "@/components/ui/combobox";
|
|
488
|
+
*
|
|
489
|
+
* export function Tools() {
|
|
490
|
+
* return (
|
|
491
|
+
* <Combobox>
|
|
492
|
+
* <ComboboxInput aria-label="Tool" />
|
|
493
|
+
* <ComboboxContent>
|
|
494
|
+
* <ComboboxGroup>
|
|
495
|
+
* <ComboboxLabel>Frameworks</ComboboxLabel>
|
|
496
|
+
* <ComboboxItem value="next">Next.js</ComboboxItem>
|
|
497
|
+
* </ComboboxGroup>
|
|
498
|
+
* </ComboboxContent>
|
|
499
|
+
* </Combobox>
|
|
500
|
+
* );
|
|
501
|
+
* }
|
|
502
|
+
* @see {@link ComboboxLabel}
|
|
503
|
+
* @since 1.0.0
|
|
504
|
+
*/
|
|
505
|
+
function ComboboxGroup<T extends PolymorphicTarget = "div">(
|
|
506
|
+
props: ComboboxGroupProps<T> & ComboboxItemsProps,
|
|
507
|
+
): JSX.Element {
|
|
508
|
+
const [local, rest] = splitProps(
|
|
509
|
+
props as unknown as ComboboxItemsProps & Record<string, unknown>,
|
|
510
|
+
["items"],
|
|
511
|
+
);
|
|
512
|
+
return (
|
|
513
|
+
<groupItemsContext.Provider value={() => local.items}>
|
|
514
|
+
<ComboboxPrimitive.Group
|
|
515
|
+
data-slot="combobox-group"
|
|
516
|
+
{...(rest as unknown as ComboboxGroupProps<T>)}
|
|
517
|
+
/>
|
|
518
|
+
</groupItemsContext.Provider>
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* A heading. Inside a {@link ComboboxGroup} it names the group; outside one it is the label of the
|
|
524
|
+
* input and names the list.
|
|
525
|
+
*
|
|
526
|
+
* @param props The label props: the attributes of the rendered element.
|
|
527
|
+
* @returns The heading element.
|
|
528
|
+
* @throws Error when rendered outside a {@link Combobox}.
|
|
529
|
+
* @example
|
|
530
|
+
* import { Combobox, ComboboxInput, ComboboxLabel } from "@/components/ui/combobox";
|
|
531
|
+
*
|
|
532
|
+
* export function FrameworkField() {
|
|
533
|
+
* return (
|
|
534
|
+
* <Combobox>
|
|
535
|
+
* <ComboboxLabel>Framework</ComboboxLabel>
|
|
536
|
+
* <ComboboxInput />
|
|
537
|
+
* </Combobox>
|
|
538
|
+
* );
|
|
539
|
+
* }
|
|
540
|
+
* @see {@link ComboboxGroup}
|
|
541
|
+
* @since 1.0.0
|
|
542
|
+
*/
|
|
543
|
+
function ComboboxLabel<T extends PolymorphicTarget = "label">(
|
|
544
|
+
props: ComboboxLabelProps<T>,
|
|
545
|
+
): JSX.Element {
|
|
546
|
+
const [local, rest] = splitProps(props as unknown as Loose, ["class"]);
|
|
547
|
+
return (
|
|
548
|
+
<ComboboxPrimitive.Label
|
|
549
|
+
data-slot="combobox-label"
|
|
550
|
+
class={cn("px-2 py-1.5 text-xs text-muted-foreground", local.class)}
|
|
551
|
+
{...rest}
|
|
552
|
+
/>
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* A divider between groups of options. It is decorative (`role="presentation"`), and it is hidden
|
|
558
|
+
* while the filter narrows the list, because the groups it divides may then be incomplete.
|
|
559
|
+
*
|
|
560
|
+
* @param props The divider props: the attributes of the rendered `div`.
|
|
561
|
+
* @returns The divider.
|
|
562
|
+
* @throws Error when rendered outside a {@link Combobox}.
|
|
563
|
+
* @example
|
|
564
|
+
* import { Combobox, ComboboxContent, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxSeparator } from "@/components/ui/combobox";
|
|
565
|
+
*
|
|
566
|
+
* export function Sections() {
|
|
567
|
+
* return (
|
|
568
|
+
* <Combobox>
|
|
569
|
+
* <ComboboxInput aria-label="Tool" />
|
|
570
|
+
* <ComboboxContent>
|
|
571
|
+
* <ComboboxGroup>
|
|
572
|
+
* <ComboboxItem value="next">Next.js</ComboboxItem>
|
|
573
|
+
* </ComboboxGroup>
|
|
574
|
+
* <ComboboxSeparator />
|
|
575
|
+
* <ComboboxGroup>
|
|
576
|
+
* <ComboboxItem value="vite">Vite</ComboboxItem>
|
|
577
|
+
* </ComboboxGroup>
|
|
578
|
+
* </ComboboxContent>
|
|
579
|
+
* </Combobox>
|
|
580
|
+
* );
|
|
581
|
+
* }
|
|
582
|
+
* @see {@link ComboboxGroup}
|
|
583
|
+
* @since 1.0.0
|
|
584
|
+
*/
|
|
585
|
+
function ComboboxSeparator<T extends PolymorphicTarget = "div">(
|
|
586
|
+
props: ComboboxSeparatorProps<T>,
|
|
587
|
+
): JSX.Element {
|
|
588
|
+
const [local, rest] = splitProps(props as unknown as Loose, ["class"]);
|
|
589
|
+
return (
|
|
590
|
+
<ComboboxPrimitive.Separator
|
|
591
|
+
data-slot="combobox-separator"
|
|
592
|
+
class={cn("pointer-events-none -mx-1 my-1 h-px bg-border", local.class)}
|
|
593
|
+
{...rest}
|
|
594
|
+
/>
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* The field of a `multiple` combobox: the chips and the input wrap inside one bordered box that the
|
|
600
|
+
* list is placed against. Put {@link ComboboxValue} (the chips) and {@link ComboboxChipsInput} inside
|
|
601
|
+
* it.
|
|
602
|
+
*
|
|
603
|
+
* @param props The attributes of the rendered `div`. `class` is merged with the field classes.
|
|
604
|
+
* @returns The field.
|
|
605
|
+
* @throws Error when rendered outside a {@link Combobox}.
|
|
606
|
+
* @example
|
|
607
|
+
* import { Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput, ComboboxContent, ComboboxItem, ComboboxValue } from "@/components/ui/combobox";
|
|
608
|
+
*
|
|
609
|
+
* export function Tags() {
|
|
610
|
+
* return (
|
|
611
|
+
* <Combobox multiple defaultValue={["next"]}>
|
|
612
|
+
* <ComboboxChips>
|
|
613
|
+
* <ComboboxValue>
|
|
614
|
+
* {(chip) => <ComboboxChip value={chip.value}>{chip.label()}</ComboboxChip>}
|
|
615
|
+
* </ComboboxValue>
|
|
616
|
+
* <ComboboxChipsInput aria-label="Frameworks" placeholder="Add a framework" />
|
|
617
|
+
* </ComboboxChips>
|
|
618
|
+
* <ComboboxContent>
|
|
619
|
+
* <ComboboxItem value="next">Next.js</ComboboxItem>
|
|
620
|
+
* <ComboboxItem value="astro">Astro</ComboboxItem>
|
|
621
|
+
* </ComboboxContent>
|
|
622
|
+
* </Combobox>
|
|
623
|
+
* );
|
|
624
|
+
* }
|
|
625
|
+
* @see {@link ComboboxValue}
|
|
626
|
+
* @since 1.0.0
|
|
627
|
+
*/
|
|
628
|
+
function ComboboxChips(props: ComponentProps<"div">): JSX.Element {
|
|
629
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
630
|
+
return (
|
|
631
|
+
<ComboboxPrimitive.Anchor
|
|
632
|
+
data-slot="combobox-chips"
|
|
633
|
+
class={cn(FIELD, "min-h-9 flex-wrap items-center gap-1.5 px-2.5 py-1.5 text-sm", local.class)}
|
|
634
|
+
{...rest}
|
|
635
|
+
/>
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Renders one {@link ComboboxChip} per selected value, in selection order. Its child is a function
|
|
641
|
+
* that receives the chip's `value` and `label()` and returns the chip. The wrapper has
|
|
642
|
+
* `display: contents`, so the chips flow inside {@link ComboboxChips}.
|
|
643
|
+
*
|
|
644
|
+
* @param props The attributes of the wrapper, and `children`: a function from the chip data to a
|
|
645
|
+
* {@link ComboboxChip}.
|
|
646
|
+
* @returns The wrapper with one chip per selected value.
|
|
647
|
+
* @throws Error when rendered outside a {@link Combobox}.
|
|
648
|
+
* @example
|
|
649
|
+
* import { Combobox, ComboboxChip, ComboboxChips, ComboboxValue } from "@/components/ui/combobox";
|
|
650
|
+
*
|
|
651
|
+
* export function Selected() {
|
|
652
|
+
* return (
|
|
653
|
+
* <Combobox multiple defaultValue={["next"]}>
|
|
654
|
+
* <ComboboxChips>
|
|
655
|
+
* <ComboboxValue>
|
|
656
|
+
* {(chip) => <ComboboxChip value={chip.value}>{chip.label()}</ComboboxChip>}
|
|
657
|
+
* </ComboboxValue>
|
|
658
|
+
* </ComboboxChips>
|
|
659
|
+
* </Combobox>
|
|
660
|
+
* );
|
|
661
|
+
* }
|
|
662
|
+
* @see {@link ComboboxChips}
|
|
663
|
+
* @since 1.0.0
|
|
664
|
+
*/
|
|
665
|
+
function ComboboxValue<T extends PolymorphicTarget = "div">(
|
|
666
|
+
props: ComboboxValueProps<T>,
|
|
667
|
+
): JSX.Element {
|
|
668
|
+
const [local, rest] = splitProps(props as unknown as Loose, ["class"]);
|
|
669
|
+
return (
|
|
670
|
+
<ComboboxPrimitive.Chips
|
|
671
|
+
data-slot="combobox-value"
|
|
672
|
+
class={cn("contents", local.class)}
|
|
673
|
+
{...(rest as unknown as ComboboxValueProps)}
|
|
674
|
+
/>
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Own props of {@link ComboboxChip}: the extra parts the chip adds.
|
|
680
|
+
* @see {@link ComboboxChip}
|
|
681
|
+
* @since 1.0.0
|
|
682
|
+
*/
|
|
683
|
+
export interface ComboboxChipExtraProps {
|
|
684
|
+
/** Shows the button that removes the chip. @default true */
|
|
685
|
+
readonly showRemove?: boolean | undefined;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Props of {@link ComboboxChip}: `value` (the selected value it stands for), `showRemove` and the
|
|
690
|
+
* attributes of the rendered `div`.
|
|
691
|
+
* @see {@link ComboboxChip}
|
|
692
|
+
* @since 1.0.0
|
|
693
|
+
*/
|
|
694
|
+
export type ComboboxChipProps<T extends PolymorphicTarget = "div"> = ComboboxChipPrimitiveProps<T> &
|
|
695
|
+
ComboboxChipExtraProps;
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* A selected value of a `multiple` combobox, shown with a remove button. From the input, ArrowLeft
|
|
699
|
+
* with the caret at the start moves focus to the last chip. Inside the chips, ArrowLeft and
|
|
700
|
+
* ArrowRight move between chips, and Backspace or Delete removes the focused chip.
|
|
701
|
+
*
|
|
702
|
+
* @param props The chip props: see {@link ComboboxChipProps}.
|
|
703
|
+
* @returns The chip.
|
|
704
|
+
* @throws Error when rendered outside a {@link Combobox}.
|
|
705
|
+
* @example
|
|
706
|
+
* import { Combobox, ComboboxChip, ComboboxChips, ComboboxValue } from "@/components/ui/combobox";
|
|
707
|
+
*
|
|
708
|
+
* export function Tag() {
|
|
709
|
+
* return (
|
|
710
|
+
* <Combobox multiple defaultValue={["next"]}>
|
|
711
|
+
* <ComboboxChips>
|
|
712
|
+
* <ComboboxValue>
|
|
713
|
+
* {(chip) => (
|
|
714
|
+
* <ComboboxChip value={chip.value} showRemove={false}>
|
|
715
|
+
* {chip.label()}
|
|
716
|
+
* </ComboboxChip>
|
|
717
|
+
* )}
|
|
718
|
+
* </ComboboxValue>
|
|
719
|
+
* </ComboboxChips>
|
|
720
|
+
* </Combobox>
|
|
721
|
+
* );
|
|
722
|
+
* }
|
|
723
|
+
* @see {@link ComboboxValue}
|
|
724
|
+
* @since 1.0.0
|
|
725
|
+
*/
|
|
726
|
+
function ComboboxChip<T extends PolymorphicTarget = "div">(
|
|
727
|
+
props: ComboboxChipProps<T>,
|
|
728
|
+
): JSX.Element {
|
|
729
|
+
const [local, rest] = splitProps(
|
|
730
|
+
props as unknown as Loose & ComboboxChipExtraProps & { readonly value: string },
|
|
731
|
+
["class", "children", "showRemove", "value"],
|
|
732
|
+
);
|
|
733
|
+
return (
|
|
734
|
+
<ComboboxPrimitive.Chip
|
|
735
|
+
data-slot="combobox-chip"
|
|
736
|
+
value={local.value}
|
|
737
|
+
class={cn(
|
|
738
|
+
"flex h-[calc(--spacing(5.5))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 forced-colors:border forced-colors:border-[ButtonText] focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-transparent",
|
|
739
|
+
local.class,
|
|
740
|
+
)}
|
|
741
|
+
{...rest}
|
|
742
|
+
>
|
|
743
|
+
{local.children}
|
|
744
|
+
{local.showRemove !== false && <ComboboxChipRemove />}
|
|
745
|
+
</ComboboxPrimitive.Chip>
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* The button that removes its chip. It keeps focus in the input and is not a tab stop; the chip
|
|
751
|
+
* itself is removable with the keyboard. Its default `aria-label` is `Remove` followed by the chip's
|
|
752
|
+
* label; pass your own to translate it. `children` replaces the X icon.
|
|
753
|
+
*
|
|
754
|
+
* @param props The button props: the attributes of the rendered element, and `children`.
|
|
755
|
+
* @returns The remove button.
|
|
756
|
+
* @throws Error when rendered outside a {@link ComboboxChip}.
|
|
757
|
+
* @example
|
|
758
|
+
* import { Combobox, ComboboxChip, ComboboxChipRemove, ComboboxChips, ComboboxValue } from "@/components/ui/combobox";
|
|
759
|
+
*
|
|
760
|
+
* export function RemovableTag() {
|
|
761
|
+
* return (
|
|
762
|
+
* <Combobox multiple defaultValue={["next"]}>
|
|
763
|
+
* <ComboboxChips>
|
|
764
|
+
* <ComboboxValue>
|
|
765
|
+
* {(chip) => (
|
|
766
|
+
* <ComboboxChip value={chip.value} showRemove={false}>
|
|
767
|
+
* {chip.label()}
|
|
768
|
+
* <ComboboxChipRemove aria-label={`Remove ${chip.label()}`} />
|
|
769
|
+
* </ComboboxChip>
|
|
770
|
+
* )}
|
|
771
|
+
* </ComboboxValue>
|
|
772
|
+
* </ComboboxChips>
|
|
773
|
+
* </Combobox>
|
|
774
|
+
* );
|
|
775
|
+
* }
|
|
776
|
+
* @see {@link ComboboxChip}
|
|
777
|
+
* @since 1.0.0
|
|
778
|
+
*/
|
|
779
|
+
function ComboboxChipRemove<T extends PolymorphicTarget = "button">(
|
|
780
|
+
props: ComboboxChipRemoveProps<T>,
|
|
781
|
+
): JSX.Element {
|
|
782
|
+
const [local, rest] = splitProps(props as unknown as Loose, ["class", "children"]);
|
|
783
|
+
return (
|
|
784
|
+
<ComboboxPrimitive.ChipRemove
|
|
785
|
+
data-slot="combobox-chip-remove"
|
|
786
|
+
class={cn(
|
|
787
|
+
"-ms-1 inline-flex size-4 items-center justify-center rounded-sm opacity-50 hover:opacity-100 disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-3.5",
|
|
788
|
+
local.class,
|
|
789
|
+
)}
|
|
790
|
+
{...rest}
|
|
791
|
+
>
|
|
792
|
+
{local.children ?? <XIcon />}
|
|
793
|
+
</ComboboxPrimitive.ChipRemove>
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* The text input inside {@link ComboboxChips}. It has no box of its own and fills the room the chips
|
|
799
|
+
* leave. Name it with an `aria-label` or a {@link ComboboxLabel}. Its keys are the ones listed on
|
|
800
|
+
* {@link Combobox}, including Backspace in an empty input, which removes the last chip.
|
|
801
|
+
*
|
|
802
|
+
* @param props The input props: the attributes of the rendered `input`. `class` is merged with the
|
|
803
|
+
* input's classes.
|
|
804
|
+
* @returns The input.
|
|
805
|
+
* @throws Error when rendered outside a {@link Combobox}.
|
|
806
|
+
* @example
|
|
807
|
+
* import { Combobox, ComboboxChips, ComboboxChipsInput, ComboboxContent, ComboboxItem } from "@/components/ui/combobox";
|
|
808
|
+
*
|
|
809
|
+
* export function Adder() {
|
|
810
|
+
* return (
|
|
811
|
+
* <Combobox multiple>
|
|
812
|
+
* <ComboboxChips>
|
|
813
|
+
* <ComboboxChipsInput aria-label="Frameworks" placeholder="Add a framework" />
|
|
814
|
+
* </ComboboxChips>
|
|
815
|
+
* <ComboboxContent>
|
|
816
|
+
* <ComboboxItem value="next">Next.js</ComboboxItem>
|
|
817
|
+
* </ComboboxContent>
|
|
818
|
+
* </Combobox>
|
|
819
|
+
* );
|
|
820
|
+
* }
|
|
821
|
+
* @see {@link ComboboxChips}
|
|
822
|
+
* @since 1.0.0
|
|
823
|
+
*/
|
|
824
|
+
function ComboboxChipsInput(props: ComboboxInputPrimitiveProps): JSX.Element {
|
|
825
|
+
const [local, rest] = splitProps(props as unknown as Loose, ["class"]);
|
|
826
|
+
return (
|
|
827
|
+
<ComboboxPrimitive.Input
|
|
828
|
+
data-slot="combobox-chips-input"
|
|
829
|
+
class={cn(
|
|
830
|
+
"min-w-16 flex-1 bg-transparent outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed",
|
|
831
|
+
local.class,
|
|
832
|
+
)}
|
|
833
|
+
{...rest}
|
|
834
|
+
/>
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* True for a render function `(item, index) => node`. A child that is a node, or the zero-argument
|
|
840
|
+
* accessor the runtime makes of a lazy child, is not one.
|
|
841
|
+
*/
|
|
842
|
+
function isRenderFunction(
|
|
843
|
+
children: unknown,
|
|
844
|
+
): children is (item: unknown, index: number) => JSX.Element {
|
|
845
|
+
return typeof children === "function" && children.length > 0;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Props of {@link ComboboxList}: the attributes of the `div`, with children that are either nodes or
|
|
850
|
+
* a render function `(item, index) => node` over the `items` of the {@link Combobox}.
|
|
851
|
+
* @since 1.0.0
|
|
852
|
+
*/
|
|
853
|
+
export type ComboboxListProps<T = unknown> = Omit<ComponentProps<"div">, "children"> & {
|
|
854
|
+
readonly children?: JSX.Element | ((item: T, index: number) => JSX.Element) | undefined;
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* The scrolling list inside a {@link ComboboxContent}, as in shadcn/ui. With nodes as children it is
|
|
859
|
+
* a layout wrapper. With a function as children it renders that function once per item of the
|
|
860
|
+
* `items` prop of the {@link Combobox} (the index is the position when the row is created). The
|
|
861
|
+
* options still filter themselves from the typed text, so the function returns a
|
|
862
|
+
* {@link ComboboxItem} (or a group) for every item. The listbox role stays on the content.
|
|
863
|
+
*
|
|
864
|
+
* @param props The `div` attributes and the children, nodes or a render function.
|
|
865
|
+
* @returns The list `div`.
|
|
866
|
+
* @throws Error when rendered outside a {@link Combobox}.
|
|
867
|
+
* @example
|
|
868
|
+
* import { Combobox, ComboboxContent, ComboboxInput, ComboboxItem, ComboboxList } from "@/components/ui/combobox";
|
|
869
|
+
*
|
|
870
|
+
* const frameworks = ["Next.js", "Astro", "Remix"];
|
|
871
|
+
*
|
|
872
|
+
* export function FrameworkPicker() {
|
|
873
|
+
* return (
|
|
874
|
+
* <Combobox items={frameworks}>
|
|
875
|
+
* <ComboboxInput aria-label="Framework" />
|
|
876
|
+
* <ComboboxContent>
|
|
877
|
+
* <ComboboxList>
|
|
878
|
+
* {(name: string) => <ComboboxItem value={name}>{name}</ComboboxItem>}
|
|
879
|
+
* </ComboboxList>
|
|
880
|
+
* </ComboboxContent>
|
|
881
|
+
* </Combobox>
|
|
882
|
+
* );
|
|
883
|
+
* }
|
|
884
|
+
* @see {@link ComboboxCollection}
|
|
885
|
+
* @since 1.0.0
|
|
886
|
+
*/
|
|
887
|
+
function ComboboxList<T = unknown>(props: ComboboxListProps<T>): JSX.Element {
|
|
888
|
+
const [local, rest] = splitProps(props as ComboboxListProps<T> & { readonly class?: unknown }, [
|
|
889
|
+
"class",
|
|
890
|
+
"children",
|
|
891
|
+
]);
|
|
892
|
+
const items = useContext(itemsContext);
|
|
893
|
+
return (
|
|
894
|
+
<div
|
|
895
|
+
data-slot="combobox-list"
|
|
896
|
+
class={cn("max-h-[inherit] overflow-y-auto overscroll-contain", local.class as ClassValue)}
|
|
897
|
+
{...rest}
|
|
898
|
+
>
|
|
899
|
+
{isRenderFunction(local.children) ? (
|
|
900
|
+
<For each={items() ?? []}>
|
|
901
|
+
{(item, index) =>
|
|
902
|
+
(local.children as (item: T, index: number) => JSX.Element)(item as T, index())
|
|
903
|
+
}
|
|
904
|
+
</For>
|
|
905
|
+
) : (
|
|
906
|
+
(local.children as JSX.Element)
|
|
907
|
+
)}
|
|
908
|
+
</div>
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Props of {@link ComboboxCollection}.
|
|
914
|
+
* @since 1.0.0
|
|
915
|
+
*/
|
|
916
|
+
export interface ComboboxCollectionProps<T = unknown> {
|
|
917
|
+
/**
|
|
918
|
+
* The items to render. Without it the items of the surrounding {@link ComboboxGroup} are used,
|
|
919
|
+
* and without a group those of the {@link Combobox}.
|
|
920
|
+
* @default the group's items, else the root's
|
|
921
|
+
*/
|
|
922
|
+
readonly items?: readonly T[] | undefined;
|
|
923
|
+
/** Renders one item. */
|
|
924
|
+
readonly children: (item: T, index: number) => JSX.Element;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Renders a render function once per item, inside a {@link ComboboxGroup} that was given `items`
|
|
929
|
+
* (or with its own `items`). It adds no element of its own, so the group keeps hiding itself while
|
|
930
|
+
* the filter rejects all of its options.
|
|
931
|
+
*
|
|
932
|
+
* @param props The items (optional) and the render function.
|
|
933
|
+
* @returns One node per item.
|
|
934
|
+
* @throws Error when rendered outside a {@link Combobox}.
|
|
935
|
+
* @example
|
|
936
|
+
* import { ComboboxCollection, ComboboxGroup, ComboboxItem, ComboboxLabel } from "@/components/ui/combobox";
|
|
937
|
+
*
|
|
938
|
+
* export function Fruits() {
|
|
939
|
+
* return (
|
|
940
|
+
* <ComboboxGroup items={["Apple", "Banana"]}>
|
|
941
|
+
* <ComboboxLabel>Fruits</ComboboxLabel>
|
|
942
|
+
* <ComboboxCollection>
|
|
943
|
+
* {(name: string) => <ComboboxItem value={name}>{name}</ComboboxItem>}
|
|
944
|
+
* </ComboboxCollection>
|
|
945
|
+
* </ComboboxGroup>
|
|
946
|
+
* );
|
|
947
|
+
* }
|
|
948
|
+
* @see {@link ComboboxList}
|
|
949
|
+
* @since 1.0.0
|
|
950
|
+
*/
|
|
951
|
+
function ComboboxCollection<T = unknown>(props: ComboboxCollectionProps<T>): JSX.Element {
|
|
952
|
+
const group = useContext(groupItemsContext);
|
|
953
|
+
const root = useContext(itemsContext);
|
|
954
|
+
const items = (): readonly unknown[] => props.items ?? group() ?? root() ?? [];
|
|
955
|
+
return <For each={items()}>{(item, index) => props.children(item as T, index())}</For>;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
export {
|
|
959
|
+
Combobox,
|
|
960
|
+
ComboboxChip,
|
|
961
|
+
ComboboxChipRemove,
|
|
962
|
+
ComboboxChips,
|
|
963
|
+
ComboboxChipsInput,
|
|
964
|
+
ComboboxClear,
|
|
965
|
+
ComboboxCollection,
|
|
966
|
+
ComboboxContent,
|
|
967
|
+
ComboboxEmpty,
|
|
968
|
+
ComboboxGroup,
|
|
969
|
+
ComboboxInput,
|
|
970
|
+
ComboboxItem,
|
|
971
|
+
ComboboxLabel,
|
|
972
|
+
ComboboxList,
|
|
973
|
+
ComboboxLoading,
|
|
974
|
+
ComboboxSeparator,
|
|
975
|
+
ComboboxTrigger,
|
|
976
|
+
ComboboxValue,
|
|
977
|
+
};
|