@yarcl/react 0.1.0 → 0.2.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/README.md +14 -2
- package/{src/apply.ts → dist/apply.d.ts} +4 -23
- package/dist/apply.js +37 -0
- package/dist/classes.d.ts +13 -0
- package/dist/classes.js +22 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +58 -0
- package/dist/color.js +49 -32
- package/dist/components/Accordion.d.ts +96 -0
- package/dist/components/Accordion.js +148 -0
- package/dist/components/Alert.d.ts +60 -0
- package/dist/components/Alert.js +61 -0
- package/dist/components/Avatar.d.ts +100 -0
- package/dist/components/Avatar.js +89 -0
- package/dist/components/Badge.d.ts +44 -0
- package/dist/components/Badge.js +43 -0
- package/dist/components/Breadcrumb.d.ts +77 -0
- package/dist/components/Breadcrumb.js +126 -0
- package/dist/components/Button.d.ts +18 -0
- package/dist/components/Button.js +33 -0
- package/dist/components/ButtonGroup.d.ts +29 -0
- package/dist/components/ButtonGroup.js +37 -0
- package/dist/components/Card.d.ts +36 -0
- package/dist/components/Card.js +26 -0
- package/dist/components/Checkbox.d.ts +28 -0
- package/dist/components/Checkbox.js +38 -0
- package/dist/components/Combobox.d.ts +75 -0
- package/dist/components/Combobox.js +362 -0
- package/dist/components/CommandPalette.d.ts +92 -0
- package/dist/components/CommandPalette.js +293 -0
- package/dist/components/DatePicker.d.ts +157 -0
- package/dist/components/DatePicker.js +230 -0
- package/dist/components/Dialog.d.ts +25 -0
- package/dist/components/Dialog.js +29 -0
- package/dist/components/Divider.d.ts +23 -0
- package/dist/components/Divider.js +25 -0
- package/dist/components/Drawer.d.ts +21 -0
- package/dist/components/Drawer.js +25 -0
- package/dist/components/EmptyState.d.ts +51 -0
- package/dist/components/EmptyState.js +47 -0
- package/dist/components/Field.d.ts +27 -0
- package/dist/components/Field.js +59 -0
- package/dist/components/Heading.d.ts +26 -0
- package/dist/components/Heading.js +25 -0
- package/dist/components/HoverCard.d.ts +47 -0
- package/dist/components/HoverCard.js +53 -0
- package/dist/components/IconButton.d.ts +19 -0
- package/dist/components/IconButton.js +32 -0
- package/dist/components/Inline.d.ts +21 -0
- package/dist/components/Inline.js +24 -0
- package/dist/components/Input.d.ts +19 -0
- package/dist/components/Input.js +25 -0
- package/dist/components/Label.d.ts +46 -0
- package/dist/components/Label.js +33 -0
- package/dist/components/Link.d.ts +27 -0
- package/dist/components/Link.js +26 -0
- package/dist/components/Menu.d.ts +73 -0
- package/dist/components/Menu.js +143 -0
- package/dist/components/Modal.d.ts +45 -0
- package/dist/components/Modal.js +110 -0
- package/dist/components/NumberInput.d.ts +49 -0
- package/dist/components/NumberInput.js +130 -0
- package/dist/components/Pagination.d.ts +86 -0
- package/dist/components/Pagination.js +119 -0
- package/dist/components/Popover.d.ts +72 -0
- package/dist/components/Popover.js +87 -0
- package/dist/components/Progress.d.ts +60 -0
- package/dist/components/Progress.js +59 -0
- package/dist/components/Radio.d.ts +32 -0
- package/dist/components/Radio.js +47 -0
- package/dist/components/RadioGroup.d.ts +57 -0
- package/dist/components/RadioGroup.js +68 -0
- package/dist/components/Select.d.ts +47 -0
- package/dist/components/Select.js +138 -0
- package/dist/components/Skeleton.d.ts +46 -0
- package/dist/components/Skeleton.js +40 -0
- package/dist/components/Slider.d.ts +126 -0
- package/dist/components/Slider.js +166 -0
- package/dist/components/Spinner.d.ts +23 -0
- package/dist/components/Spinner.js +23 -0
- package/dist/components/Stack.d.ts +35 -0
- package/dist/components/Stack.js +24 -0
- package/dist/components/Switch.d.ts +27 -0
- package/dist/components/Switch.js +33 -0
- package/dist/components/Table.d.ts +71 -0
- package/dist/components/Table.js +61 -0
- package/dist/components/Tabs.d.ts +66 -0
- package/dist/components/Tabs.js +109 -0
- package/dist/components/Text.d.ts +32 -0
- package/dist/components/Text.js +28 -0
- package/dist/components/Textarea.d.ts +15 -0
- package/dist/components/Textarea.js +24 -0
- package/dist/components/Toast.d.ts +90 -0
- package/dist/components/Toast.js +156 -0
- package/dist/components/ToggleGroup.d.ts +79 -0
- package/dist/components/ToggleGroup.js +101 -0
- package/dist/components/Tooltip.d.ts +46 -0
- package/dist/components/Tooltip.js +62 -0
- package/dist/components/VisuallyHidden.d.ts +18 -0
- package/dist/components/VisuallyHidden.js +22 -0
- package/dist/contrast.d.ts +32 -0
- package/dist/css.d.ts +2 -2
- package/dist/css.js +193 -157
- package/dist/define.d.ts +40 -15
- package/dist/define.js +30 -27
- package/dist/demo.d.ts +2 -0
- package/dist/demo.js +2 -0
- package/dist/esbuild.cjs +5 -0
- package/dist/esbuild.d.ts +4 -0
- package/dist/esbuild.js +6 -0
- package/dist/field-context.d.ts +16 -0
- package/dist/field-context.js +17 -0
- package/dist/floating.d.ts +12 -0
- package/dist/floating.js +52 -0
- package/dist/hooks.d.ts +1 -0
- package/dist/hooks.js +12 -0
- package/{src/index.ts → dist/index.d.ts} +11 -33
- package/dist/index.js +50 -0
- package/dist/init.d.ts +18 -0
- package/dist/init.js +224 -0
- package/dist/plugin.d.ts +10 -26
- package/dist/plugin.js +152 -78
- package/dist/reference/DemoYarcl.d.ts +12 -0
- package/dist/reference/DemoYarcl.js +312 -0
- package/dist/reference/DesignReference.d.ts +24 -0
- package/dist/reference/DesignReference.js +403 -0
- package/dist/reference/index.js +2 -0
- package/dist/rollup.cjs +5 -0
- package/dist/rollup.d.ts +4 -0
- package/dist/rollup.js +6 -0
- package/dist/rspack.cjs +5 -0
- package/dist/rspack.d.ts +4 -0
- package/dist/rspack.js +6 -0
- package/dist/runtime.d.ts +13 -0
- package/dist/runtime.js +30 -0
- package/dist/setup.d.ts +6 -0
- package/dist/setup.js +33 -0
- package/{src → dist}/styles.css +224 -106
- package/dist/themes/bloom.d.ts +295 -0
- package/dist/themes/bloom.js +202 -0
- package/dist/themes/brutalist.d.ts +277 -0
- package/dist/themes/brutalist.js +199 -0
- package/dist/themes/compact.d.ts +274 -0
- package/dist/themes/compact.js +200 -0
- package/dist/themes/contract.d.ts +23 -0
- package/dist/themes/editorial.d.ts +277 -0
- package/dist/themes/editorial.js +183 -0
- package/dist/themes/index.d.ts +1425 -0
- package/dist/themes/index.js +45 -0
- package/{src/types.ts → dist/types.d.ts} +40 -37
- package/dist/vite.cjs +5 -0
- package/dist/vite.d.ts +4 -0
- package/dist/vite.js +6 -0
- package/dist/webpack.cjs +5 -0
- package/dist/webpack.d.ts +4 -0
- package/dist/webpack.js +6 -0
- package/dist/yarcl.config.d.ts +276 -0
- package/dist/yarcl.config.js +273 -0
- package/package.json +69 -17
- package/src/classes.ts +0 -23
- package/src/color.ts +0 -48
- package/src/components/Accordion.tsx +0 -274
- package/src/components/Alert.tsx +0 -85
- package/src/components/Badge.tsx +0 -79
- package/src/components/Breadcrumb.tsx +0 -239
- package/src/components/Button.tsx +0 -58
- package/src/components/ButtonGroup.tsx +0 -61
- package/src/components/Card.tsx +0 -50
- package/src/components/Checkbox.tsx +0 -49
- package/src/components/Combobox.tsx +0 -522
- package/src/components/CommandPalette.tsx +0 -452
- package/src/components/DatePicker.tsx +0 -432
- package/src/components/Dialog.tsx +0 -39
- package/src/components/Divider.tsx +0 -34
- package/src/components/Drawer.tsx +0 -31
- package/src/components/Field.tsx +0 -61
- package/src/components/Heading.tsx +0 -40
- package/src/components/HoverCard.tsx +0 -108
- package/src/components/IconButton.tsx +0 -59
- package/src/components/Inline.tsx +0 -42
- package/src/components/Input.tsx +0 -28
- package/src/components/Label.tsx +0 -58
- package/src/components/Link.tsx +0 -41
- package/src/components/Menu.tsx +0 -207
- package/src/components/Modal.tsx +0 -164
- package/src/components/NumberInput.tsx +0 -208
- package/src/components/Pagination.tsx +0 -227
- package/src/components/Popover.tsx +0 -140
- package/src/components/Progress.tsx +0 -122
- package/src/components/Radio.tsx +0 -59
- package/src/components/RadioGroup.tsx +0 -114
- package/src/components/Select.tsx +0 -197
- package/src/components/Skeleton.tsx +0 -95
- package/src/components/Slider.tsx +0 -315
- package/src/components/Spinner.tsx +0 -38
- package/src/components/Stack.tsx +0 -57
- package/src/components/Switch.tsx +0 -42
- package/src/components/Table.tsx +0 -99
- package/src/components/Tabs.tsx +0 -159
- package/src/components/Text.tsx +0 -67
- package/src/components/Textarea.tsx +0 -26
- package/src/components/Toast.tsx +0 -190
- package/src/components/ToggleGroup.tsx +0 -227
- package/src/components/Tooltip.tsx +0 -78
- package/src/contrast.ts +0 -82
- package/src/css.ts +0 -228
- package/src/define.check.ts +0 -259
- package/src/define.ts +0 -379
- package/src/field-context.ts +0 -31
- package/src/floating.tsx +0 -61
- package/src/hooks.ts +0 -19
- package/src/plugin.ts +0 -94
- package/src/reference/DesignReference.tsx +0 -374
- package/src/runtime.ts +0 -38
- package/src/themes/bloom.ts +0 -72
- package/src/themes/brutalist.ts +0 -63
- package/src/themes/compact.ts +0 -63
- package/src/themes/contract.ts +0 -24
- package/src/themes/editorial.ts +0 -55
- package/src/themes/index.ts +0 -47
- package/src/yarcl.config.ts +0 -104
- /package/{src/reference/index.ts → dist/reference/index.d.ts} +0 -0
- /package/{src → dist}/reference/reference.css +0 -0
|
@@ -1,522 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
autoUpdate,
|
|
3
|
-
FloatingPortal,
|
|
4
|
-
useDismiss,
|
|
5
|
-
useFloating,
|
|
6
|
-
useInteractions,
|
|
7
|
-
useListNavigation,
|
|
8
|
-
useMergeRefs,
|
|
9
|
-
useRole,
|
|
10
|
-
} from '@floating-ui/react';
|
|
11
|
-
import {
|
|
12
|
-
useId,
|
|
13
|
-
useRef,
|
|
14
|
-
useState,
|
|
15
|
-
type ChangeEvent,
|
|
16
|
-
type ComponentProps,
|
|
17
|
-
type FocusEvent,
|
|
18
|
-
type KeyboardEvent,
|
|
19
|
-
type MouseEvent,
|
|
20
|
-
type ReactNode,
|
|
21
|
-
type RefObject,
|
|
22
|
-
} from 'react';
|
|
23
|
-
import { colorClass, cx, radiusClass, sizeClass } from '../classes';
|
|
24
|
-
import { useFieldProps } from '../field-context';
|
|
25
|
-
import { CheckIcon, floatingMiddleware } from '../floating';
|
|
26
|
-
import { useControllable } from '../hooks';
|
|
27
|
-
import type { Color, Size, TokenProps } from '../types';
|
|
28
|
-
import { Badge, BadgeRemove } from './Badge';
|
|
29
|
-
import type { SelectOption } from './Select';
|
|
30
|
-
import { useDefaults } from '../runtime';
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
/** Props shared by both forms of {@link ComboboxProps}. */
|
|
34
|
-
export interface ComboboxBaseProps<V extends string = string>
|
|
35
|
-
extends TokenProps,
|
|
36
|
-
Omit<ComponentProps<'input'>, 'color' | 'size' | 'value' | 'defaultValue' | 'onChange' | 'children' | 'multiple'> {
|
|
37
|
-
/** The options to suggest. For async search, update this as results arrive and set `filter={false}`. */
|
|
38
|
-
options: readonly SelectOption<V>[];
|
|
39
|
-
/** Controlled text in the input. */
|
|
40
|
-
inputValue?: string;
|
|
41
|
-
/** Called on every keystroke with the input text. Use it to fetch options for async search. */
|
|
42
|
-
onInputValueChange?: (text: string) => void;
|
|
43
|
-
/**
|
|
44
|
-
* How options are matched against the typed text.
|
|
45
|
-
* `true`: case-insensitive "contains" on the label; a function for custom matching;
|
|
46
|
-
* `false` to show `options` as given (e.g. already filtered by a server).
|
|
47
|
-
* @default true
|
|
48
|
-
*/
|
|
49
|
-
filter?: boolean | ((option: SelectOption<V>, text: string) => boolean);
|
|
50
|
-
/**
|
|
51
|
-
* Accepts typed text that doesn't match an option (typeahead / autocomplete).
|
|
52
|
-
* When `false`, unchosen text is discarded on blur: the input resets to the selected option's label,
|
|
53
|
-
* or clears with `multiple`. With `multiple`, Enter adds the typed text as a chip.
|
|
54
|
-
* @default false
|
|
55
|
-
*/
|
|
56
|
-
allowCustomValue?: boolean;
|
|
57
|
-
/** Shows a loading row instead of options. */
|
|
58
|
-
loading?: boolean;
|
|
59
|
-
/**
|
|
60
|
-
* Shown when no options match.
|
|
61
|
-
* @default 'No results'
|
|
62
|
-
*/
|
|
63
|
-
emptyMessage?: ReactNode;
|
|
64
|
-
/**
|
|
65
|
-
* Shown while `loading` is set.
|
|
66
|
-
* @default 'Loading…'
|
|
67
|
-
*/
|
|
68
|
-
loadingMessage?: ReactNode;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Props for {@link Combobox}. `multiple` switches the value from one option to an array of them. */
|
|
72
|
-
export type ComboboxProps<V extends string = string> = ComboboxBaseProps<V> &
|
|
73
|
-
(
|
|
74
|
-
| {
|
|
75
|
-
/** One value at a time. The input shows the selected option's label. */
|
|
76
|
-
multiple?: false;
|
|
77
|
-
/** Controlled selected value. `null` means nothing is selected. */
|
|
78
|
-
value?: V | null;
|
|
79
|
-
/** Initial selected value when uncontrolled. */
|
|
80
|
-
defaultValue?: V | null;
|
|
81
|
-
/** Called when an option is chosen, or with the typed text when `allowCustomValue` is set. */
|
|
82
|
-
onValueChange?: (value: V | null) => void;
|
|
83
|
-
maxSelected?: never;
|
|
84
|
-
}
|
|
85
|
-
| {
|
|
86
|
-
/** Any number of values, shown as removable chips before the input. */
|
|
87
|
-
multiple: true;
|
|
88
|
-
/** Controlled selected values. */
|
|
89
|
-
value?: readonly V[];
|
|
90
|
-
/** Initial selected values when uncontrolled. */
|
|
91
|
-
defaultValue?: readonly V[];
|
|
92
|
-
/** Called with all selected values. */
|
|
93
|
-
onValueChange?: (value: V[]) => void;
|
|
94
|
-
/** The most values that can be selected. Other options are disabled once it is reached. */
|
|
95
|
-
maxSelected?: number;
|
|
96
|
-
}
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
type SingleProps<V extends string> = Extract<ComboboxProps<V>, { multiple?: false }>;
|
|
100
|
-
type MultipleProps<V extends string> = Extract<ComboboxProps<V>, { multiple: true }>;
|
|
101
|
-
|
|
102
|
-
const contains = (option: SelectOption, text: string) => option.label.toLowerCase().includes(text.trim().toLowerCase());
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* A text input with a filtered list of suggestions.
|
|
106
|
-
* Covers searchable select (default), typeahead / autocomplete (`allowCustomValue`),
|
|
107
|
-
* async search (`filter={false}` + `onInputValueChange` + `loading`)
|
|
108
|
-
* and multi-select with removable chips (`multiple`).
|
|
109
|
-
* Shares the size scale with {@link Input}. Works inside a {@link Field}.
|
|
110
|
-
*
|
|
111
|
-
* @example
|
|
112
|
-
* ```tsx
|
|
113
|
-
* <Combobox placeholder="Country" options={countries} onValueChange={setCountry} />
|
|
114
|
-
* <Combobox multiple placeholder="Countries" options={countries} onValueChange={setCountries} />
|
|
115
|
-
* ```
|
|
116
|
-
*/
|
|
117
|
-
export function Combobox<V extends string = string>(props: ComboboxProps<V>) {
|
|
118
|
-
return props.multiple ? <MultipleCombobox {...props} /> : <SingleCombobox {...props} />;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
interface ListOptions<V extends string> {
|
|
122
|
-
options: readonly SelectOption<V>[];
|
|
123
|
-
text: string;
|
|
124
|
-
filter: ComboboxBaseProps<V>['filter'];
|
|
125
|
-
loading: boolean;
|
|
126
|
-
emptyMessage: ReactNode;
|
|
127
|
-
loadingMessage: ReactNode;
|
|
128
|
-
size: Size | undefined;
|
|
129
|
-
color: Color | undefined;
|
|
130
|
-
multiple: boolean;
|
|
131
|
-
isSelected: (option: SelectOption<V>) => boolean;
|
|
132
|
-
isDisabled: (option: SelectOption<V>) => boolean;
|
|
133
|
-
onChoose: (option: SelectOption<V>) => void;
|
|
134
|
-
onDismiss: () => void;
|
|
135
|
-
containerRef?: RefObject<HTMLElement | null>;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function useComboboxList<V extends string>(opts: ListOptions<V>) {
|
|
139
|
-
const { options, text, filter, loading, isSelected, isDisabled, onChoose, containerRef } = opts;
|
|
140
|
-
const [open, setOpen] = useState(false);
|
|
141
|
-
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
|
142
|
-
const listRef = useRef<(HTMLElement | null)[]>([]);
|
|
143
|
-
const idPrefix = useId();
|
|
144
|
-
|
|
145
|
-
const match = filter === true || filter === undefined ? contains : filter === false ? () => true : filter;
|
|
146
|
-
const visible = options.filter((option) => match(option, text));
|
|
147
|
-
|
|
148
|
-
function close() {
|
|
149
|
-
setOpen(false);
|
|
150
|
-
setActiveIndex(null);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const { refs, floatingStyles, context } = useFloating({
|
|
154
|
-
open,
|
|
155
|
-
onOpenChange(next) {
|
|
156
|
-
setOpen(next);
|
|
157
|
-
if (!next) opts.onDismiss();
|
|
158
|
-
},
|
|
159
|
-
placement: 'bottom-start',
|
|
160
|
-
transform: false,
|
|
161
|
-
whileElementsMounted: autoUpdate,
|
|
162
|
-
middleware: floatingMiddleware({ gap: 4, matchWidth: true }),
|
|
163
|
-
});
|
|
164
|
-
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
|
|
165
|
-
useRole(context, { role: 'combobox' }),
|
|
166
|
-
useDismiss(context, {
|
|
167
|
-
outsidePress: (event) => !containerRef?.current?.contains(event.target as Node),
|
|
168
|
-
}),
|
|
169
|
-
useListNavigation(context, {
|
|
170
|
-
listRef,
|
|
171
|
-
activeIndex,
|
|
172
|
-
onNavigate: setActiveIndex,
|
|
173
|
-
virtual: true,
|
|
174
|
-
loop: true,
|
|
175
|
-
disabledIndices: visible.flatMap((option, i) => (isDisabled(option) ? [i] : [])),
|
|
176
|
-
}),
|
|
177
|
-
]);
|
|
178
|
-
|
|
179
|
-
const showList = open && (loading || visible.length > 0 || text.trim() !== '');
|
|
180
|
-
|
|
181
|
-
const list = showList && (
|
|
182
|
-
<FloatingPortal>
|
|
183
|
-
<div
|
|
184
|
-
ref={refs.setFloating}
|
|
185
|
-
style={floatingStyles}
|
|
186
|
-
className={cx('yarcl-floating yarcl-panel yarcl-listbox', sizeClass(opts.size), colorClass(opts.color))}
|
|
187
|
-
{...getFloatingProps({
|
|
188
|
-
'aria-multiselectable': opts.multiple || undefined,
|
|
189
|
-
onMouseDown: (event: MouseEvent) => event.preventDefault(),
|
|
190
|
-
})}
|
|
191
|
-
>
|
|
192
|
-
{loading ? (
|
|
193
|
-
<div className="yarcl-listbox-message">{opts.loadingMessage}</div>
|
|
194
|
-
) : visible.length === 0 ? (
|
|
195
|
-
<div className="yarcl-listbox-message">{opts.emptyMessage}</div>
|
|
196
|
-
) : (
|
|
197
|
-
visible.map((option, i) => {
|
|
198
|
-
const isActive = i === activeIndex;
|
|
199
|
-
const selected = isSelected(option);
|
|
200
|
-
return (
|
|
201
|
-
<div
|
|
202
|
-
key={option.value}
|
|
203
|
-
id={`${idPrefix}-${i}`}
|
|
204
|
-
ref={(node) => {
|
|
205
|
-
listRef.current[i] = node;
|
|
206
|
-
}}
|
|
207
|
-
aria-disabled={isDisabled(option) || undefined}
|
|
208
|
-
data-active={isActive || undefined}
|
|
209
|
-
className="yarcl-option"
|
|
210
|
-
{...getItemProps({ active: isActive, selected, onClick: () => onChoose(option) })}
|
|
211
|
-
>
|
|
212
|
-
<span className="yarcl-option-label">{option.label}</span>
|
|
213
|
-
{selected && <CheckIcon />}
|
|
214
|
-
</div>
|
|
215
|
-
);
|
|
216
|
-
})
|
|
217
|
-
)}
|
|
218
|
-
</div>
|
|
219
|
-
</FloatingPortal>
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
return { open, setOpen, activeIndex, setActiveIndex, close, visible, match, refs, getReferenceProps, list };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function SingleCombobox<V extends string>(props: SingleProps<V>) {
|
|
226
|
-
const own = useDefaults('Combobox');
|
|
227
|
-
const {
|
|
228
|
-
options,
|
|
229
|
-
value: valueProp,
|
|
230
|
-
defaultValue = null,
|
|
231
|
-
onValueChange,
|
|
232
|
-
inputValue: inputValueProp,
|
|
233
|
-
onInputValueChange,
|
|
234
|
-
filter = true,
|
|
235
|
-
allowCustomValue = false,
|
|
236
|
-
loading = false,
|
|
237
|
-
emptyMessage = 'No results',
|
|
238
|
-
loadingMessage = 'Loading…',
|
|
239
|
-
size,
|
|
240
|
-
radius,
|
|
241
|
-
color,
|
|
242
|
-
className,
|
|
243
|
-
onBlur,
|
|
244
|
-
multiple: _multiple,
|
|
245
|
-
maxSelected: _maxSelected,
|
|
246
|
-
...rest
|
|
247
|
-
} = useFieldProps(props);
|
|
248
|
-
|
|
249
|
-
const [value, setValue] = useControllable<V | null>(valueProp, defaultValue, onValueChange);
|
|
250
|
-
const labelOf = (v: V | null) => options.find((option) => option.value === v)?.label ?? (allowCustomValue ? (v ?? '') : '');
|
|
251
|
-
const [text, setText] = useControllable(inputValueProp, labelOf(defaultValue), onInputValueChange);
|
|
252
|
-
|
|
253
|
-
function choose(option: SelectOption<V>) {
|
|
254
|
-
if (option.disabled) return;
|
|
255
|
-
setValue(option.value);
|
|
256
|
-
setText(option.label);
|
|
257
|
-
close();
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function restore() {
|
|
261
|
-
if (allowCustomValue) return;
|
|
262
|
-
const label = labelOf(value);
|
|
263
|
-
if (text !== label) setText(label);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const { open, setOpen, activeIndex, setActiveIndex, close, visible, refs, getReferenceProps, list } = useComboboxList({
|
|
267
|
-
options,
|
|
268
|
-
text,
|
|
269
|
-
filter,
|
|
270
|
-
loading,
|
|
271
|
-
emptyMessage,
|
|
272
|
-
loadingMessage,
|
|
273
|
-
size: size ?? own.size,
|
|
274
|
-
color: color ?? own.color,
|
|
275
|
-
multiple: false,
|
|
276
|
-
isSelected: (option) => option.value === value,
|
|
277
|
-
isDisabled: (option) => !!option.disabled,
|
|
278
|
-
onChoose: choose,
|
|
279
|
-
onDismiss: restore,
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
function onChange(event: ChangeEvent<HTMLInputElement>) {
|
|
283
|
-
const next = event.target.value;
|
|
284
|
-
setText(next);
|
|
285
|
-
setOpen(true);
|
|
286
|
-
setActiveIndex(next ? 0 : null);
|
|
287
|
-
if (allowCustomValue) setValue(next ? (next as V) : null);
|
|
288
|
-
else if (!next) setValue(null);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function onKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
|
292
|
-
if (event.key !== 'Enter' || !open) return;
|
|
293
|
-
event.preventDefault();
|
|
294
|
-
const option = activeIndex != null ? visible[activeIndex] : undefined;
|
|
295
|
-
if (option) choose(option);
|
|
296
|
-
else close();
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return (
|
|
300
|
-
<>
|
|
301
|
-
<input
|
|
302
|
-
ref={refs.setReference}
|
|
303
|
-
className={cx('yarcl-input yarcl-combobox', sizeClass(size ?? own.size), radiusClass(radius ?? own.radius, size ?? own.size), colorClass(color ?? own.color), className)}
|
|
304
|
-
autoComplete="off"
|
|
305
|
-
aria-autocomplete="list"
|
|
306
|
-
{...getReferenceProps({
|
|
307
|
-
...rest,
|
|
308
|
-
value: text,
|
|
309
|
-
onChange,
|
|
310
|
-
onKeyDown,
|
|
311
|
-
onFocus: () => setOpen(true),
|
|
312
|
-
onClick: () => setOpen(true),
|
|
313
|
-
onBlur(event) {
|
|
314
|
-
restore();
|
|
315
|
-
onBlur?.(event as FocusEvent<HTMLInputElement>);
|
|
316
|
-
},
|
|
317
|
-
})}
|
|
318
|
-
/>
|
|
319
|
-
{list}
|
|
320
|
-
</>
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function MultipleCombobox<V extends string>(props: MultipleProps<V>) {
|
|
325
|
-
const own = useDefaults('Combobox');
|
|
326
|
-
const {
|
|
327
|
-
options,
|
|
328
|
-
value: valueProp,
|
|
329
|
-
defaultValue = [],
|
|
330
|
-
onValueChange,
|
|
331
|
-
maxSelected,
|
|
332
|
-
inputValue: inputValueProp,
|
|
333
|
-
onInputValueChange,
|
|
334
|
-
filter = true,
|
|
335
|
-
allowCustomValue = false,
|
|
336
|
-
loading = false,
|
|
337
|
-
emptyMessage = 'No results',
|
|
338
|
-
loadingMessage = 'Loading…',
|
|
339
|
-
size,
|
|
340
|
-
radius,
|
|
341
|
-
color,
|
|
342
|
-
className,
|
|
343
|
-
style,
|
|
344
|
-
name,
|
|
345
|
-
required,
|
|
346
|
-
disabled,
|
|
347
|
-
placeholder,
|
|
348
|
-
onBlur,
|
|
349
|
-
multiple: _multiple,
|
|
350
|
-
...rest
|
|
351
|
-
} = useFieldProps(props);
|
|
352
|
-
|
|
353
|
-
const [values, setValues] = useControllable<readonly V[]>(valueProp, defaultValue, onValueChange as (value: readonly V[]) => void);
|
|
354
|
-
const [text, setText] = useControllable(inputValueProp, '', onInputValueChange);
|
|
355
|
-
const controlRef = useRef<HTMLDivElement | null>(null);
|
|
356
|
-
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
357
|
-
const chipsRef = useRef<HTMLUListElement>(null);
|
|
358
|
-
const knownLabels = useRef(new Map<V, string>());
|
|
359
|
-
const quietFocus = useRef(false);
|
|
360
|
-
|
|
361
|
-
const full = maxSelected != null && values.length >= maxSelected;
|
|
362
|
-
const labelOf = (v: V) => options.find((option) => option.value === v)?.label ?? knownLabels.current.get(v) ?? v;
|
|
363
|
-
|
|
364
|
-
function toggle(option: SelectOption<V>) {
|
|
365
|
-
if (option.disabled) return;
|
|
366
|
-
const selected = values.includes(option.value);
|
|
367
|
-
if (!selected && full) return;
|
|
368
|
-
knownLabels.current.set(option.value, option.label);
|
|
369
|
-
setValues(selected ? values.filter((v) => v !== option.value) : [...values, option.value]);
|
|
370
|
-
if (text) {
|
|
371
|
-
setText('');
|
|
372
|
-
const index = options.filter((o) => match(o, '')).indexOf(option);
|
|
373
|
-
setActiveIndex(index >= 0 ? index : null);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function addCustom() {
|
|
378
|
-
const typed = text.trim() as V;
|
|
379
|
-
if (!values.includes(typed) && !full) setValues([...values, typed]);
|
|
380
|
-
setText('');
|
|
381
|
-
setActiveIndex(null);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function focusInput() {
|
|
385
|
-
const input = inputRef.current;
|
|
386
|
-
if (!input) return;
|
|
387
|
-
if (document.activeElement !== input) quietFocus.current = true;
|
|
388
|
-
input.focus();
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function remove(v: V) {
|
|
392
|
-
setValues(values.filter((x) => x !== v));
|
|
393
|
-
focusInput();
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function restore() {
|
|
397
|
-
if (!allowCustomValue && text) setText('');
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const { open, setOpen, activeIndex, setActiveIndex, close, visible, match, refs, getReferenceProps, list } = useComboboxList({
|
|
401
|
-
options,
|
|
402
|
-
text,
|
|
403
|
-
filter,
|
|
404
|
-
loading,
|
|
405
|
-
emptyMessage,
|
|
406
|
-
loadingMessage,
|
|
407
|
-
size: size ?? own.size,
|
|
408
|
-
color: color ?? own.color,
|
|
409
|
-
multiple: true,
|
|
410
|
-
isSelected: (option) => values.includes(option.value),
|
|
411
|
-
isDisabled: (option) => !!option.disabled || (full && !values.includes(option.value)),
|
|
412
|
-
onChoose: toggle,
|
|
413
|
-
onDismiss: () => {},
|
|
414
|
-
containerRef: controlRef,
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
const controlRefs = useMergeRefs([controlRef, refs.setPositionReference]);
|
|
418
|
-
const inputRefs = useMergeRefs([inputRef, refs.setReference]);
|
|
419
|
-
const chipButtons = () => [...(chipsRef.current?.querySelectorAll<HTMLButtonElement>('.yarcl-badge-remove') ?? [])];
|
|
420
|
-
|
|
421
|
-
function onChange(event: ChangeEvent<HTMLInputElement>) {
|
|
422
|
-
const next = event.target.value;
|
|
423
|
-
setText(next);
|
|
424
|
-
setOpen(true);
|
|
425
|
-
setActiveIndex(next ? 0 : null);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function onKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
|
429
|
-
const input = event.currentTarget;
|
|
430
|
-
if (event.key === 'Enter') {
|
|
431
|
-
const option = open && activeIndex != null ? visible[activeIndex] : undefined;
|
|
432
|
-
if (option) toggle(option);
|
|
433
|
-
else if (allowCustomValue && text.trim()) addCustom();
|
|
434
|
-
else if (open) close();
|
|
435
|
-
else return;
|
|
436
|
-
event.preventDefault();
|
|
437
|
-
} else if (event.key === 'Backspace' && text === '' && values.length > 0) {
|
|
438
|
-
event.preventDefault();
|
|
439
|
-
setValues(values.slice(0, -1));
|
|
440
|
-
} else if (event.key === 'ArrowLeft' && input.selectionStart === 0 && input.selectionEnd === 0 && values.length > 0) {
|
|
441
|
-
event.preventDefault();
|
|
442
|
-
close();
|
|
443
|
-
chipButtons().at(-1)?.focus();
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function onChipKeyDown(event: KeyboardEvent<HTMLUListElement>) {
|
|
448
|
-
const buttons = chipButtons();
|
|
449
|
-
const i = buttons.indexOf(event.target as HTMLButtonElement);
|
|
450
|
-
if (i < 0) return;
|
|
451
|
-
if (event.key === 'ArrowLeft') buttons[Math.max(i - 1, 0)].focus();
|
|
452
|
-
else if (event.key === 'ArrowRight') {
|
|
453
|
-
if (i === buttons.length - 1) focusInput();
|
|
454
|
-
else buttons[i + 1].focus();
|
|
455
|
-
} else if (event.key === 'Backspace' || event.key === 'Delete') remove(values[i]);
|
|
456
|
-
else if (event.key === 'Escape') focusInput();
|
|
457
|
-
else return;
|
|
458
|
-
event.preventDefault();
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function onControlMouseDown(event: MouseEvent<HTMLDivElement>) {
|
|
462
|
-
if (disabled || (event.target as Element).closest('button, input')) return;
|
|
463
|
-
event.preventDefault();
|
|
464
|
-
inputRef.current?.focus();
|
|
465
|
-
setOpen(true);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const s = size ?? own.size;
|
|
469
|
-
|
|
470
|
-
return (
|
|
471
|
-
<>
|
|
472
|
-
<div
|
|
473
|
-
ref={controlRefs}
|
|
474
|
-
className={cx('yarcl-input yarcl-combobox-control', sizeClass(s), radiusClass(radius ?? own.radius, s), colorClass(color ?? own.color), className)}
|
|
475
|
-
style={style}
|
|
476
|
-
onMouseDown={onControlMouseDown}
|
|
477
|
-
>
|
|
478
|
-
{values.length > 0 && (
|
|
479
|
-
<ul ref={chipsRef} role="list" aria-label="Selected" className="yarcl-combobox-chips" onKeyDown={onChipKeyDown}>
|
|
480
|
-
{values.map((v) => {
|
|
481
|
-
const label = labelOf(v);
|
|
482
|
-
return (
|
|
483
|
-
<li key={v}>
|
|
484
|
-
<Badge size={s} color={color ?? own.color}>
|
|
485
|
-
<span className="yarcl-combobox-chip-label">{label}</span>
|
|
486
|
-
<BadgeRemove aria-label={`Remove ${label}`} tabIndex={-1} disabled={disabled} onClick={() => remove(v)} />
|
|
487
|
-
</Badge>
|
|
488
|
-
</li>
|
|
489
|
-
);
|
|
490
|
-
})}
|
|
491
|
-
</ul>
|
|
492
|
-
)}
|
|
493
|
-
<input
|
|
494
|
-
ref={inputRefs}
|
|
495
|
-
className="yarcl-combobox-input"
|
|
496
|
-
autoComplete="off"
|
|
497
|
-
aria-autocomplete="list"
|
|
498
|
-
{...getReferenceProps({
|
|
499
|
-
...rest,
|
|
500
|
-
disabled,
|
|
501
|
-
required: required && values.length === 0,
|
|
502
|
-
placeholder: values.length > 0 ? undefined : placeholder,
|
|
503
|
-
value: text,
|
|
504
|
-
onChange,
|
|
505
|
-
onKeyDown,
|
|
506
|
-
onFocus() {
|
|
507
|
-
if (quietFocus.current) quietFocus.current = false;
|
|
508
|
-
else setOpen(true);
|
|
509
|
-
},
|
|
510
|
-
onClick: () => setOpen(true),
|
|
511
|
-
onBlur(event) {
|
|
512
|
-
if (!controlRef.current?.contains(event.relatedTarget as Node)) restore();
|
|
513
|
-
onBlur?.(event as FocusEvent<HTMLInputElement>);
|
|
514
|
-
},
|
|
515
|
-
})}
|
|
516
|
-
/>
|
|
517
|
-
{name && values.map((v) => <input key={v} type="hidden" name={name} value={v} />)}
|
|
518
|
-
</div>
|
|
519
|
-
{list}
|
|
520
|
-
</>
|
|
521
|
-
);
|
|
522
|
-
}
|