@godxjp/ui 21.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 (154) hide show
  1. package/README.md +4 -4
  2. package/dist/app/app-provider.js +2 -1
  3. package/dist/components/data-display/badge.d.ts +28 -1
  4. package/dist/components/data-display/badge.js +2 -0
  5. package/dist/components/data-display/data-table.d.ts +18 -2
  6. package/dist/components/data-display/data-table.js +4 -0
  7. package/dist/components/data-display/index.d.ts +1 -1
  8. package/dist/components/data-display/popover.d.ts +19 -1
  9. package/dist/components/data-display/popover.js +9 -1
  10. package/dist/components/data-display/progress.d.ts +37 -0
  11. package/dist/components/data-display/progress.js +57 -5
  12. package/dist/components/data-display/scroll-area.d.ts +24 -4
  13. package/dist/components/data-display/scroll-area.js +34 -72
  14. package/dist/components/data-entry/cascader.js +8 -6
  15. package/dist/components/data-entry/checkbox.js +2 -0
  16. package/dist/components/data-entry/choice-hit-target.d.ts +24 -0
  17. package/dist/components/data-entry/choice-hit-target.js +12 -0
  18. package/dist/components/data-entry/control-surface.d.ts +8 -0
  19. package/dist/components/data-entry/control-surface.js +10 -1
  20. package/dist/components/data-entry/date-picker.d.ts +10 -3
  21. package/dist/components/data-entry/date-picker.js +503 -238
  22. package/dist/components/data-entry/form-field.d.ts +1 -1
  23. package/dist/components/data-entry/form-field.js +7 -3
  24. package/dist/components/data-entry/form.js +10 -1
  25. package/dist/components/data-entry/index.d.ts +0 -6
  26. package/dist/components/data-entry/index.js +0 -6
  27. package/dist/components/data-entry/label.d.ts +14 -1
  28. package/dist/components/data-entry/label.js +6 -1
  29. package/dist/components/data-entry/radio.d.ts +27 -6
  30. package/dist/components/data-entry/radio.js +89 -43
  31. package/dist/components/data-entry/search-select.js +226 -89
  32. package/dist/components/data-entry/select.d.ts +76 -14
  33. package/dist/components/data-entry/select.js +497 -163
  34. package/dist/components/data-entry/slider.d.ts +3 -11
  35. package/dist/components/data-entry/slider.js +582 -93
  36. package/dist/components/data-entry/switch.d.ts +6 -3
  37. package/dist/components/data-entry/switch.js +36 -11
  38. package/dist/components/layout/mobile-shell.d.ts +1 -1
  39. package/dist/components/layout/mobile-shell.js +2 -0
  40. package/dist/components/navigation/dropdown-menu.d.ts +11 -3
  41. package/dist/components/navigation/dropdown-menu.js +190 -15
  42. package/dist/components/navigation/index.d.ts +0 -3
  43. package/dist/components/navigation/index.js +0 -78
  44. package/dist/components/navigation/steps.d.ts +1 -1
  45. package/dist/components/navigation/steps.js +3 -1
  46. package/dist/components/navigation/tabs-scroll.d.ts +27 -0
  47. package/dist/components/navigation/tabs-scroll.js +52 -1
  48. package/dist/components/navigation/tabs.d.ts +2 -2
  49. package/dist/components/navigation/tabs.js +88 -23
  50. package/dist/components/ui/index.d.ts +0 -4
  51. package/dist/components/ui/index.js +0 -4
  52. package/dist/components/ui/input-otp.d.ts +1 -1
  53. package/dist/components/ui/segmented.d.ts +15 -1
  54. package/dist/components/ui/segmented.js +15 -6
  55. package/dist/components/ui/tag-input.js +3 -5
  56. package/dist/i18n/messages/en.json +3 -2
  57. package/dist/i18n/messages/ja.json +3 -2
  58. package/dist/i18n/messages/vi.json +3 -2
  59. package/dist/lib/datetime/picker-format.d.ts +18 -0
  60. package/dist/lib/datetime/picker-format.js +35 -1
  61. package/dist/lib/field-a11y.d.ts +12 -0
  62. package/dist/lib/field-a11y.js +14 -1
  63. package/dist/lib/select-options.d.ts +21 -0
  64. package/dist/lib/select-options.js +46 -0
  65. package/dist/props/components/data-display.prop.d.ts +16 -6
  66. package/dist/props/components/data-entry.prop.d.ts +503 -129
  67. package/dist/props/components/index.d.ts +2 -2
  68. package/dist/props/components/layout.prop.d.ts +12 -1
  69. package/dist/props/components/navigation.prop.d.ts +93 -1
  70. package/dist/props/registry.d.ts +79 -16
  71. package/dist/props/registry.js +85 -15
  72. package/dist/props/vocabulary/data.prop.d.ts +12 -0
  73. package/dist/props/vocabulary/index.d.ts +2 -2
  74. package/dist/props/vocabulary/layout.prop.d.ts +11 -0
  75. package/dist/styles/badge-layout.css +4 -0
  76. package/dist/styles/card-layout.css +6 -6
  77. package/dist/styles/control.css +220 -29
  78. package/dist/styles/data-display-layout.css +94 -23
  79. package/dist/styles/focus-ring.css +6 -4
  80. package/dist/styles/fonts.css +88 -4
  81. package/dist/styles/navigation-layout.css +36 -236
  82. package/dist/styles/shell-layout.css +149 -139
  83. package/dist/styles/table-layout.css +41 -3
  84. package/dist/tokens/components/control.css +4 -0
  85. package/dist/tokens/components/data-display.css +9 -5
  86. package/dist/tokens/components/navigation.css +9 -5
  87. package/dist/tokens/components/scroll-area.css +7 -0
  88. package/dist/tokens/components/segmented.css +0 -2
  89. package/dist/tokens/components/shell.css +2 -0
  90. package/dist/tokens/components/table.css +3 -0
  91. package/dist/tokens/foundation.css +7 -0
  92. package/docs/COMPONENTS.md +14 -14
  93. package/docs/CONSUMER-RULES.md +24 -1
  94. package/docs/CUSTOMER-THEMING.md +7 -1
  95. package/docs/FRAME-COVERAGE-REPORT.md +5 -11
  96. package/docs/TOKENS.md +65 -0
  97. package/docs/data-display/badge.tsx +37 -0
  98. package/docs/data-display/data-table/index.tsx +45 -1
  99. package/docs/data-display/popover.tsx +43 -5
  100. package/docs/data-display/progress.tsx +28 -0
  101. package/docs/data-display/scroll-area.tsx +51 -45
  102. package/docs/data-entry/calendar.tsx +2 -2
  103. package/docs/data-entry/date-picker.tsx +144 -5
  104. package/docs/data-entry/form/examples/invoice-form.tsx +2 -2
  105. package/docs/data-entry/form-field/examples/a11y-contract.tsx +3 -5
  106. package/docs/data-entry/form.tsx +4 -4
  107. package/docs/data-entry/input-otp.tsx +5 -2
  108. package/docs/data-entry/number-input.tsx +31 -0
  109. package/docs/data-entry/segmented.tsx +143 -2
  110. package/docs/data-entry/select-matrix.tsx +1 -2
  111. package/docs/data-entry/select.tsx +108 -3
  112. package/docs/data-entry/slider.tsx +125 -47
  113. package/docs/feedback/dialog.tsx +1 -2
  114. package/docs/feedback/sheet.tsx +1 -2
  115. package/docs/layout/app-shell-states.tsx +143 -0
  116. package/docs/layout/mobile-shell.tsx +5 -0
  117. package/docs/navigation/dropdown-menu.tsx +75 -4
  118. package/docs/navigation/tabs.tsx +149 -0
  119. package/docs/roadmap/antd-parity.md +35 -34
  120. package/docs/roadmap/parity-audit-data-entry.md +196 -172
  121. package/docs/roadmap/parity-audit-layout-navigation-general.md +304 -310
  122. package/docs/roadmap/parity-backlog.md +8 -10
  123. package/docs/showcase/acme-portal.tsx +7 -0
  124. package/package.json +28 -64
  125. package/scripts/ui-audit.mjs +18 -5
  126. package/scripts/visual-audit-rules.mjs +0 -7
  127. package/scripts/visual-audit.mjs +6 -27
  128. package/dist/components/data-entry/date-range-picker.d.ts +0 -11
  129. package/dist/components/data-entry/date-range-picker.js +0 -349
  130. package/dist/components/data-entry/month-picker.d.ts +0 -10
  131. package/dist/components/data-entry/month-picker.js +0 -241
  132. package/dist/components/data-entry/month-range-picker.d.ts +0 -10
  133. package/dist/components/data-entry/month-range-picker.js +0 -301
  134. package/dist/components/navigation/context-menu.d.ts +0 -21
  135. package/dist/components/navigation/context-menu.js +0 -149
  136. package/dist/components/navigation/menubar.d.ts +0 -21
  137. package/dist/components/navigation/menubar.js +0 -136
  138. package/dist/components/navigation/navigation-menu.d.ts +0 -10
  139. package/dist/components/navigation/navigation-menu.js +0 -91
  140. package/dist/components/ui/context-menu.d.ts +0 -1
  141. package/dist/components/ui/context-menu.js +0 -2
  142. package/dist/components/ui/date-range-picker.d.ts +0 -1
  143. package/dist/components/ui/date-range-picker.js +0 -2
  144. package/dist/components/ui/menubar.d.ts +0 -1
  145. package/dist/components/ui/menubar.js +0 -2
  146. package/dist/components/ui/navigation-menu.d.ts +0 -1
  147. package/dist/components/ui/navigation-menu.js +0 -2
  148. package/docs/FRAME-A11Y-CI.md +0 -349
  149. package/docs/data-entry/date-range-picker.tsx +0 -175
  150. package/docs/data-entry/month-picker.tsx +0 -101
  151. package/docs/data-entry/month-range-picker.tsx +0 -115
  152. package/docs/navigation/context-menu.tsx +0 -128
  153. package/docs/navigation/menubar.tsx +0 -141
  154. 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
