@rentnerkev/select 2.0.0 → 3.1.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 CHANGED
@@ -114,7 +114,7 @@ export function RegionSelect() {
114
114
 
115
115
  When `name` is set, each selected array item is submitted under the same field name. Read all values with `new FormData(form).getAll('regions')`.
116
116
 
117
- The legacy combination of `multiple`, a comma-separated string value, and a string callback remains compatible in version 1 but is deprecated. It cannot represent values containing commas unambiguously and will be removed in version 2.
117
+ Multiple selection uses a typed array value and submits one native form entry per selected option. Values containing commas remain unambiguous.
118
118
 
119
119
  ## Forms and accessibility
120
120
 
@@ -154,7 +154,7 @@ export function DepartmentForm() {
154
154
 
155
155
  ## Localization
156
156
 
157
- German messages remain the default for backward compatibility. Set `locale="en"` for the complete English catalog, or override individual messages with a typed `Partial<SelectMessages>`. The catalog and resolver are available as `selectMessageCatalog` and `resolveSelectMessages`.
157
+ German messages remain the default for backward compatibility. Set `locale` to `de`, `en`, `es`, or `fr`, or override individual messages with a typed `Partial<SelectMessages>`. The catalog and resolver are available as `selectMessageCatalog` and `resolveSelectMessages`.
158
158
 
159
159
  ```tsx
160
160
  import { CustomSelect, type SelectMessages } from '@rentnerkev/select'
@@ -181,38 +181,90 @@ export function LocalizedSelect() {
181
181
  }
182
182
  ```
183
183
 
184
+ ## Project-wide defaults
185
+
186
+ Use `SelectProvider` once near the root of your app to set locale, messages, search behavior, styling slots, and a CSP nonce. Any prop passed to an individual select overrides the corresponding provider default; `messages` and `classNames` are merged by key.
187
+
188
+ ```tsx
189
+ import { CustomSelect, SelectProvider } from '@rentnerkev/select'
190
+
191
+ export function App({ cspNonce }: { cspNonce?: string }) {
192
+ return (
193
+ <SelectProvider
194
+ locale="en"
195
+ searchable={false}
196
+ nonce={cspNonce}
197
+ classNames={{ trigger: 'w-full', content: 'shadow-xl' }}
198
+ >
199
+ <CustomSelect
200
+ value=""
201
+ onValueChange={() => undefined}
202
+ options={[{ value: 'one', label: 'One' }]}
203
+ placeholder="Choose"
204
+ />
205
+ </SelectProvider>
206
+ )
207
+ }
208
+ ```
209
+
210
+ The nonce is forwarded to Radix's viewport style tag. The package CSS also contains the viewport scrollbar rules, so the scrollbar remains styled under strict CSP. Search is enabled by default for compatibility; set `searchable={false}` globally or per select for short menus.
211
+
212
+ ## Custom option content
213
+
214
+ `renderOption` and `renderValue` allow icons, flags, or project-specific layouts without replacing the select's interaction logic. Keep decorative content `aria-hidden`; the plain `label` remains the searchable, accessible text. Individual options can be disabled.
215
+
216
+ ```tsx
217
+ <CustomSelect
218
+ value={language}
219
+ onValueChange={setLanguage}
220
+ options={[
221
+ { value: 'de', label: 'Deutsch' },
222
+ { value: 'en', label: 'English', disabled: true },
223
+ ]}
224
+ renderOption={(option) => <span>{option.label}</span>}
225
+ renderValue={(selected) => <span>{selected[0]?.label}</span>}
226
+ onBlur={handleBlur}
227
+ />
228
+ ```
229
+
184
230
  ## API
185
231
 
186
232
  ### `CustomSelect` props
187
233
 
188
- | Prop | Type | Description |
189
- | ---------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------- |
190
- | `id` | `string` | ID for the visible trigger and associated labels. |
191
- | `name` | `string` | Native form field name. |
192
- | `value` | `TValue \| null \| undefined` or `ReadonlyArray<TValue>` | Controlled single or multiple value. |
193
- | `onValueChange` | `(value: TValue) => void` or `(value: TValue[]) => void` | Typed callback for the selected mode. |
194
- | `options` | `ReadonlyArray<Option<TValue>>` | Available options. |
195
- | `required` | `boolean` | Enables required-field validation. Defaults to `false`. |
196
- | `label` | `ReactNode` | Visible label linked to the trigger. |
197
- | `description` | `ReactNode` | Supporting text linked through `aria-describedby`. |
198
- | `error` | `string \| null` | External validation message; `null` clears external and native errors. |
199
- | `disabled` | `boolean` | Disables interaction and validation. |
200
- | `readOnly` | `boolean` | Prevents changes while retaining the form value. |
201
- | `className` | `string` | Additional Tailwind classes for the visible trigger. |
202
- | `placeholder` | `string` | Text shown while no value is selected. |
203
- | `icon` | `ReactNode` | Icon rendered at the start of the trigger. |
204
- | `fallbackOption` | `string` | Message shown when no options are available. |
205
- | `multiple` | `true` | Enables array-based multiple selection. |
206
- | `minSelection` | `number` | Minimum number of selected options. |
207
- | `maxSelection` | `number` | Maximum number of selected options. |
208
- | `isOptionEqualToValue` | `(optionValue, value) => boolean` | Compares option and selected values. |
209
- | `getFormValue` | `(value: TValue) => string` | Serializes a value for native form submission. |
210
- | `locale` | `'de' \| 'en'` | Selects the default message catalog. Defaults to `'de'`. |
211
- | `messages` | `Partial<SelectMessages>` | Overrides individual messages and ARIA text. |
212
- | `aria-label` | `string` | Accessible name for the visible trigger. |
213
- | `aria-labelledby` | `string` | External accessible-label IDs. |
214
- | `aria-describedby` | `string` | External description IDs combined with internal text. |
215
- | `triggerRef` | `Ref<HTMLButtonElement>` | Ref for the visible, focusable trigger. |
234
+ | Prop | Type | Description |
235
+ | ---------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
236
+ | `id` | `string` | ID for the visible trigger and associated labels. |
237
+ | `name` | `string` | Native form field name. |
238
+ | `value` | `TValue \| null \| undefined` or `ReadonlyArray<TValue>` | Controlled single or multiple value. |
239
+ | `onValueChange` | `(value: TValue) => void` or `(value: TValue[]) => void` | Typed callback for the selected mode. |
240
+ | `options` | `ReadonlyArray<Option<TValue>>` | Available options. |
241
+ | `required` | `boolean` | Enables required-field validation. Defaults to `false`. |
242
+ | `label` | `ReactNode` | Visible label linked to the trigger. |
243
+ | `description` | `ReactNode` | Supporting text linked through `aria-describedby`. |
244
+ | `error` | `string \| null` | External validation message; `null` clears external and native errors. |
245
+ | `disabled` | `boolean` | Disables interaction and validation. |
246
+ | `readOnly` | `boolean` | Prevents changes while retaining the form value. |
247
+ | `className` | `string` | Additional Tailwind classes for the visible trigger. |
248
+ | `classNames` | `SelectClassNames` | Classes for root, trigger, content, search, viewport, option, empty state, label, and description. |
249
+ | `searchable` | `boolean` | Show the search field. Defaults to `true`. |
250
+ | `nonce` | `string` | CSP nonce for Radix's generated viewport style. |
251
+ | `onBlur` | `FocusEventHandler<HTMLButtonElement>` | Blur handler on the visible trigger. |
252
+ | `renderOption` | `(option, state) => ReactNode` | Custom content for each menu item; `state` includes selected/disabled. |
253
+ | `renderValue` | `(selectedOptions) => ReactNode` | Custom content for the closed trigger. |
254
+ | `placeholder` | `string` | Text shown while no value is selected. |
255
+ | `icon` | `ReactNode` | Icon rendered at the start of the trigger. |
256
+ | `fallbackOption` | `string` | Message shown when no options are available. |
257
+ | `multiple` | `true` | Enables array-based multiple selection. |
258
+ | `minSelection` | `number` | Minimum number of selected options. |
259
+ | `maxSelection` | `number` | Maximum number of selected options. |
260
+ | `isOptionEqualToValue` | `(optionValue, value) => boolean` | Compares option and selected values. |
261
+ | `getFormValue` | `(value: TValue) => string` | Serializes a value for native form submission. |
262
+ | `locale` | `'de' \| 'en' \| 'es' \| 'fr'` | Selects the default message catalog. Defaults to `'de'`. |
263
+ | `messages` | `Partial<SelectMessages>` | Overrides individual messages and ARIA text. |
264
+ | `aria-label` | `string` | Accessible name for the visible trigger. |
265
+ | `aria-labelledby` | `string` | External accessible-label IDs. |
266
+ | `aria-describedby` | `string` | External description IDs combined with internal text. |
267
+ | `triggerRef` | `Ref<HTMLButtonElement>` | Ref for the visible, focusable trigger. |
216
268
 
217
269
  Additional React `aria-*` attributes are forwarded to the visible trigger.
218
270
 
@@ -223,6 +275,7 @@ interface Option<TValue = string> {
223
275
  value: TValue
224
276
  label: string
225
277
  subOption?: string
278
+ disabled?: boolean
226
279
  }
227
280
  ```
228
281
 
@@ -237,19 +290,32 @@ Import the package entry after Tailwind CSS in your application stylesheet:
237
290
  @import '@rentnerkev/select/tailwind.css';
238
291
  ```
239
292
 
240
- The package entry scans only the published JavaScript under `dist` and provides the shared theme tokens `primary`, `primary-hover`, `background-dark`, `surface-dark`, `input-dark`, `border-dark`, `secondary-text`, and `muted-foreground`. Override them with a later `@theme` block when needed.
293
+ The package entry scans only the published JavaScript under `dist`. Select styling uses `--color-select-control`, `--color-select-surface`, `--color-select-hover`, `--color-select-border`, `--color-select-border-strong`, `--color-select-foreground`, `--color-select-muted`, and `--color-select-accent`. Override these tokens with a later `@theme inline` block to map them to your app's light/dark tokens. The older shared theme tokens remain available for compatibility.
294
+
295
+ ```css
296
+ @theme inline {
297
+ --color-select-control: var(--app-input);
298
+ --color-select-surface: var(--app-panel);
299
+ --color-select-hover: var(--app-hover);
300
+ --color-select-border: var(--app-border);
301
+ --color-select-border-strong: var(--app-border-strong);
302
+ --color-select-foreground: var(--app-text);
303
+ --color-select-muted: var(--app-muted);
304
+ --color-select-accent: var(--app-accent);
305
+ }
306
+ ```
241
307
 
242
308
  ## Public entry points
243
309
 
244
- | Entry point | Purpose |
245
- | --------------------------------- | ---------------------------------------------- |
246
- | `@rentnerkev/select` | Component, messages, and public types. |
247
- | `@rentnerkev/select/select` | `CustomSelect` component module. |
248
- | `@rentnerkev/select/value` | Value parsing, comparison, and toggle helpers. |
249
- | `@rentnerkev/select/messages` | Locale catalog, resolver, and message types. |
250
- | `@rentnerkev/select/types` | Component and option types. |
251
- | `@rentnerkev/select/tailwind.css` | Tailwind source and shared theme tokens. |
252
- | `@rentnerkev/select/package.json` | Package metadata. |
310
+ | Entry point | Purpose |
311
+ | --------------------------------- | -------------------------------------------- |
312
+ | `@rentnerkev/select` | Component, messages, and public types. |
313
+ | `@rentnerkev/select/select` | `CustomSelect` component module. |
314
+ | `@rentnerkev/select/value` | Value comparison and toggle helpers. |
315
+ | `@rentnerkev/select/messages` | Locale catalog, resolver, and message types. |
316
+ | `@rentnerkev/select/types` | Component and option types. |
317
+ | `@rentnerkev/select/tailwind.css` | Tailwind source and shared theme tokens. |
318
+ | `@rentnerkev/select/package.json` | Package metadata. |
253
319
 
254
320
  ## Development
255
321
 
@@ -0,0 +1,12 @@
1
+ import type { SingleSelectProps } from '../types.js';
2
+ export type SelectViewProps<TValue> = Omit<SingleSelectProps<TValue>, 'value' | 'onValueChange' | 'multiple' | 'getFormValue'> & {
3
+ selectedValues: ReadonlyArray<TValue>;
4
+ formEntries: ReadonlyArray<{
5
+ key: string;
6
+ value: string;
7
+ }>;
8
+ multiple: boolean;
9
+ onSelectValue: (value: TValue) => void;
10
+ };
11
+ export default function SelectView<TValue>({ id, name, options, selectedValues, formEntries, onSelectValue, required, label, description, error: externalError, disabled, readOnly, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, triggerRef, onBlur, icon, placeholder, className, classNames: providedClassNames, searchable: providedSearchable, nonce: providedNonce, renderOption, renderValue, fallbackOption, multiple, minSelection, maxSelection, locale: providedLocale, messages: providedMessages, isOptionEqualToValue: isOptionEqualToValueProp, ...ariaProps }: SelectViewProps<TValue>): import("react").JSX.Element;
12
+ //# sourceMappingURL=SelectView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SelectView.d.ts","sourceRoot":"","sources":["../../src/Components/SelectView.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAEpD,MAAM,MAAM,eAAe,CAAC,MAAM,IAAI,IAAI,CACtC,iBAAiB,CAAC,MAAM,CAAC,EACzB,OAAO,GAAG,eAAe,GAAG,UAAU,GAAG,cAAc,CAC1D,GAAG;IACA,cAAc,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACrC,WAAW,EAAE,aAAa,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC1D,QAAQ,EAAE,OAAO,CAAA;IACjB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACzC,CAAA;AAUD,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,MAAM,EAAE,EACvC,EAAE,EACF,IAAI,EACJ,OAAO,EACP,cAAc,EACd,WAAW,EACX,aAAa,EACb,QAAgB,EAChB,KAAK,EACL,WAAW,EACX,KAAK,EAAE,aAAa,EACpB,QAAgB,EAChB,QAAgB,EAChB,YAAY,EAAE,SAAS,EACvB,iBAAiB,EAAE,cAAc,EACjC,kBAAkB,EAAE,eAAe,EACnC,UAAU,EACV,MAAM,EACN,IAAI,EACJ,WAAW,EACX,SAAS,EACT,UAAU,EAAE,kBAAkB,EAC9B,UAAU,EAAE,kBAAkB,EAC9B,KAAK,EAAE,aAAa,EACpB,YAAY,EACZ,WAAW,EACX,cAAc,EACd,QAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,gBAAgB,EAC1B,oBAAoB,EAAE,wBAAwB,EAC9C,GAAG,SAAS,EACf,EAAE,eAAe,CAAC,MAAM,CAAC,+BA+WzB"}
@@ -0,0 +1,104 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as SelectPrimitive from '@radix-ui/react-select';
3
+ import { CustomTooltip } from '@rentnerkev/tooltips';
4
+ import { AlertCircle, Check, ChevronDown } from 'lucide-react';
5
+ import { useSelectDefaults } from '../SelectProvider.js';
6
+ import useSelectLogic from '../Hooks/useSelect.logic.js';
7
+ function mergeAriaIds(...values) {
8
+ const ids = values.flatMap((value) => value?.split(/\s+/).filter(Boolean) ?? []);
9
+ return [...new Set(ids)].join(' ') || undefined;
10
+ }
11
+ export default function SelectView({ id, name, options, selectedValues, formEntries, onSelectValue, required = false, label, description, error: externalError, disabled = false, readOnly = false, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, triggerRef, onBlur, icon, placeholder, className, classNames: providedClassNames, searchable: providedSearchable, nonce: providedNonce, renderOption, renderValue, fallbackOption, multiple = false, minSelection, maxSelection, locale: providedLocale, messages: providedMessages, isOptionEqualToValue: isOptionEqualToValueProp, ...ariaProps }) {
12
+ const defaults = useSelectDefaults();
13
+ const locale = providedLocale ?? defaults.locale ?? 'de';
14
+ const searchable = providedSearchable ?? defaults.searchable ?? true;
15
+ const nonce = providedNonce ?? defaults.nonce;
16
+ const classNames = { ...defaults.classNames, ...providedClassNames };
17
+ const messages = { ...defaults.messages, ...providedMessages };
18
+ const logic = useSelectLogic({
19
+ id,
20
+ options,
21
+ selectedValues,
22
+ multiple,
23
+ required,
24
+ externalError,
25
+ disabled,
26
+ readOnly,
27
+ searchable,
28
+ triggerRef,
29
+ minSelection,
30
+ maxSelection,
31
+ locale,
32
+ messages,
33
+ isOptionEqualToValue: isOptionEqualToValueProp,
34
+ onSelectValue,
35
+ });
36
+ const { triggerId, labelId, descriptionId, errorId, messages: resolvedMessages, open, searchValue, filteredOptions, selectedEntries, selectedRadixValue, hasError, resolvedError, isOptionEqualToValue, } = logic.state;
37
+ const { trigger, validationInput, searchInput } = logic.ref;
38
+ const { handleInvalid, handleValueChange, handleOpenChange, handleContentKeyDownCapture, setSearchValue, } = logic.handler;
39
+ const { shouldKeepOpen } = logic.setter;
40
+ const hasLeftIcon = Boolean(icon || hasError);
41
+ const describedBy = mergeAriaIds(ariaDescribedBy, description !== undefined && description !== null
42
+ ? descriptionId
43
+ : undefined, hasError ? errorId : undefined);
44
+ const labelledBy = mergeAriaIds(ariaLabelledBy, label !== undefined && label !== null ? labelId : undefined);
45
+ return (_jsxs(SelectPrimitive.Root, { open: open, onOpenChange: handleOpenChange, value: selectedRadixValue, onValueChange: handleValueChange, children: [_jsxs("div", { className: `group relative ${classNames.root || ''}`, children: [label !== undefined && label !== null && (_jsx("label", { id: labelId, htmlFor: triggerId, className: `mb-1 block text-sm font-medium text-select-foreground ${classNames.label || ''}`, children: label })), _jsx("input", { ref: validationInput, name: name, value: formEntries[0]?.value ?? '', onChange: () => undefined, onInvalid: handleInvalid, required: required &&
46
+ selectedValues.length === 0 &&
47
+ !disabled &&
48
+ externalError === undefined, disabled: disabled, readOnly: readOnly, tabIndex: -1, "aria-hidden": "true", className: "pointer-events-none absolute left-0 top-1/2 h-px w-px -translate-y-1/2 opacity-0" }), multiple &&
49
+ formEntries
50
+ .slice(1)
51
+ .map((formEntry) => (_jsx("input", { type: "hidden", name: name, value: formEntry.value, disabled: disabled, readOnly: readOnly }, formEntry.key))), hasLeftIcon && (_jsx("div", { className: "absolute left-3 top-1/2 z-10 -translate-y-1/2 text-select-muted", children: hasError ? (_jsx(CustomTooltip, { content: resolvedError || '', side: "bottom", children: _jsx(AlertCircle, { className: "h-4 w-4 text-red-500" }) })) : (_jsx("span", { className: "pointer-events-none flex items-center transition-colors group-focus-within:text-primary [&>svg]:h-4 [&>svg]:w-4", children: icon })) })), _jsxs(SelectPrimitive.Trigger, { ref: trigger, id: triggerId, disabled: disabled, onBlur: onBlur, ...ariaProps, "aria-invalid": hasError || ariaProps['aria-invalid'] || undefined, "aria-required": disabled
52
+ ? undefined
53
+ : externalError === undefined
54
+ ? required ||
55
+ ariaProps['aria-required'] ||
56
+ undefined
57
+ : ariaProps['aria-required'], "aria-label": ariaLabel, "aria-labelledby": labelledBy, "aria-describedby": describedBy, "aria-errormessage": hasError ? errorId : ariaProps['aria-errormessage'], "aria-readonly": readOnly || ariaProps['aria-readonly'] || undefined, "aria-disabled": disabled || ariaProps['aria-disabled'] || undefined, onPointerDown: (event) => {
58
+ if (readOnly) {
59
+ event.preventDefault();
60
+ event.currentTarget.focus();
61
+ }
62
+ }, onKeyDown: (event) => {
63
+ if (readOnly &&
64
+ (event.key === 'Enter' ||
65
+ event.key === ' ' ||
66
+ event.key === 'ArrowDown' ||
67
+ event.key === 'ArrowUp')) {
68
+ event.preventDefault();
69
+ }
70
+ }, className: `box-border flex h-12 w-full min-w-0 cursor-pointer items-center justify-between gap-2 rounded-xl border bg-select-control pr-9 text-left text-sm font-medium tracking-normal normal-case text-select-foreground outline-none transition-[border-color,box-shadow,background-color] hover:border-select-border-strong focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none ${hasLeftIcon ? 'pl-9' : 'pl-3'} ${hasError
71
+ ? 'border-red-500 focus-visible:ring-red-500/20 data-[state=open]:border-red-500'
72
+ : 'border-select-border focus-visible:border-select-accent focus-visible:ring-select-accent/20 data-[state=open]:border-select-accent'} ${classNames.trigger || ''} ${className || ''}`, children: [_jsx("span", { className: "min-w-0 flex-1 text-left", children: selectedEntries.length > 0 ? (_jsx("span", { className: "flex min-w-0 flex-col gap-0.5", children: renderValue ? (renderValue(selectedEntries.map(({ option }) => option))) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "truncate leading-5", children: selectedEntries
73
+ .map(({ option }) => option.label)
74
+ .join(', ') }), selectedEntries.length === 1 &&
75
+ selectedEntries[0].option
76
+ .subOption && (_jsx("span", { className: "truncate text-xs leading-4 text-select-muted", children: selectedEntries[0]
77
+ .option.subOption }))] })) })) : (_jsx(SelectPrimitive.Value, { placeholder: placeholder })) }), _jsx(SelectPrimitive.Icon, { asChild: true, children: _jsx(ChevronDown, { className: "absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-select-muted" }) })] })] }), description !== undefined && description !== null && (_jsx("div", { id: descriptionId, className: `mt-1 text-xs text-select-muted ${classNames.description || ''}`, children: description })), hasError && (_jsx("span", { id: errorId, className: "sr-only", "aria-live": "polite", children: resolvedError })), _jsx(SelectPrimitive.Portal, { children: _jsxs(SelectPrimitive.Content, { position: "popper", sideOffset: 4, onKeyDownCapture: handleContentKeyDownCapture, className: `z-9998 w-(--radix-select-trigger-width) min-w-45 overflow-hidden rounded-xl border border-select-border bg-select-surface text-select-foreground shadow-xl motion-safe:animate-in motion-safe:fade-in motion-safe: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 ${classNames.content || ''}`, children: [searchable && (_jsx("div", { className: "border-b border-select-border p-2", children: _jsx("input", { ref: searchInput, "aria-label": resolvedMessages.searchOptions, value: searchValue, onChange: (event) => setSearchValue(event.target.value), onPointerDownCapture: (event) => event.stopPropagation(), onKeyDownCapture: (event) => {
78
+ if (event.key !== 'Escape') {
79
+ event.stopPropagation();
80
+ }
81
+ }, onKeyDown: (event) => {
82
+ if (event.key !== 'Escape') {
83
+ event.stopPropagation();
84
+ }
85
+ }, placeholder: resolvedMessages.searchPlaceholder, className: `h-10 w-full rounded-lg border border-select-border bg-select-control px-3 text-sm font-normal tracking-normal normal-case text-select-foreground placeholder:text-select-muted outline-none focus-visible:border-select-accent focus-visible:ring-2 focus-visible:ring-select-accent/20 ${classNames.search || ''}` }) })), _jsx("div", { className: "rentnerselect-scrollbar max-h-[min(var(--radix-select-content-available-height),16rem)] overflow-y-scroll scrollbar-gutter-stable", children: _jsx(SelectPrimitive.Viewport, { nonce: nonce, className: `p-1.5 ${classNames.viewport || ''}`, children: filteredOptions.length > 0 ? (filteredOptions.map(({ option, radixValue }) => (_jsxs(SelectPrimitive.Item, { value: radixValue, textValue: option.label, disabled: option.disabled, onPointerDown: () => {
86
+ if (multiple) {
87
+ shouldKeepOpen.current = true;
88
+ }
89
+ }, onKeyDown: (event) => {
90
+ if (multiple &&
91
+ (event.key === 'Enter' ||
92
+ event.key === ' ')) {
93
+ shouldKeepOpen.current = true;
94
+ }
95
+ }, className: `relative flex min-h-11 w-full cursor-pointer select-none items-center rounded-lg py-2 pl-9 pr-3 text-sm font-medium tracking-normal normal-case text-select-foreground outline-none transition-colors data-[highlighted]:bg-select-hover data-[highlighted]:text-select-foreground data-[state=checked]:bg-select-hover data-disabled:cursor-not-allowed data-disabled:opacity-40 ${classNames.option || ''}`, children: [_jsx("span", { className: "absolute left-3 flex h-3.5 w-3.5 items-center justify-center", children: multiple ? (selectedValues.some((value) => isOptionEqualToValue(option.value, value)) && (_jsx(Check, { className: "h-4 w-4" }))) : (_jsx(SelectPrimitive.ItemIndicator, { children: _jsx(Check, { className: "h-4 w-4" }) })) }), _jsx(SelectPrimitive.ItemText, { children: renderOption ? (renderOption(option, {
96
+ selected: selectedValues.some((value) => isOptionEqualToValue(option.value, value)),
97
+ disabled: option.disabled ??
98
+ false,
99
+ })) : (_jsxs("span", { className: "flex min-w-0 flex-col gap-0.5", children: [_jsx("span", { className: "truncate leading-5", children: option.label }), option.subOption && (_jsx("span", { className: "truncate text-xs leading-4 text-select-muted", children: option.subOption }))] })) })] }, radixValue)))) : (_jsx("div", { className: `relative flex w-full select-none items-center rounded-lg py-2 pl-9 pr-3 text-sm text-select-muted ${classNames.empty || ''}`, children: searchable && searchValue.trim()
100
+ ? resolvedMessages.noResults
101
+ : fallbackOption ||
102
+ resolvedMessages.noOptions })) }) })] }) })] }));
103
+ }
104
+ //# sourceMappingURL=SelectView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SelectView.js","sourceRoot":"","sources":["../../src/Components/SelectView.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ import type { InvalidEvent, KeyboardEvent, Ref } from 'react';
2
+ import { type SelectLocale, type SelectMessages } from '../i18n.js';
3
+ import type { Option } from '../types.js';
4
+ export interface UseSelectLogicOptions<TValue> {
5
+ id?: string;
6
+ name?: string;
7
+ options: ReadonlyArray<Option<TValue>>;
8
+ selectedValues: ReadonlyArray<TValue>;
9
+ multiple: boolean;
10
+ required?: boolean;
11
+ externalError?: string | null;
12
+ disabled?: boolean;
13
+ readOnly?: boolean;
14
+ searchable?: boolean;
15
+ triggerRef?: Ref<HTMLButtonElement>;
16
+ minSelection?: number;
17
+ maxSelection?: number;
18
+ locale?: SelectLocale;
19
+ messages?: Partial<SelectMessages>;
20
+ isOptionEqualToValue?: (optionValue: TValue, value: TValue) => boolean;
21
+ onSelectValue: (value: TValue) => void;
22
+ }
23
+ export default function useSelectLogic<TValue>({ id, options, selectedValues, multiple, required, externalError, disabled, readOnly, searchable, triggerRef, minSelection, maxSelection, locale, messages: providedMessages, isOptionEqualToValue: isOptionEqualToValueProp, onSelectValue, }: UseSelectLogicOptions<TValue>): {
24
+ ref: {
25
+ trigger: (node: HTMLButtonElement | null) => void;
26
+ searchInput: import("react").RefObject<HTMLInputElement | null>;
27
+ validationInput: import("react").RefObject<HTMLInputElement | null>;
28
+ };
29
+ state: {
30
+ messages: SelectMessages;
31
+ triggerId: string;
32
+ labelId: string;
33
+ descriptionId: string;
34
+ errorId: string;
35
+ open: boolean;
36
+ searchValue: string;
37
+ optionEntries: {
38
+ option: Option<TValue>;
39
+ radixValue: string;
40
+ }[];
41
+ filteredOptions: {
42
+ option: Option<TValue>;
43
+ radixValue: string;
44
+ }[];
45
+ selectedEntries: {
46
+ option: Option<TValue>;
47
+ radixValue: string;
48
+ }[];
49
+ selectedRadixValue: string;
50
+ hasError: boolean;
51
+ resolvedError: string | null;
52
+ hasLeftIcon: boolean;
53
+ isOptionEqualToValue: (value1: any, value2: any) => boolean;
54
+ };
55
+ handler: {
56
+ handleInvalid: (event: InvalidEvent<HTMLInputElement>) => void;
57
+ handleValueChange: (nextRadixValue: string) => void;
58
+ handleOpenChange: (nextOpen: boolean) => void;
59
+ handleContentKeyDownCapture: (event: KeyboardEvent<HTMLDivElement>) => void;
60
+ setSearchValue: import("react").Dispatch<import("react").SetStateAction<string>>;
61
+ };
62
+ setter: {
63
+ setOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
64
+ setSearchValue: import("react").Dispatch<import("react").SetStateAction<string>>;
65
+ setIsTouched: import("react").Dispatch<import("react").SetStateAction<boolean>>;
66
+ shouldKeepOpen: import("react").RefObject<boolean>;
67
+ };
68
+ };
69
+ export type SelectLogic = ReturnType<typeof useSelectLogic>;
70
+ //# sourceMappingURL=useSelect.logic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSelect.logic.d.ts","sourceRoot":"","sources":["../../src/Hooks/useSelect.logic.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAC7D,OAAO,EAEH,KAAK,YAAY,EACjB,KAAK,cAAc,EACtB,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,MAAM,WAAW,qBAAqB,CAAC,MAAM;IACzC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAA;IACtC,cAAc,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACrC,QAAQ,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,GAAG,CAAC,iBAAiB,CAAC,CAAA;IACnC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;IAClC,oBAAoB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAA;IACtE,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACzC;AAED,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,MAAM,EAAE,EAC3C,EAAE,EACF,OAAO,EACP,cAAc,EACd,QAAQ,EACR,QAAgB,EAChB,aAAa,EACb,QAAgB,EAChB,QAAgB,EAChB,UAAiB,EACjB,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,MAAa,EACb,QAAQ,EAAE,gBAAgB,EAC1B,oBAAoB,EAAE,wBAAwB,EAC9C,aAAa,GAChB,EAAE,qBAAqB,CAAC,MAAM,CAAC;;QAkNpB,OAAO,SAvHJ,iBAAiB,GAAG,IAAI;QAwH3B,WAAW;QACX,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAvEO,YAAY,CAAC,gBAAgB,CAAC;4CAMjB,MAAM;qCAUb,OAAO;6CAcC,aAAa,CAAC,cAAc,CAAC;;;;;;;;;EA0E5E;AAED,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAA"}
@@ -0,0 +1,203 @@
1
+ import { useCallback, useEffect, useId, useImperativeHandle, useMemo, useRef, useState, } from 'react';
2
+ import { resolveSelectMessages, } from '../i18n.js';
3
+ export default function useSelectLogic({ id, options, selectedValues, multiple, required = false, externalError, disabled = false, readOnly = false, searchable = true, triggerRef, minSelection, maxSelection, locale = 'de', messages: providedMessages, isOptionEqualToValue: isOptionEqualToValueProp, onSelectValue, }) {
4
+ const messages = resolveSelectMessages(locale, providedMessages);
5
+ const [open, setOpen] = useState(false);
6
+ const interactionDisabled = disabled || readOnly;
7
+ const [previousInteractionDisabled, setPreviousInteractionDisabled] = useState(interactionDisabled);
8
+ const [searchValue, setSearchValue] = useState('');
9
+ const [isTouched, setIsTouched] = useState(false);
10
+ const searchInputRef = useRef(null);
11
+ const validationInputRef = useRef(null);
12
+ const internalTriggerRef = useRef(null);
13
+ const shouldKeepOpen = useRef(false);
14
+ const generatedId = useId();
15
+ const triggerId = id ?? `select-${generatedId}`;
16
+ const labelId = `${triggerId}-label`;
17
+ const descriptionId = `${triggerId}-description`;
18
+ const errorId = `${triggerId}-error`;
19
+ const isOptionEqualToValue = isOptionEqualToValueProp ?? Object.is;
20
+ const optionEntries = useMemo(() => options.map((option, index) => ({
21
+ option,
22
+ radixValue: `option-${index}`,
23
+ })), [options]);
24
+ const filteredOptions = useMemo(() => {
25
+ const normalizedSearch = (searchable ? searchValue : '')
26
+ .trim()
27
+ .toLowerCase();
28
+ if (!normalizedSearch)
29
+ return optionEntries;
30
+ return optionEntries.filter(({ option }) => {
31
+ const optionLabel = option.label.toLowerCase();
32
+ const optionValue = String(option.value).toLowerCase();
33
+ const subOption = option.subOption?.toLowerCase() || '';
34
+ return (optionLabel.includes(normalizedSearch) ||
35
+ optionValue.includes(normalizedSearch) ||
36
+ subOption.includes(normalizedSearch));
37
+ });
38
+ }, [optionEntries, searchValue, searchable]);
39
+ const selectedEntries = useMemo(() => optionEntries.filter(({ option }) => selectedValues.some((value) => isOptionEqualToValue(option.value, value))), [isOptionEqualToValue, optionEntries, selectedValues]);
40
+ const selectedRadixValue = multiple
41
+ ? ''
42
+ : (selectedEntries[0]?.radixValue ?? '');
43
+ const internalError = useMemo(() => {
44
+ if (required && selectedValues.length === 0)
45
+ return messages.required;
46
+ if (multiple &&
47
+ minSelection !== undefined &&
48
+ selectedValues.length < minSelection) {
49
+ return messages.minSelection(minSelection);
50
+ }
51
+ if (multiple &&
52
+ maxSelection !== undefined &&
53
+ selectedValues.length > maxSelection) {
54
+ return messages.maxSelection(maxSelection);
55
+ }
56
+ return null;
57
+ }, [
58
+ maxSelection,
59
+ messages,
60
+ minSelection,
61
+ multiple,
62
+ required,
63
+ selectedValues,
64
+ ]);
65
+ const resolvedError = externalError !== undefined ? externalError : internalError;
66
+ const hasError = externalError !== undefined
67
+ ? Boolean(resolvedError)
68
+ : isTouched && Boolean(resolvedError);
69
+ const setTriggerRef = useCallback((node) => {
70
+ internalTriggerRef.current = node;
71
+ if (typeof triggerRef === 'function')
72
+ triggerRef(node);
73
+ }, [triggerRef]);
74
+ useImperativeHandle(typeof triggerRef === 'object' ? triggerRef : null, () => internalTriggerRef.current);
75
+ const focusSearchInput = useCallback(() => {
76
+ if (!searchable)
77
+ return;
78
+ requestAnimationFrame(() => searchInputRef.current?.focus());
79
+ window.setTimeout(() => searchInputRef.current?.focus(), 0);
80
+ }, [searchable]);
81
+ useEffect(() => {
82
+ if (open)
83
+ focusSearchInput();
84
+ }, [focusSearchInput, open]);
85
+ if (previousInteractionDisabled !== interactionDisabled) {
86
+ setPreviousInteractionDisabled(interactionDisabled);
87
+ if (interactionDisabled && open) {
88
+ setOpen(false);
89
+ }
90
+ }
91
+ useEffect(() => {
92
+ validationInputRef.current?.setCustomValidity(disabled ? '' : resolvedError || '');
93
+ }, [disabled, resolvedError]);
94
+ useEffect(() => {
95
+ const input = validationInputRef.current;
96
+ const form = input?.form;
97
+ if (!input || !form)
98
+ return;
99
+ const currentInput = input;
100
+ function handleFormSubmit() {
101
+ if (currentInput.validity.valid)
102
+ setIsTouched(false);
103
+ }
104
+ form.addEventListener('submit', handleFormSubmit);
105
+ return () => form.removeEventListener('submit', handleFormSubmit);
106
+ }, []);
107
+ function handleInvalid(event) {
108
+ event.preventDefault();
109
+ setIsTouched(true);
110
+ internalTriggerRef.current?.focus();
111
+ }
112
+ function handleValueChange(nextRadixValue) {
113
+ if (disabled || readOnly)
114
+ return;
115
+ const entry = optionEntries.find(({ radixValue }) => radixValue === nextRadixValue);
116
+ if (!entry)
117
+ return;
118
+ if (!multiple)
119
+ setSearchValue('');
120
+ onSelectValue(entry.option.value);
121
+ }
122
+ function handleOpenChange(nextOpen) {
123
+ if (disabled || readOnly) {
124
+ setOpen(false);
125
+ return;
126
+ }
127
+ if (multiple && !nextOpen && shouldKeepOpen.current) {
128
+ shouldKeepOpen.current = false;
129
+ return;
130
+ }
131
+ setOpen(nextOpen);
132
+ if (nextOpen)
133
+ focusSearchInput();
134
+ else
135
+ setSearchValue('');
136
+ }
137
+ function handleContentKeyDownCapture(event) {
138
+ if (!searchable)
139
+ return;
140
+ if (event.target === searchInputRef.current) {
141
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
142
+ const focusableOptions = Array.from(event.currentTarget.querySelectorAll('[role="option"]:not([data-disabled])'));
143
+ const option = event.key === 'ArrowDown'
144
+ ? focusableOptions[0]
145
+ : focusableOptions[focusableOptions.length - 1];
146
+ if (option) {
147
+ event.preventDefault();
148
+ event.stopPropagation();
149
+ option.focus();
150
+ }
151
+ }
152
+ return;
153
+ }
154
+ if (event.ctrlKey ||
155
+ event.altKey ||
156
+ event.metaKey ||
157
+ event.key.length !== 1) {
158
+ return;
159
+ }
160
+ event.preventDefault();
161
+ event.stopPropagation();
162
+ setSearchValue((current) => `${current}${event.key}`);
163
+ focusSearchInput();
164
+ }
165
+ return {
166
+ ref: {
167
+ trigger: setTriggerRef,
168
+ searchInput: searchInputRef,
169
+ validationInput: validationInputRef,
170
+ },
171
+ state: {
172
+ messages,
173
+ triggerId,
174
+ labelId,
175
+ descriptionId,
176
+ errorId,
177
+ open: open && !disabled && !readOnly,
178
+ searchValue,
179
+ optionEntries,
180
+ filteredOptions,
181
+ selectedEntries,
182
+ selectedRadixValue,
183
+ hasError,
184
+ resolvedError,
185
+ hasLeftIcon: Boolean(hasError),
186
+ isOptionEqualToValue,
187
+ },
188
+ handler: {
189
+ handleInvalid,
190
+ handleValueChange,
191
+ handleOpenChange,
192
+ handleContentKeyDownCapture,
193
+ setSearchValue,
194
+ },
195
+ setter: {
196
+ setOpen,
197
+ setSearchValue,
198
+ setIsTouched,
199
+ shouldKeepOpen,
200
+ },
201
+ };
202
+ }
203
+ //# sourceMappingURL=useSelect.logic.js.map