@godxjp/ui 22.0.0 → 23.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.
Files changed (83) hide show
  1. package/README.md +4 -4
  2. package/dist/components/data-display/scroll-area.d.ts +24 -4
  3. package/dist/components/data-display/scroll-area.js +34 -72
  4. package/dist/components/data-entry/cascader.js +8 -6
  5. package/dist/components/data-entry/checkbox.js +2 -0
  6. package/dist/components/data-entry/choice-hit-target.d.ts +24 -0
  7. package/dist/components/data-entry/choice-hit-target.js +12 -0
  8. package/dist/components/data-entry/control-surface.d.ts +8 -0
  9. package/dist/components/data-entry/control-surface.js +10 -1
  10. package/dist/components/data-entry/form-field.js +3 -2
  11. package/dist/components/data-entry/radio.js +3 -2
  12. package/dist/components/data-entry/search-select.js +225 -95
  13. package/dist/components/data-entry/select.d.ts +76 -14
  14. package/dist/components/data-entry/select.js +495 -163
  15. package/dist/components/data-entry/slider.d.ts +3 -11
  16. package/dist/components/data-entry/slider.js +582 -93
  17. package/dist/components/data-entry/switch.js +2 -0
  18. package/dist/components/navigation/dropdown-menu.d.ts +11 -3
  19. package/dist/components/navigation/dropdown-menu.js +190 -15
  20. package/dist/components/navigation/index.d.ts +0 -3
  21. package/dist/components/navigation/index.js +0 -78
  22. package/dist/components/ui/index.d.ts +0 -3
  23. package/dist/components/ui/index.js +0 -3
  24. package/dist/components/ui/input-otp.d.ts +1 -1
  25. package/dist/components/ui/segmented.js +3 -1
  26. package/dist/components/ui/tag-input.js +3 -5
  27. package/dist/lib/select-options.d.ts +21 -0
  28. package/dist/lib/select-options.js +46 -0
  29. package/dist/props/components/data-display.prop.d.ts +9 -6
  30. package/dist/props/components/data-entry.prop.d.ts +323 -30
  31. package/dist/props/components/index.d.ts +1 -1
  32. package/dist/props/components/navigation.prop.d.ts +17 -0
  33. package/dist/props/registry.d.ts +63 -0
  34. package/dist/props/registry.js +69 -0
  35. package/dist/styles/control.css +205 -23
  36. package/dist/styles/data-display-layout.css +18 -10
  37. package/dist/styles/focus-ring.css +2 -2
  38. package/dist/styles/fonts.css +88 -4
  39. package/dist/styles/navigation-layout.css +11 -236
  40. package/dist/styles/shell-layout.css +144 -139
  41. package/dist/styles/table-layout.css +7 -3
  42. package/dist/tokens/components/control.css +4 -0
  43. package/dist/tokens/components/data-display.css +0 -5
  44. package/dist/tokens/components/navigation.css +3 -5
  45. package/dist/tokens/components/scroll-area.css +7 -0
  46. package/docs/CUSTOMER-THEMING.md +7 -1
  47. package/docs/FRAME-COVERAGE-REPORT.md +4 -7
  48. package/docs/data-display/data-table/index.tsx +27 -1
  49. package/docs/data-display/popover.tsx +4 -6
  50. package/docs/data-display/scroll-area.tsx +51 -45
  51. package/docs/data-entry/date-picker.tsx +2 -3
  52. package/docs/data-entry/input-otp.tsx +5 -2
  53. package/docs/data-entry/segmented.tsx +40 -1
  54. package/docs/data-entry/select-matrix.tsx +1 -2
  55. package/docs/data-entry/select.tsx +108 -3
  56. package/docs/data-entry/slider.tsx +125 -47
  57. package/docs/feedback/dialog.tsx +1 -2
  58. package/docs/feedback/sheet.tsx +1 -2
  59. package/docs/layout/app-shell-states.tsx +143 -0
  60. package/docs/navigation/dropdown-menu.tsx +75 -4
  61. package/docs/roadmap/parity-audit-data-entry.md +44 -33
  62. package/docs/roadmap/parity-audit-layout-navigation-general.md +304 -310
  63. package/docs/roadmap/parity-backlog.md +2 -3
  64. package/package.json +20 -44
  65. package/scripts/ui-audit.mjs +1 -1
  66. package/scripts/visual-audit-rules.mjs +0 -7
  67. package/scripts/visual-audit.mjs +6 -27
  68. package/dist/components/navigation/context-menu.d.ts +0 -21
  69. package/dist/components/navigation/context-menu.js +0 -149
  70. package/dist/components/navigation/menubar.d.ts +0 -28
  71. package/dist/components/navigation/menubar.js +0 -136
  72. package/dist/components/navigation/navigation-menu.d.ts +0 -10
  73. package/dist/components/navigation/navigation-menu.js +0 -91
  74. package/dist/components/ui/context-menu.d.ts +0 -1
  75. package/dist/components/ui/context-menu.js +0 -2
  76. package/dist/components/ui/menubar.d.ts +0 -1
  77. package/dist/components/ui/menubar.js +0 -2
  78. package/dist/components/ui/navigation-menu.d.ts +0 -1
  79. package/dist/components/ui/navigation-menu.js +0 -2
  80. package/docs/FRAME-A11Y-CI.md +0 -349
  81. package/docs/navigation/context-menu.tsx +0 -128
  82. package/docs/navigation/menubar.tsx +0 -141
  83. package/docs/navigation/navigation-menu.tsx +0 -158