@@ -298,6 +616,7 @@ function DataSelect(props) {
298
616
  size,
299
617
  status,
300
618
  variant,
619
+ width,
301
620
  loading,
302
621
  open,
303
622
  defaultOpen,
@@ -309,12 +628,18 @@ function DataSelect(props) {
309
628
  value,
310
629
  defaultValue,
311
630
  onValueChange,
631
+ labelRender,
632
+ prefix,
633
+ suffixIcon,
634
+ placement,
635
+ popupRender,
636
+ listHeight,
637
+ onPopupScroll,
312
638
  id,
313
639
  className,
314
640
  "data-testid": dataTestId,
315
641
  ...rest
316
642
  } = props;
317
- const currentValue = value ?? uncontrolledValue;
318
643
  const ariaProps = Object.fromEntries(
319
644
  Object.entries(rest).filter(([key]) => key.startsWith("aria-"))
320
645
  );
@@ -326,6 +651,7 @@ function DataSelect(props) {
326
651
  SelectItem,
327
652
  {
328
653
  value: option.value,
654
+ textValue: option.label,
329
655
  disabled: option.disabled,
330
656
  "data-testid": optionTestId(option.value),
331
657
  selectedIcon: menuItemSelectedIcon,
@@ -344,60 +670,68 @@ function DataSelect(props) {
344
670
  const showClear = canClear && Boolean(value);
345
671
  const showEmptyPopup = !hasOptions && notFoundContent !== void 0;
346
672
  const select = /* @__PURE__ */ jsxs(
347
- SelectPrimitive.Root,
673
+ SelectRoot,
348
674
  {
349
- "data-slot": "select",
350
675
  value: isControlled ? value : void 0,
351
676
  defaultValue: isControlled ? void 0 : defaultValue || void 0,
352
677
  onValueChange: (next) => {
353
- setUncontrolledValue(next);
354
678
  onValueChange?.(
355
679
  next,
356
680
  options.find((option) => option.value === next)
357
681
  );
358
682
  },
359
683
  disabled: disabled || !hasOptions && !showEmptyPopup,
684
+ allowsEmptyCollection: showEmptyPopup,
685
+ "aria-label": ariaProps["aria-label"],
686
+ "aria-labelledby": ariaProps["aria-labelledby"],
360
687
  name: resolvedName,
361
688
  open,
362
689
  defaultOpen,
363
690
  onOpenChange,
364
691
  children: [
365
- /* @__PURE__ */ jsx(
692
+ /* @__PURE__ */ jsxs(
366
693
  SelectTrigger,
367
694
  {
368
695
  id,
369
696
  "data-testid": dataTestId,
370
697
  "data-field": resolvedField,
371
- "data-value": currentValue || void 0,
372
698
  size,
373
699
  variant,
374
700
  status,
701
+ width,
375
702
  "aria-busy": loading || void 0,
376
703
  className: cn(
704
+ prefix === void 0 || prefix === null ? void 0 : "ui-select-trigger-prefixed",
377
705
  (showClear || loading) && "ui-control-trigger-affixed",
378
706
  canClear || loading ? void 0 : className
379
707
  ),
380
- showIndicator: !showClear && !loading,
708
+ showIndicator: suffixIcon === void 0 && !showClear && !loading,
381
709
  ...ariaProps,
382
- 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
+ ]
383
715
  }
384
716
  ),
385
- /* @__PURE__ */ jsxs(
386
- SelectContent,
717
+ /* @__PURE__ */ jsx(
718
+ SelectPopup,
387
719
  {
720
+ renderEmpty: showEmptyPopup ? notFoundContent : void 0,
721
+ placement,
722
+ popupRender,
723
+ listHeight,
724
+ onPopupScroll,
388
725
  "data-popup-match": popupMatchSelectWidth === void 0 || popupMatchSelectWidth === true ? void 0 : popupMatchSelectWidth === false ? "content" : "fixed",
389
726
  style: typeof popupMatchSelectWidth === "number" ? {
390
727
  "--select-content-inline-size": `${popupMatchSelectWidth}px`
391
728
  } : void 0,
392
- children: [
393
- showEmptyPopup ? /* @__PURE__ */ jsx("div", { "data-slot": "select-empty", className: "ui-select-empty", children: notFoundContent }) : null,
394
- groupDataOptions(options).map(
395
- (group) => group.heading ? /* @__PURE__ */ jsxs(SelectGroup, { children: [
396
- /* @__PURE__ */ jsx(SelectLabel, { children: group.heading }),
397
- group.items.map(renderItem)
398
- ] }, group.heading) : /* @__PURE__ */ jsx(React.Fragment, { children: group.items.map(renderItem) }, "__ungrouped")
399
- )
400
- ]
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
+ )
401
735
  }
402
736
  )
403
737
  ]