@@ -1,71 +1,230 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import * as SelectPrimitive from "@radix-ui/react-select";
5
- import { ChevronDown, ChevronUp, Loader2, X } from "lucide-react";
4
+ import {
5
+ Button as AriaButton,
6
+ ButtonContext,
7
+ Header as AriaHeader,
8
+ ListBox,
9
+ ListBoxItem,
10
+ ListBoxSection,
11
+ Popover as AriaPopover,
12
+ Select as AriaSelect,
13
+ SelectStateContext,
14
+ SelectValue as AriaSelectValue,
15
+ Separator as AriaSeparator
16
+ } from "react-aria-components";
17
+ import { ChevronDown, Loader2, X } from "lucide-react";
6
18
  import { cn } from "../../lib/utils.js";
7
19
  import { controlSurfaceTriggerClass } from "../../lib/control-styles.js";
8
20
  import { controlSurfaceAttrs, resolveAllowClear, resolveAriaInvalid } from "./control-surface.js";
9
- import { useInertHiddenBackground } from "../general/inert-background.js";
10
21
  import {
11
22
  mergeAriaIds,
12
23
  useFieldIdentity,
13
- omitFieldA11y,
14
24
  pickFieldA11y,
15
25
  useFieldNameFallback
16
26
  } from "../../lib/field-a11y.js";
27
+ import { useOverlayPortalContainer } from "../../lib/overlay-portal.js";
28
+ import { radixSurfaceState, toPlacement } from "../navigation/dropdown-menu.js";
17
29
  import { SearchSelect } from "./search-select.js";
18
30
  import { useTranslation } from "../../i18n/use-translation.js";
31
+ import { normalizeSelectOptions } from "../../lib/select-options.js";
19
32
  function isDataSelect(props) {
20
33
  return "options" in props || "loadOptions" in props;
21
34
  }
22
35
  const SelectFieldA11yContext = React.createContext(null);
36
+ function toKey(value) {
37
+ if (value === void 0) return void 0;
38
+ return value === "" ? null : value;
39
+ }
40
+ function splitDomProps(props) {
41
+ const events = {};
42
+ const attributes = {};
43
+ for (const [key, value] of Object.entries(props)) {
44
+ if (/^on[A-Z]/.test(key)) events[key] = value;
45
+ else attributes[key] = value;
46
+ }
47
+ return { events, attributes };
48
+ }
49
+ function itemTextValue(node) {
50
+ if (node == null || typeof node === "boolean") return "";
51
+ if (typeof node === "string" || typeof node === "number") return String(node);
52
+ if (Array.isArray(node)) return node.map(itemTextValue).join("");
53
+ if (React.isValidElement(node)) {
54
+ return itemTextValue(node.props.children);
55
+ }
56
+ return "";
57
+ }
58
+ function SelectRoot({
59
+ id,
60
+ value,
61
+ defaultValue,
62
+ onValueChange,
63
+ open,
64
+ defaultOpen,
65
+ onOpenChange,
66
+ dir,
67
+ name,
68
+ autoComplete,
69
+ disabled,
70
+ isDisabled,
71
+ required,
72
+ form,
73
+ allowsEmptyCollection,
74
+ "aria-label": ariaLabel,
75
+ "aria-labelledby": ariaLabelledby,
76
+ children
77
+ }) {
78
+ void dir;
79
+ return /* @__PURE__ */ jsx(
80
+ AriaSelect,
81
+ {
82
+ "data-slot": "select",
83
+ className: "ui-select-root",
84
+ id,
85
+ value: toKey(value),
86
+ defaultValue: toKey(defaultValue) ?? void 0,
87
+ onChange: (key) => {
88
+ if (key != null) onValueChange?.(String(key));
89
+ },
90
+ isOpen: open,
91
+ defaultOpen,
92
+ onOpenChange,
93
+ isDisabled: disabled || isDisabled,
94
+ isRequired: required,
95
+ name,
96
+ form,
97
+ autoComplete,
98
+ allowsEmptyCollection,
99
+ "aria-label": ariaLabel,
100
+ "aria-labelledby": ariaLabelledby,
101
+ children
102
+ }
103
+ );
104
+ }
23
105
  function Select(props) {
24
106
  if (isDataSelect(props)) {
107
+ if (props.labelInValue) return /* @__PURE__ */ jsx(LabelInValueSelect, { ...props });
25
108
  return /* @__PURE__ */ jsx(DataSelect, { ...props });
26
109
  }
27
110
  return /* @__PURE__ */ jsx(CompoundSelect, { ...props });
28
111
  }
29
- function CompoundSelect({ id, ...props }) {
30
- const fieldA11y = pickFieldA11y(props);
31
- const rootProps = omitFieldA11y(props);
32
- const [uncontrolled, setUncontrolled] = React.useState(props.defaultValue);
33
- const value = props.value ?? uncontrolled;
34
- const identity = useFieldIdentity({
35
- id,
36
- name: props.name,
37
- "data-field": fieldA11y["data-field"]
112
+ function LabelInValueSelect({
113
+ labelInValue,
114
+ value,
115
+ defaultValue,
116
+ onValueChange,
117
+ onSelect,
118
+ ...rest
119
+ }) {
120
+ void labelInValue;
121
+ const multiple = rest.mode === "multiple" || rest.mode === "tags";
122
+ const onDeselect = rest.onDeselect;
123
+ const entries = React.useMemo(() => {
124
+ const current = value ?? defaultValue;
125
+ if (current == null) return [];
126
+ return Array.isArray(current) ? current : [current];
127
+ }, [value, defaultValue]);
128
+ const options = React.useMemo(() => {
129
+ const base = normalizeSelectOptions(rest.options, rest.fieldNames);
130
+ const known = new Set(base.map((option) => option.value));
131
+ const carried = entries.filter((entry) => entry && !known.has(entry.value)).map((entry) => ({ value: entry.value, label: itemTextValue(entry.label) || entry.value }));
132
+ return carried.length ? [...base, ...carried] : base;
133
+ }, [rest.options, rest.fieldNames, entries]);
134
+ const toLabeled = (picked, option) => ({
135
+ value: picked,
136
+ // The OPTION's label wins when the row is known; the label that rode in on the value is the
137
+ // fallback for the row that has not loaded.
138
+ label: option?.label ?? entries.find((entry) => entry.value === picked)?.label ?? picked
38
139
  });
140
+ const shared = {
141
+ ...rest,
142
+ options,
143
+ fieldNames: void 0,
144
+ onSelect: onSelect ? (picked, option) => onSelect(
145
+ toLabeled(picked, option),
146
+ option
147
+ ) : void 0
148
+ };
149
+ if (multiple) {
150
+ const labeledValues = value;
151
+ const labeledDefaults = defaultValue;
152
+ const adapted2 = {
153
+ ...shared,
154
+ value: labeledValues?.map((entry) => entry.value),
155
+ defaultValue: labeledDefaults?.map((entry) => entry.value),
156
+ onValueChange: (picked, pickedOptions) => onValueChange?.(
157
+ picked.map(
158
+ (entry) => toLabeled(
159
+ entry,
160
+ pickedOptions?.find((option) => option.value === entry)
161
+ )
162
+ ),
163
+ pickedOptions
164
+ ),
165
+ onDeselect: onDeselect ? (picked, option) => onDeselect(toLabeled(picked, option), option) : void 0
166
+ };
167
+ return /* @__PURE__ */ jsx(DataSelect, { ...adapted2 });
168
+ }
169
+ const labeledValue = value;
170
+ const labeledDefault = defaultValue;
171
+ const adapted = {
172
+ ...shared,
173
+ value: value === void 0 ? void 0 : labeledValue?.value ?? "",
174
+ defaultValue: labeledDefault?.value,
175
+ onValueChange: (picked, option) => onValueChange?.(picked ? toLabeled(picked, option) : void 0, option)
176
+ };
177
+ return /* @__PURE__ */ jsx(DataSelect, { ...adapted });
178
+ }
179
+ function CompoundSelect({ id, name, ...props }) {
180
+ const fieldA11y = pickFieldA11y(props);
181
+ const identity = useFieldIdentity({ id, name, "data-field": fieldA11y["data-field"] });
39
182
  return /* @__PURE__ */ jsx(
40
183
  SelectFieldA11yContext.Provider,
41
184
  {
42
185
  value: {
43
186
  ...fieldA11y,
44
187
  "data-field": identity["data-field"] ?? fieldA11y["data-field"],
45
- id,
46
- value
188
+ id
47
189
  },
48
190
  children: /* @__PURE__ */ jsx(
49
- SelectPrimitive.Root,
191
+ SelectRoot,
50
192
  {
51
- "data-slot": "select",
52
- ...rootProps,
53
- name: props.name ?? identity.name,
54
- onValueChange: (next) => {
55
- setUncontrolled(next);
56
- props.onValueChange?.(next);
57
- }
193
+ ...props,
194
+ id,
195
+ name: name ?? identity.name,
196
+ "aria-label": fieldA11y["aria-label"],
197
+ "aria-labelledby": fieldA11y["aria-labelledby"]
58
198
  }
59
199
  )
60
200
  }
61
201
  );
62
202
  }
63
- function SelectGroup(props) {
64
- return /* @__PURE__ */ jsx(SelectPrimitive.Group, { "data-slot": "select-group", ...props });
65
- }
66
- function SelectValue(props) {
67
- return /* @__PURE__ */ jsx(SelectPrimitive.Value, { "data-slot": "select-value", ...props });
203
+ function SelectGroup({ className, children, ...props }) {
204
+ return /* @__PURE__ */ jsx(
205
+ ListBoxSection,
206
+ {
207
+ "data-slot": "select-group",
208
+ className,
209
+ ...splitDomProps(props).attributes,
210
+ children
211
+ }
212
+ );
68
213
  }
214
+ const SelectValue = React.forwardRef(function SelectValue2({ placeholder, children, asChild, className, style, ...props }, ref) {
215
+ void asChild;
216
+ return /* @__PURE__ */ jsx(
217
+ AriaSelectValue,
218
+ {
219
+ ref,
220
+ "data-slot": "select-value",
221
+ className: className ?? "",
222
+ style,
223
+ ...props,
224
+ children: ({ isPlaceholder, defaultChildren }) => isPlaceholder ? placeholder ?? null : children ?? defaultChildren
225
+ }
226
+ );
227
+ });
69
228
  const SelectTrigger = React.forwardRef(
70
229
  ({
71
230
  className,
@@ -75,25 +234,28 @@ const SelectTrigger = React.forwardRef(
75
234
  status,
76
235
  width = "full",
77
236
  showIndicator = true,
237
+ disabled,
238
+ asChild,
239
+ style,
78
240
  ...props
79
241
  }, ref) => {
242
+ void asChild;
80
243
  const field = React.useContext(SelectFieldA11yContext);
244
+ const state = React.useContext(SelectStateContext);
245
+ const ariaTrigger = React.useContext(ButtonContext);
81
246
  const ownsName = props["aria-label"] !== void 0 || props["aria-labelledby"] !== void 0;
82
247
  const fieldOwnsName = field?.["aria-label"] !== void 0 || field?.["aria-labelledby"] !== void 0;
83
248
  const nameFallback = useFieldNameFallback({
84
249
  "aria-label": ownsName ? props["aria-label"] : field?.["aria-label"],
85
250
  "aria-labelledby": ownsName ? props["aria-labelledby"] : field?.["aria-labelledby"]
86
251
  });
87
- const fieldA11y = field || nameFallback["aria-labelledby"] !== void 0 ? {
88
- id: props.id ?? field?.id,
252
+ const hasField = Boolean(field) || nameFallback["aria-labelledby"] !== void 0;
253
+ const fieldA11y = hasField ? {
89
254
  ...ownsName ? {} : fieldOwnsName ? {
90
255
  "aria-label": field?.["aria-label"],
91
256
  "aria-labelledby": field?.["aria-labelledby"]
92
257
  } : nameFallback,
93
- "aria-describedby": mergeAriaIds(
94
- props["aria-describedby"],
95
- field?.["aria-describedby"]
96
- ),
258
+ "aria-describedby": mergeAriaIds(props["aria-describedby"], field?.["aria-describedby"]),
97
259
  "aria-errormessage": mergeAriaIds(
98
260
  props["aria-errormessage"],
99
261
  field?.["aria-errormessage"]
@@ -105,147 +267,277 @@ const SelectTrigger = React.forwardRef(
105
267
  ),
106
268
  "data-field": props["data-field"] ?? field?.["data-field"]
107
269
  } : { "aria-invalid": resolveAriaInvalid(props["aria-invalid"], status) };
108
- const dataValue = (props["data-value"] ?? field?.value) || void 0;
109
- return /* @__PURE__ */ jsxs(
110
- SelectPrimitive.Trigger,
270
+ const id = props.id ?? (hasField ? field?.id : void 0);
271
+ const isOpen = state?.isOpen ?? false;
272
+ const buttonRef = React.useRef(null);
273
+ const composedRef = React.useCallback(
274
+ (node) => {
275
+ buttonRef.current = node;
276
+ if (typeof ref === "function") ref(node);
277
+ else if (ref) ref.current = node;
278
+ },
279
+ [ref]
280
+ );
281
+ const wasOpen = React.useRef(false);
282
+ React.useEffect(() => {
283
+ if (!wasOpen.current || isOpen) {
284
+ wasOpen.current = isOpen;
285
+ return;
286
+ }
287
+ wasOpen.current = isOpen;
288
+ const button = buttonRef.current;
289
+ if (!button) return;
290
+ const restore = button.ownerDocument.defaultView?.setTimeout(() => {
291
+ const owner = button.ownerDocument;
292
+ if (!button.isConnected) return;
293
+ if (owner.activeElement === null || owner.activeElement === owner.body) button.focus();
294
+ }, 0);
295
+ return () => {
296
+ if (restore !== void 0) button.ownerDocument.defaultView?.clearTimeout(restore);
297
+ };
298
+ }, [isOpen]);
299
+ const selected = state ? state.value : null;
300
+ const hasValue = selected != null && selected !== "";
301
+ const dataValue = (props["data-value"] ?? (hasValue ? String(selected) : void 0)) || void 0;
302
+ const { events, attributes } = splitDomProps(props);
303
+ delete attributes.id;
304
+ const triggerContext = React.useMemo(() => {
305
+ if (!ariaTrigger || "slots" in ariaTrigger) return ariaTrigger;
306
+ const next = { ...ariaTrigger };
307
+ delete next["aria-labelledby"];
308
+ return next;
309
+ }, [ariaTrigger]);
310
+ return /* @__PURE__ */ jsx(ButtonContext.Provider, { value: triggerContext, children: /* @__PURE__ */ jsxs(
311
+ AriaButton,
111
312
  {
112
- ref,
113
- "data-slot": "select-trigger",
114
- "data-size": size,
115
- "data-variant": controlSurfaceAttrs({ variant })["data-variant"],
116
- "data-status": status,
117
- "data-width": width,
313
+ ref: composedRef,
314
+ id,
315
+ onPointerDown: (event) => {
316
+ event.currentTarget.focus();
317
+ },
318
+ ...events,
319
+ isDisabled: disabled || void 0,
320
+ style,
118
321
  className: cn(
119
322
  controlSurfaceTriggerClass,
120
323
  // `bounded` deliberately emits NO width utility: its width is owned by the
121
324
  // `[data-width="bounded"]` rule in control.css. A utility here would win the layer order
122
325
  // and make that rule — and therefore the token — dead, the way `w-full` did to
123
- // `.ui-app-setting-picker-icon` (gh#366, gh#371).
326
+ // `.ui-app-setting-picker-icon`'s `inline-size` (gh#366, gh#371).
124
327
  width === "auto" && "w-auto",
125
328
  width === "full" && "w-full",
126
329
  "aria-invalid:border-destructive data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground whitespace-nowrap transition-[color,box-shadow] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center",
127
330
  className
128
331
  ),
129
- ...props,
130
- ...fieldA11y,
131
- "data-value": dataValue,
332
+ render: (domProps) => /* @__PURE__ */ jsx(
333
+ "button",
334
+ {
335
+ ...domProps,
336
+ ...attributes,
337
+ ...fieldA11y,
338
+ role: "combobox",
339
+ "aria-autocomplete": "none",
340
+ "data-slot": "select-trigger",
341
+ "data-size": size,
342
+ "data-variant": controlSurfaceAttrs({ variant })["data-variant"],
343
+ "data-status": status,
344
+ "data-width": width,
345
+ "data-state": isOpen ? "open" : "closed",
346
+ "data-placeholder": hasValue ? void 0 : "",
347
+ "data-value": dataValue
348
+ }
349
+ ),
132
350
  children: [
133
351
  children,
134
- showIndicator ? /* @__PURE__ */ jsx(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ jsx(
352
+ showIndicator ? /* @__PURE__ */ jsx(
135
353
  ChevronDown,
136
354
  {
137
355
  "data-slot": "select-chevron",
138
356
  className: "ui-select-chevron",
139
357
  "aria-hidden": "true"
140
358
  }
141
- ) }) : null
359
+ ) : null
142
360
  ]
143
361
  }
144
- );
362
+ ) });
145
363
  }
146
364
  );
147
- SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
148
- const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
149
- SelectPrimitive.ScrollUpButton,
150
- {
151
- ref,
152
- "data-slot": "select-scroll-up-button",
153
- className: cn("ui-select-scroll-button", className),
154
- ...props,
155
- children: /* @__PURE__ */ jsx(ChevronUp, { className: "ui-select-scroll-icon", "aria-hidden": "true" })
365
+ SelectTrigger.displayName = "SelectTrigger";
366
+ const SelectScrollUpButton = React.forwardRef(
367
+ function SelectScrollUpButton2(props, ref) {
368
+ void props;
369
+ void ref;
370
+ return null;
156
371
  }
157
- ));
158
- SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
159
- const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
160
- SelectPrimitive.ScrollDownButton,
161
- {
162
- ref,
163
- "data-slot": "select-scroll-down-button",
164
- className: cn("ui-select-scroll-button", className),
165
- ...props,
166
- children: /* @__PURE__ */ jsx(ChevronDown, { className: "ui-select-scroll-icon", "aria-hidden": "true" })
372
+ );
373
+ const SelectScrollDownButton = React.forwardRef(
374
+ function SelectScrollDownButton2(props, ref) {
375
+ void props;
376
+ void ref;
377
+ return null;
167
378
  }
168
- ));
169
- SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
170
- const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => {
171
- const contentRef = useInertHiddenBackground(ref);
172
- return /* @__PURE__ */ jsx(SelectPrimitive.Portal, { children: /* @__PURE__ */ jsxs(
173
- SelectPrimitive.Content,
379
+ );
380
+ const SELECT_PLACEMENT = {
381
+ bottomStart: { side: "bottom", align: "start" },
382
+ bottomEnd: { side: "bottom", align: "end" },
383
+ topStart: { side: "top", align: "start" },
384
+ topEnd: { side: "top", align: "end" }
385
+ };
386
+ const SelectPopup = React.forwardRef(function SelectPopup2({
387
+ className,
388
+ children,
389
+ style,
390
+ side,
391
+ sideOffset = 4,
392
+ align = "start",
393
+ alignOffset,
394
+ avoidCollisions,
395
+ collisionPadding,
396
+ position,
397
+ sticky,
398
+ hideWhenDetached,
399
+ onCloseAutoFocus,
400
+ onEscapeKeyDown,
401
+ onPointerDownOutside,
402
+ forceMount,
403
+ renderEmpty,
404
+ placement,
405
+ popupRender,
406
+ listHeight,
407
+ onPopupScroll,
408
+ "data-popup-match": popupMatch,
409
+ ...props
410
+ }, ref) {
411
+ void position;
412
+ void sticky;
413
+ void hideWhenDetached;
414
+ void onCloseAutoFocus;
415
+ void onEscapeKeyDown;
416
+ void onPointerDownOutside;
417
+ void forceMount;
418
+ const portalContainer = useOverlayPortalContainer();
419
+ const { events, attributes } = splitDomProps(props);
420
+ const anchor = placement ? SELECT_PLACEMENT[placement] : void 0;
421
+ const list = /* @__PURE__ */ jsx(
422
+ ListBox,
174
423
  {
175
- ref: contentRef,
176
- "data-slot": "select-content",
424
+ "data-slot": "select-viewport",
425
+ className: "ui-select-viewport",
426
+ ...events,
427
+ onScroll: onPopupScroll,
428
+ renderEmptyState: renderEmpty === void 0 ? void 0 : () => /* @__PURE__ */ jsx("div", { "data-slot": "select-empty", className: "ui-select-empty", children: renderEmpty }),
429
+ children
430
+ }
431
+ );
432
+ return /* @__PURE__ */ jsx(
433
+ AriaPopover,
434
+ {
435
+ ref,
436
+ UNSTABLE_portalContainer: portalContainer,
437
+ placement: toPlacement(side ?? anchor?.side, align ?? anchor?.align),
438
+ offset: sideOffset,
439
+ crossOffset: alignOffset,
440
+ shouldFlip: avoidCollisions,
441
+ containerPadding: collisionPadding,
442
+ style: listHeight === void 0 ? style : { ...style, "--select-content-max-height": `${listHeight}px` },
177
443
  className: cn(
178
444
  "ui-select-content 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",
179
- position === "popper" && "translate-y-1",
180
445
  className
181
446
  ),
182
- position,
183
- ...props,
184
- children: [
185
- /* @__PURE__ */ jsx(SelectScrollUpButton, {}),
447
+ render: (popoverProps, popoverState) => (
448
+ // `role={undefined}`: a listbox is not a dialog — see the file note (3).
186
449
  /* @__PURE__ */ jsx(
187
- SelectPrimitive.Viewport,
450
+ "div",
188
451
  {
189
- "data-slot": "select-viewport",
190
- className: cn(
191
- "ui-select-viewport",
192
- position === "popper" && "h-[var(--radix-select-trigger-height)] w-full",
193
- // The trigger-width floor is a UTILITY, so no `[data-popup-match]` rule in control.css
194
- // could ever lift it (gh#366). It is therefore WITHHELD whenever the consumer stated a
195
- // width of their own — which is exactly what antd's `popupMatchSelectWidth` is for.
196
- position === "popper" && !props["data-popup-match"] && "min-w-[var(--radix-select-trigger-width)]"
197
- ),
198
- children
452
+ ...popoverProps,
453
+ ...attributes,
454
+ role: void 0,
455
+ ...radixSurfaceState(popoverState),
456
+ "data-slot": "select-content",
457
+ "data-popup-match": popupMatch
199
458
  }
200
- ),
201
- /* @__PURE__ */ jsx(SelectScrollDownButton, {})
202
- ]
459
+ )
460
+ ),
461
+ children: popupRender ? popupRender(list) : list
203
462
  }
204
- ) });
463
+ );
205
464
  });
206
- SelectContent.displayName = SelectPrimitive.Content.displayName;
207
- const SelectLabel = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
208
- SelectPrimitive.Label,
209
- {
210
- ref,
211
- "data-slot": "select-label",
212
- className: cn("ui-select-label", className),
213
- ...props
465
+ const SelectContent = React.forwardRef(
466
+ function SelectContent2(props, ref) {
467
+ return /* @__PURE__ */ jsx(SelectPopup, { ref, ...props });
214
468
  }
215
- ));
216
- SelectLabel.displayName = SelectPrimitive.Label.displayName;
217
- const SelectItem = React.forwardRef(({ className, children, selectedIcon, ...props }, ref) => /* @__PURE__ */ jsxs(
218
- SelectPrimitive.Item,
219
- {
220
- ref,
221
- "data-slot": "select-item",
222
- className: cn("ui-select-item [&_svg:not([class*='text-'])]:text-muted-foreground", className),
223
- ...props,
224
- children: [
225
- /* @__PURE__ */ jsx(SelectPrimitive.ItemText, { children }),
226
- selectedIcon ? /* @__PURE__ */ jsx(SelectPrimitive.ItemIndicator, { asChild: true, children: /* @__PURE__ */ jsx(
227
- "span",
228
- {
229
- "data-slot": "select-item-selected-icon",
230
- className: "ui-search-select-selected-icon",
231
- "aria-hidden": "true",
232
- children: selectedIcon
233
- }
234
- ) }) : null
235
- ]
469
+ );
470
+ const SelectLabel = React.forwardRef(
471
+ function SelectLabel2({ className, ...props }, ref) {
472
+ return /* @__PURE__ */ jsx(
473
+ AriaHeader,
474
+ {
475
+ ref,
476
+ "data-slot": "select-label",
477
+ className: cn("ui-select-label", className),
478
+ ...props
479
+ }
480
+ );
236
481
  }
237
- ));
238
- SelectItem.displayName = SelectPrimitive.Item.displayName;
239
- const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
240
- SelectPrimitive.Separator,
241
- {
242
- ref,
243
- "data-slot": "select-separator",
244
- className: cn("ui-select-separator", className),
245
- ...props
482
+ );
483
+ const SelectItem = React.forwardRef(function SelectItem2({ className, children, selectedIcon, value, disabled, textValue, asChild, ...props }, ref) {
484
+ void asChild;
485
+ const { events, attributes } = splitDomProps(props);
486
+ return /* @__PURE__ */ jsx(
487
+ ListBoxItem,
488
+ {
489
+ ref,
490
+ id: value,
491
+ textValue: textValue ?? (itemTextValue(children) || value),
492
+ isDisabled: disabled,
493
+ ...events,
494
+ className: cn(
495
+ "ui-select-item [&_svg:not([class*='text-'])]:text-muted-foreground",
496
+ className
497
+ ),
498
+ render: (itemProps, itemState) => (
499
+ // The Radix `data-*` vocabulary control.css reads (`[data-state="checked"]`,
500
+ // `[data-highlighted]`), translated from react-aria's state.
501
+ /* @__PURE__ */ jsx(
502
+ "div",
503
+ {
504
+ ...itemProps,
505
+ ...attributes,
506
+ "data-slot": "select-item",
507
+ "data-state": itemState.isSelected ? "checked" : "unchecked",
508
+ "data-highlighted": itemState.isFocused ? "" : void 0,
509
+ "data-disabled": itemState.isDisabled ? "" : void 0
510
+ }
511
+ )
512
+ ),
513
+ children: ({ isSelected }) => /* @__PURE__ */ jsxs(Fragment, { children: [
514
+ children,
515
+ selectedIcon && isSelected ? /* @__PURE__ */ jsx(
516
+ "span",
517
+ {
518
+ "data-slot": "select-item-selected-icon",
519
+ className: "ui-search-select-selected-icon",
520
+ "aria-hidden": "true",
521
+ children: selectedIcon
522
+ }
523
+ ) : null
524
+ ] })
525
+ }
526
+ );
527
+ });
528
+ const SelectSeparator = React.forwardRef(
529
+ function SelectSeparator2({ className, ...props }, ref) {
530
+ return /* @__PURE__ */ jsx(
531
+ AriaSeparator,
532
+ {
533
+ ref,
534
+ "data-slot": "select-separator",
535
+ className: cn("ui-select-separator", className),
536
+ ...splitDomProps(props).attributes
537
+ }
538
+ );
246
539
  }
247
- ));
248
- SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
540
+ );
249
541
  function groupDataOptions(options) {
250
542
  const order = [];
251
543
  const buckets = /* @__PURE__ */ new Map();
@@ -259,11 +551,19 @@ function groupDataOptions(options) {
259
551
  }
260
552
  return order.map((key) => ({ heading: key || void 0, items: buckets.get(key) ?? [] }));
261
553
  }
554
+ function DataSelectLabel({
555
+ labelRender,
556
+ options
557
+ }) {
558
+ const state = React.useContext(SelectStateContext);
559
+ const selected = state ? state.value : null;
560
+ if (selected == null || selected === "") return null;
561
+ const value = String(selected);
562
+ const option = options.find((entry) => entry.value === value);
563
+ return /* @__PURE__ */ jsx(Fragment, { children: labelRender({ value, label: option?.label ?? value, option }) });
564
+ }
262
565
  function DataSelect(props) {
263
566
  const { t } = useTranslation();
264
- const [uncontrolledValue, setUncontrolledValue] = React.useState(
265
- typeof props.defaultValue === "string" ? props.defaultValue : void 0
266
- );
267
567
  const identity = useFieldIdentity({
268
568
  id: props.id,
269
569
  name: props.name,
@@ -271,15 +571,33 @@ function DataSelect(props) {
271
571
  });
272
572
  const resolvedName = props.name ?? identity.name;
273
573
  const resolvedField = props["data-field"] ?? identity["data-field"];
274
- const options = props.options ?? [];
574
+ const options = React.useMemo(
575
+ () => normalizeSelectOptions(props.options, props.fieldNames),
576
+ [props.options, props.fieldNames]
577
+ );
275
578
  const hasOptions = options.length > 0;
276
- const searchable = props.showSearch ?? (Boolean(props.loadOptions) || props.mode === "multiple");
277
- if (props.mode === "multiple" || searchable) {
579
+ const showSearchConfig = typeof props.showSearch === "object" && props.showSearch !== null ? props.showSearch : void 0;
580
+ const showSearch = showSearchConfig ? true : props.showSearch;
581
+ const searchable = showSearch ?? (Boolean(props.loadOptions) || props.mode !== void 0);
582
+ if (props.mode !== void 0 || searchable) {
583
+ const antdFilter = showSearchConfig?.filterOption;
584
+ const filterOption = antdFilter === void 0 ? props.filterOption : antdFilter === false ? () => true : antdFilter === true ? void 0 : (option, query) => antdFilter(query, option);
585
+ const onSearch = showSearchConfig?.onSearch;
278
586
  return /* @__PURE__ */ jsx(
279
587
  SearchSelect,
280
588
  {
281
589
  ...props,
282
590
  options,
591
+ fieldNames: void 0,
592
+ filterOption,
593
+ filterSort: showSearchConfig?.filterSort ?? props.filterSort,
594
+ optionFilterProp: showSearchConfig?.optionFilterProp ?? props.optionFilterProp,
595
+ search: showSearchConfig?.searchValue ?? props.search,
596
+ onSearchChange: onSearch || props.onSearchChange ? (query) => {
597
+ onSearch?.(query);
598
+ props.onSearchChange?.(query);
599
+ } : void 0,
600
+ autoClearSearchValue: showSearchConfig?.autoClearSearchValue ?? props.autoClearSearchValue,
283
601
  disabled: props.disabled || !props.loadOptions && !hasOptions,
284
602
  name: resolvedName,
285
603
  "data-field": resolvedField
@@ -310,12 +628,18 @@ function DataSelect(props) {
310
628
  value,
311
629
  defaultValue,
312
630
  onValueChange,
631
+ labelRender,
632
+ prefix,
633
+ suffixIcon,
634
+ placement,
635
+ popupRender,
636
+ listHeight,
637
+ onPopupScroll,
313
638
  id,
314
639
  className,
315
640
  "data-testid": dataTestId,
316
641
  ...rest
317
642
  } = props;
318
- const currentValue = value ?? uncontrolledValue;
319
643
  const ariaProps = Object.fromEntries(
320
644
  Object.entries(rest).filter(([key]) => key.startsWith("aria-"))
321
645
  );
@@ -327,6 +651,7 @@ function DataSelect(props) {
327
651
  SelectItem,
328
652
  {
329
653
  value: option.value,
654
+ textValue: option.label,
330
655
  disabled: option.disabled,
331
656
  "data-testid": optionTestId(option.value),
332
657
  selectedIcon: menuItemSelectedIcon,
@@ -345,61 +670,68 @@ function DataSelect(props) {
345
670
  const showClear = canClear && Boolean(value);
346
671
  const showEmptyPopup = !hasOptions && notFoundContent !== void 0;
347
672
  const select = /* @__PURE__ */ jsxs(
348
- SelectPrimitive.Root,
673
+ SelectRoot,
349
674
  {
350
- "data-slot": "select",
351
675
  value: isControlled ? value : void 0,
352
676
  defaultValue: isControlled ? void 0 : defaultValue || void 0,
353
677
  onValueChange: (next) => {
354
- setUncontrolledValue(next);
355
678
  onValueChange?.(
356
679
  next,
357
680
  options.find((option) => option.value === next)
358
681
  );
359
682
  },
360
683
  disabled: disabled || !hasOptions && !showEmptyPopup,
684
+ allowsEmptyCollection: showEmptyPopup,
685
+ "aria-label": ariaProps["aria-label"],
686
+ "aria-labelledby": ariaProps["aria-labelledby"],
361
687
  name: resolvedName,
362
688
  open,
363
689
  defaultOpen,
364
690
  onOpenChange,
365
691
  children: [
366
- /* @__PURE__ */ jsx(
692
+ /* @__PURE__ */ jsxs(
367
693
  SelectTrigger,
368
694
  {
369
695
  id,
370
696
  "data-testid": dataTestId,
371
697
  "data-field": resolvedField,
372
- "data-value": currentValue || void 0,
373
698
  size,
374
699
  variant,
375
700
  status,
376
701
  width,
377
702
  "aria-busy": loading || void 0,
378
703
  className: cn(
704
+ prefix === void 0 || prefix === null ? void 0 : "ui-select-trigger-prefixed",
379
705
  (showClear || loading) && "ui-control-trigger-affixed",
380
706
  canClear || loading ? void 0 : className
381
707
  ),
382
- showIndicator: !showClear && !loading,
708
+ showIndicator: suffixIcon === void 0 && !showClear && !loading,
383
709
  ...ariaProps,
384
- children: /* @__PURE__ */ jsx(SelectValue, { placeholder })
710
+ children: [
711
+ prefix === void 0 || prefix === null ? null : /* @__PURE__ */ jsx("span", { "data-slot": "select-prefix", className: "ui-select-prefix", children: prefix }),
712
+ /* @__PURE__ */ jsx(SelectValue, { placeholder, children: labelRender ? /* @__PURE__ */ jsx(DataSelectLabel, { labelRender, options }) : void 0 }),
713
+ suffixIcon === void 0 || suffixIcon === null || showClear || loading ? null : /* @__PURE__ */ jsx("span", { "data-slot": "select-suffix", className: "ui-select-suffix", "aria-hidden": "true", children: suffixIcon })
714
+ ]
385
715
  }
386
716
  ),
387
- /* @__PURE__ */ jsxs(
388
- SelectContent,
717
+ /* @__PURE__ */ jsx(
718
+ SelectPopup,
389
719
  {
720
+ renderEmpty: showEmptyPopup ? notFoundContent : void 0,
721
+ placement,
722
+ popupRender,
723
+ listHeight,
724
+ onPopupScroll,
390
725
  "data-popup-match": popupMatchSelectWidth === void 0 || popupMatchSelectWidth === true ? void 0 : popupMatchSelectWidth === false ? "content" : "fixed",
391
726
  style: typeof popupMatchSelectWidth === "number" ? {
392
727
  "--select-content-inline-size": `${popupMatchSelectWidth}px`
393
728
  } : void 0,
394
- children: [
395
- showEmptyPopup ? /* @__PURE__ */ jsx("div", { "data-slot": "select-empty", className: "ui-select-empty", children: notFoundContent }) : null,
396
- groupDataOptions(options).map(
397
- (group) => group.heading ? /* @__PURE__ */ jsxs(SelectGroup, { children: [
398
- /* @__PURE__ */ jsx(SelectLabel, { children: group.heading }),
399
- group.items.map(renderItem)
400
- ] }, group.heading) : /* @__PURE__ */ jsx(React.Fragment, { children: group.items.map(renderItem) }, "__ungrouped")
401
- )
402
- ]
729
+ children: groupDataOptions(options).map(
730
+ (group) => group.heading ? /* @__PURE__ */ jsxs(SelectGroup, { children: [
731
+ /* @__PURE__ */ jsx(SelectLabel, { children: group.heading }),
732
+ group.items.map(renderItem)
733
+ ] }, group.heading) : /* @__PURE__ */ jsx(React.Fragment, { children: group.items.map(renderItem) }, "__ungrouped")
734
+ )
403
735
  }
404
736
  )
405
737
  ]