@godxjp/ui 22.0.0 → 23.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 +4 -4
- package/dist/app/date-format-labels.d.ts +14 -2
- package/dist/app/date-format-labels.js +1 -1
- package/dist/app/date-formats.d.ts +14 -2
- package/dist/app/date-formats.js +9 -2
- package/dist/components/data-display/data-table.d.ts +8 -1
- package/dist/components/data-display/data-table.js +9 -2
- package/dist/components/data-display/scroll-area.d.ts +24 -4
- package/dist/components/data-display/scroll-area.js +34 -72
- package/dist/components/data-entry/cascader.js +8 -6
- package/dist/components/data-entry/checkbox.js +3 -1
- package/dist/components/data-entry/choice-hit-target.d.ts +24 -0
- package/dist/components/data-entry/choice-hit-target.js +12 -0
- package/dist/components/data-entry/control-surface.d.ts +8 -0
- package/dist/components/data-entry/control-surface.js +10 -1
- package/dist/components/data-entry/form-field.js +5 -4
- package/dist/components/data-entry/input.js +1 -1
- package/dist/components/data-entry/number-input.js +27 -6
- package/dist/components/data-entry/radio.js +4 -3
- package/dist/components/data-entry/search-select.js +225 -95
- package/dist/components/data-entry/select.d.ts +76 -14
- package/dist/components/data-entry/select.js +495 -163
- package/dist/components/data-entry/slider.d.ts +3 -11
- package/dist/components/data-entry/slider.js +582 -93
- package/dist/components/data-entry/switch.js +3 -1
- package/dist/components/feedback/dialog.js +2 -2
- package/dist/components/feedback/sheet.js +2 -22
- package/dist/components/navigation/dropdown-menu.d.ts +11 -3
- package/dist/components/navigation/dropdown-menu.js +190 -15
- package/dist/components/navigation/filter-bar.js +2 -1
- package/dist/components/navigation/index.d.ts +0 -3
- package/dist/components/navigation/index.js +0 -78
- package/dist/components/navigation/tabs.js +16 -4
- package/dist/components/ui/index.d.ts +0 -3
- package/dist/components/ui/index.js +0 -3
- package/dist/components/ui/input-otp.d.ts +1 -1
- package/dist/components/ui/segmented.js +3 -1
- package/dist/components/ui/tag-input.js +3 -5
- package/dist/i18n/messages/en.json +5 -1
- package/dist/i18n/messages/ja.json +5 -1
- package/dist/i18n/messages/vi.json +5 -1
- package/dist/lib/breakpoint-token.d.ts +8 -0
- package/dist/lib/breakpoint-token.js +29 -0
- package/dist/lib/select-options.d.ts +21 -0
- package/dist/lib/select-options.js +46 -0
- package/dist/props/components/data-display.prop.d.ts +17 -7
- package/dist/props/components/data-entry.prop.d.ts +323 -30
- package/dist/props/components/index.d.ts +1 -1
- package/dist/props/components/navigation.prop.d.ts +25 -1
- package/dist/props/registry.d.ts +69 -1
- package/dist/props/registry.js +75 -0
- package/dist/props/vocabulary/data.prop.d.ts +5 -0
- package/dist/props/vocabulary/index.d.ts +1 -1
- package/dist/styles/control.css +246 -28
- package/dist/styles/data-display-layout.css +18 -10
- package/dist/styles/focus-ring.css +28 -0
- package/dist/styles/fonts.css +88 -4
- package/dist/styles/form-layout.css +4 -0
- package/dist/styles/layout.css +2 -1
- package/dist/styles/navigation-layout.css +24 -238
- package/dist/styles/shell-layout.css +148 -139
- package/dist/styles/table-layout.css +12 -5
- package/dist/tokens/components/control.css +12 -3
- package/dist/tokens/components/data-display.css +0 -5
- package/dist/tokens/components/navigation.css +5 -5
- package/dist/tokens/components/scroll-area.css +7 -0
- package/docs/CONSUMER-RULES.md +7 -2
- package/docs/CUSTOMER-THEMING.md +25 -9
- package/docs/DATETIME.md +11 -2
- package/docs/FRAME-COVERAGE-REPORT.md +4 -7
- package/docs/SPACING.md +35 -5
- package/docs/TOKENS.md +1 -1
- package/docs/data-display/data-table/index.tsx +44 -3
- package/docs/data-display/popover.tsx +4 -6
- package/docs/data-display/scroll-area.tsx +51 -45
- package/docs/data-display/table.tsx +1 -1
- package/docs/data-entry/date-picker.tsx +2 -3
- package/docs/data-entry/input-otp.tsx +5 -2
- package/docs/data-entry/segmented-in-filter-row.tsx +77 -0
- package/docs/data-entry/segmented.tsx +40 -1
- package/docs/data-entry/select-matrix.tsx +1 -2
- package/docs/data-entry/select.tsx +108 -3
- package/docs/data-entry/slider.tsx +125 -47
- package/docs/feedback/dialog.tsx +1 -2
- package/docs/feedback/sheet.tsx +1 -2
- package/docs/layout/app-shell-states.tsx +143 -0
- package/docs/navigation/dropdown-menu.tsx +77 -6
- package/docs/navigation/tabs.tsx +72 -0
- package/docs/roadmap/ai-chat-components.md +1 -2
- package/docs/roadmap/parity-audit-data-entry.md +44 -33
- package/docs/roadmap/parity-audit-layout-navigation-general.md +304 -310
- package/docs/roadmap/parity-backlog.md +2 -3
- package/docs/roadmap/tree-components.md +0 -1
- package/package.json +25 -55
- package/scripts/_agent-setup.mjs +18 -0
- package/scripts/guinea-pig-skill.md +12 -6
- package/scripts/ui-audit.mjs +185 -7
- package/scripts/visual-audit-rules.mjs +0 -7
- package/scripts/visual-audit.mjs +6 -27
- package/dist/components/navigation/context-menu.d.ts +0 -21
- package/dist/components/navigation/context-menu.js +0 -149
- package/dist/components/navigation/menubar.d.ts +0 -28
- package/dist/components/navigation/menubar.js +0 -136
- package/dist/components/navigation/navigation-menu.d.ts +0 -10
- package/dist/components/navigation/navigation-menu.js +0 -91
- package/dist/components/ui/context-menu.d.ts +0 -1
- package/dist/components/ui/context-menu.js +0 -2
- package/dist/components/ui/menubar.d.ts +0 -1
- package/dist/components/ui/menubar.js +0 -2
- package/dist/components/ui/navigation-menu.d.ts +0 -1
- package/dist/components/ui/navigation-menu.js +0 -2
- package/docs/FRAME-A11Y-CI.md +0 -349
- package/docs/navigation/context-menu.tsx +0 -128
- package/docs/navigation/menubar.tsx +0 -141
- package/docs/navigation/navigation-menu.tsx +0 -158
|
@@ -7,6 +7,7 @@ import { cn } from "../../lib/utils.js";
|
|
|
7
7
|
import { pickFieldA11y, useFieldIdentity, useMirroredInputAttributes } from "../../lib/field-a11y.js";
|
|
8
8
|
import { Field } from "./field.js";
|
|
9
9
|
import { choiceGroupClassName } from "./choice-option.js";
|
|
10
|
+
import { withOwnHitTarget } from "./choice-hit-target.js";
|
|
10
11
|
function checkedState(isSelected) {
|
|
11
12
|
return isSelected ? "checked" : "unchecked";
|
|
12
13
|
}
|
|
@@ -61,11 +62,11 @@ const RadioItem = React.forwardRef(
|
|
|
61
62
|
// records: react-aria's root is a `<label>`, which is `display:inline`, so without it the
|
|
62
63
|
// 16px dot collapses to nothing. `data-[invalid]` replaces `aria-invalid:` because
|
|
63
64
|
// react-aria puts `aria-invalid` on the `<input>` and `data-invalid` on this box.
|
|
64
|
-
"ui-radio data-[invalid]:border-destructive inline-flex shrink-0 items-center justify-center shadow-xs transition-shadow
|
|
65
|
+
"ui-radio data-[invalid]:border-destructive inline-flex shrink-0 items-center justify-center shadow-xs transition-shadow",
|
|
65
66
|
className
|
|
66
67
|
),
|
|
67
68
|
...props,
|
|
68
|
-
render: (domProps, state) => /* @__PURE__ */ jsx("label", { ...domProps, "data-state": checkedState(state.isSelected) }),
|
|
69
|
+
render: (domProps, state) => /* @__PURE__ */ jsx("label", { ...domProps, "data-state": checkedState(state.isSelected), children: withOwnHitTarget(domProps.children) }),
|
|
69
70
|
children: /* @__PURE__ */ jsx("span", { "data-slot": "radio-group-indicator", className: "ui-choice-indicator", children: /* @__PURE__ */ jsx(Circle, { className: "ui-radio-icon", "aria-hidden": "true" }) })
|
|
70
71
|
}
|
|
71
72
|
);
|
|
@@ -87,7 +88,7 @@ function RadioBarButton({
|
|
|
87
88
|
isDisabled: option.disabled,
|
|
88
89
|
"data-slot": "radio-button",
|
|
89
90
|
className: "ui-radio-button ui-focus-ring",
|
|
90
|
-
render: (domProps, state) => /* @__PURE__ */ jsx("label", { ...domProps, "data-state": checkedState(state.isSelected) }),
|
|
91
|
+
render: (domProps, state) => /* @__PURE__ */ jsx("label", { ...domProps, "data-state": checkedState(state.isSelected), children: withOwnHitTarget(domProps.children) }),
|
|
91
92
|
children: /* @__PURE__ */ jsx("span", { className: "ui-radio-button-label", children: option.label })
|
|
92
93
|
}
|
|
93
94
|
);
|
|
@@ -10,15 +10,24 @@ import {
|
|
|
10
10
|
applyMaxTagCount,
|
|
11
11
|
controlSurfaceAttrs,
|
|
12
12
|
resolveAllowClear,
|
|
13
|
-
resolveAriaInvalid
|
|
13
|
+
resolveAriaInvalid,
|
|
14
|
+
splitByTokenSeparators
|
|
14
15
|
} from "./control-surface.js";
|
|
16
|
+
import { normalizeSelectOptions } from "../../lib/select-options.js";
|
|
15
17
|
import { Popover, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
|
|
16
18
|
import { Command, CommandGroup } from "./command.js";
|
|
17
19
|
import { Input } from "./input.js";
|
|
18
20
|
const DEBOUNCE_MS = 250;
|
|
21
|
+
function PopupRender({
|
|
22
|
+
render,
|
|
23
|
+
children
|
|
24
|
+
}) {
|
|
25
|
+
return /* @__PURE__ */ jsx(Fragment, { children: render ? render(children) : children });
|
|
26
|
+
}
|
|
19
27
|
function SearchSelect(props) {
|
|
20
28
|
const {
|
|
21
|
-
options:
|
|
29
|
+
options: rawOptions,
|
|
30
|
+
fieldNames,
|
|
22
31
|
loadOptions,
|
|
23
32
|
renderOption,
|
|
24
33
|
labelRender,
|
|
@@ -44,11 +53,18 @@ function SearchSelect(props) {
|
|
|
44
53
|
search: searchProp,
|
|
45
54
|
onSearchChange,
|
|
46
55
|
filterOption,
|
|
56
|
+
optionFilterProp,
|
|
47
57
|
filterSort,
|
|
48
58
|
autoClearSearchValue = true,
|
|
49
59
|
optionRender,
|
|
50
60
|
menuItemSelectedIcon,
|
|
51
61
|
notFoundContent,
|
|
62
|
+
prefix,
|
|
63
|
+
suffixIcon,
|
|
64
|
+
placement,
|
|
65
|
+
popupRender,
|
|
66
|
+
listHeight,
|
|
67
|
+
onPopupScroll,
|
|
52
68
|
popupMatchSelectWidth = true,
|
|
53
69
|
allowClear,
|
|
54
70
|
onClear,
|
|
@@ -66,7 +82,15 @@ function SearchSelect(props) {
|
|
|
66
82
|
"aria-invalid": ariaInvalid,
|
|
67
83
|
"aria-required": ariaRequired
|
|
68
84
|
} = props;
|
|
69
|
-
const
|
|
85
|
+
const staticOptions = React.useMemo(
|
|
86
|
+
() => normalizeSelectOptions(rawOptions, fieldNames),
|
|
87
|
+
[rawOptions, fieldNames]
|
|
88
|
+
);
|
|
89
|
+
const multiple = props.mode === "multiple" || props.mode === "tags";
|
|
90
|
+
const tagsMode = props.mode === "tags";
|
|
91
|
+
const maxTagTextLength = props.mode !== void 0 ? props.maxTagTextLength : void 0;
|
|
92
|
+
const tokenSeparators = props.mode !== void 0 ? props.tokenSeparators : void 0;
|
|
93
|
+
const tagRender = props.mode !== void 0 ? props.tagRender : void 0;
|
|
70
94
|
const { t } = useTranslation();
|
|
71
95
|
const nameFallback = useFieldNameFallback({
|
|
72
96
|
"aria-label": ariaLabel,
|
|
@@ -111,7 +135,7 @@ function SearchSelect(props) {
|
|
|
111
135
|
{}
|
|
112
136
|
);
|
|
113
137
|
const [internalValue, setInternalValue] = React.useState(
|
|
114
|
-
props.defaultValue ?? (props.mode === "multiple" ? [] : "")
|
|
138
|
+
props.defaultValue ?? (props.mode === "multiple" || props.mode === "tags" ? [] : "")
|
|
115
139
|
);
|
|
116
140
|
const isControlled = props.value !== void 0;
|
|
117
141
|
const rawValue = isControlled ? props.value : internalValue;
|
|
@@ -127,14 +151,20 @@ function SearchSelect(props) {
|
|
|
127
151
|
() => loadOptions ?? (async ({ query: search }) => {
|
|
128
152
|
const needle = search.trim();
|
|
129
153
|
const list = staticOptions ?? [];
|
|
130
|
-
const matches = (option) =>
|
|
154
|
+
const matches = (option) => {
|
|
155
|
+
if (filterOption) return filterOption(option, needle);
|
|
156
|
+
const haystack = optionFilterProp ? [String(option[optionFilterProp] ?? "")] : [option.label, option.value];
|
|
157
|
+
return haystack.some(
|
|
158
|
+
(field) => String(field).toLowerCase().includes(needle.toLowerCase())
|
|
159
|
+
);
|
|
160
|
+
};
|
|
131
161
|
const kept = needle ? list.filter(matches) : list;
|
|
132
162
|
return {
|
|
133
163
|
options: filterSort ? [...kept].sort((a, b) => filterSort(a, b, { searchValue: needle })) : kept,
|
|
134
164
|
hasMore: false
|
|
135
165
|
};
|
|
136
166
|
}),
|
|
137
|
-
[loadOptions, staticOptions, filterOption, filterSort]
|
|
167
|
+
[loadOptions, staticOptions, filterOption, optionFilterProp, filterSort]
|
|
138
168
|
);
|
|
139
169
|
React.useEffect(() => {
|
|
140
170
|
const handle = window.setTimeout(() => setDebouncedQuery(query.trim()), DEBOUNCE_MS);
|
|
@@ -171,10 +201,24 @@ function SearchSelect(props) {
|
|
|
171
201
|
setActiveIndex(0);
|
|
172
202
|
void fetchPage(1, debouncedQuery, false);
|
|
173
203
|
}, [open, debouncedQuery, fetchPage]);
|
|
204
|
+
const displayOptions = React.useMemo(() => {
|
|
205
|
+
if (!tagsMode) return loaded;
|
|
206
|
+
const known = new Set(loaded.map((option) => option.value));
|
|
207
|
+
const extras = [];
|
|
208
|
+
for (const entry of values) {
|
|
209
|
+
if (known.has(entry)) continue;
|
|
210
|
+
known.add(entry);
|
|
211
|
+
extras.push(pickedOptions[entry] ?? { value: entry, label: entry });
|
|
212
|
+
}
|
|
213
|
+
const needle = query.trim();
|
|
214
|
+
const exists = known.has(needle) || loaded.some((option) => option.label === needle);
|
|
215
|
+
const create = needle && !exists ? [{ value: needle, label: needle }] : [];
|
|
216
|
+
return create.length || extras.length ? [...create, ...extras, ...loaded] : loaded;
|
|
217
|
+
}, [tagsMode, loaded, values, query, pickedOptions]);
|
|
174
218
|
const grouped = React.useMemo(() => {
|
|
175
219
|
const order = [];
|
|
176
220
|
const buckets = /* @__PURE__ */ new Map();
|
|
177
|
-
for (const option of
|
|
221
|
+
for (const option of displayOptions) {
|
|
178
222
|
const key = option.group ?? "";
|
|
179
223
|
if (!buckets.has(key)) {
|
|
180
224
|
buckets.set(key, []);
|
|
@@ -187,7 +231,7 @@ function SearchSelect(props) {
|
|
|
187
231
|
heading: key || void 0,
|
|
188
232
|
items: (buckets.get(key) ?? []).map((option) => ({ option, index: flatIndex++ }))
|
|
189
233
|
}));
|
|
190
|
-
}, [
|
|
234
|
+
}, [displayOptions]);
|
|
191
235
|
const flatOrdered = React.useMemo(
|
|
192
236
|
() => grouped.flatMap((group) => group.items.map((entry) => entry.option)),
|
|
193
237
|
[grouped]
|
|
@@ -197,9 +241,10 @@ function SearchSelect(props) {
|
|
|
197
241
|
const selectedOption = value ? optionFor(value) : null;
|
|
198
242
|
const currentLabel = value ? selectedOption?.label ?? selectedLabel ?? value : resolvedPlaceholder;
|
|
199
243
|
const currentIcon = value ? selectedOption?.icon ?? selectedIcon : null;
|
|
244
|
+
const chipLabel = (label) => maxTagTextLength !== void 0 && maxTagTextLength >= 0 && label.length > maxTagTextLength ? `${label.slice(0, maxTagTextLength)}\u2026` : label;
|
|
200
245
|
const selectedChips = multiple ? values.map((entry) => ({
|
|
201
246
|
value: entry,
|
|
202
|
-
label: optionFor(entry)?.label ?? entry
|
|
247
|
+
label: chipLabel(optionFor(entry)?.label ?? entry)
|
|
203
248
|
})) : [];
|
|
204
249
|
const {
|
|
205
250
|
visible: visibleChips,
|
|
@@ -207,15 +252,15 @@ function SearchSelect(props) {
|
|
|
207
252
|
overflow: chipOverflow
|
|
208
253
|
} = applyMaxTagCount(
|
|
209
254
|
selectedChips,
|
|
210
|
-
props.mode
|
|
211
|
-
props.mode
|
|
255
|
+
props.mode !== void 0 ? props.maxTagCount : void 0,
|
|
256
|
+
props.mode !== void 0 ? props.maxTagPlaceholder : void 0
|
|
212
257
|
);
|
|
213
|
-
const maxCount = props.mode
|
|
258
|
+
const maxCount = props.mode !== void 0 ? props.maxCount : void 0;
|
|
214
259
|
const atMaxCount = maxCount !== void 0 && values.length >= maxCount;
|
|
215
260
|
const remember = (option) => setPickedOptions((prev) => ({ ...prev, [option.value]: option }));
|
|
216
261
|
const select = (option) => {
|
|
217
262
|
if (option.disabled) return;
|
|
218
|
-
if (props.mode
|
|
263
|
+
if (props.mode !== void 0) {
|
|
219
264
|
const already = values.includes(option.value);
|
|
220
265
|
if (!already && atMaxCount) return;
|
|
221
266
|
const next = already ? values.filter((entry) => entry !== option.value) : [...values, option.value];
|
|
@@ -237,8 +282,35 @@ function SearchSelect(props) {
|
|
|
237
282
|
if (autoClearSearchValue) setQuery("");
|
|
238
283
|
setOpen(false);
|
|
239
284
|
};
|
|
285
|
+
const selectTokens = (text) => {
|
|
286
|
+
if (props.mode === void 0) return;
|
|
287
|
+
const next = [...values];
|
|
288
|
+
const picked = [];
|
|
289
|
+
for (const raw of splitByTokenSeparators(text, tokenSeparators ?? [])) {
|
|
290
|
+
const token = raw.trim();
|
|
291
|
+
if (!token) continue;
|
|
292
|
+
const match = flatOrdered.find((option2) => option2.value === token || option2.label === token);
|
|
293
|
+
const option = match ?? (tagsMode ? { value: token, label: token } : void 0);
|
|
294
|
+
if (!option || option.disabled) continue;
|
|
295
|
+
if (next.includes(option.value)) continue;
|
|
296
|
+
if (maxCount !== void 0 && next.length >= maxCount) break;
|
|
297
|
+
next.push(option.value);
|
|
298
|
+
picked.push(option);
|
|
299
|
+
}
|
|
300
|
+
if (picked.length === 0) return;
|
|
301
|
+
for (const option of picked) remember(option);
|
|
302
|
+
if (!isControlled) setInternalValue(next);
|
|
303
|
+
props.onValueChange?.(
|
|
304
|
+
next,
|
|
305
|
+
next.map((entry) => picked.find((option) => option.value === entry) ?? optionFor(entry)).filter((entry) => entry != null)
|
|
306
|
+
);
|
|
307
|
+
for (const option of picked) props.onSelect?.(option.value, option);
|
|
308
|
+
};
|
|
309
|
+
const deselect = (chipValue) => {
|
|
310
|
+
select(optionFor(chipValue) ?? { value: chipValue, label: chipValue });
|
|
311
|
+
};
|
|
240
312
|
const clear = () => {
|
|
241
|
-
if (props.mode
|
|
313
|
+
if (props.mode !== void 0) {
|
|
242
314
|
if (!isControlled) setInternalValue([]);
|
|
243
315
|
props.onValueChange?.([], []);
|
|
244
316
|
} else {
|
|
@@ -302,100 +374,136 @@ function SearchSelect(props) {
|
|
|
302
374
|
);
|
|
303
375
|
const showClear = clearControl.enabled && hasSelection && !disabled && !readOnly && !loadingProp;
|
|
304
376
|
const surface = controlSurfaceAttrs({ variant, status, size });
|
|
377
|
+
const triggerProps = {
|
|
378
|
+
id,
|
|
379
|
+
role: "combobox",
|
|
380
|
+
...surface,
|
|
381
|
+
"aria-expanded": open,
|
|
382
|
+
"aria-controls": open ? listId : void 0,
|
|
383
|
+
"aria-label": triggerAriaLabel,
|
|
384
|
+
"aria-labelledby": triggerAriaLabelledby,
|
|
385
|
+
"aria-describedby": ariaDescribedby,
|
|
386
|
+
"aria-errormessage": ariaErrorMessage,
|
|
387
|
+
"aria-invalid": resolveAriaInvalid(ariaInvalid, status),
|
|
388
|
+
"aria-required": ariaRequired,
|
|
389
|
+
"aria-readonly": readOnly || void 0,
|
|
390
|
+
"aria-busy": loadingProp || void 0,
|
|
391
|
+
"data-testid": dataTestId,
|
|
392
|
+
"data-field": resolvedField,
|
|
393
|
+
// label. `""` (nothing selected) is omitted rather than rendered as an empty attribute.
|
|
394
|
+
"data-value": (multiple ? values.join(",") : value) || void 0,
|
|
395
|
+
"data-mode": multiple ? "multiple" : void 0,
|
|
396
|
+
"data-width": width,
|
|
397
|
+
className: cn(
|
|
398
|
+
controlSurfaceTriggerClass,
|
|
399
|
+
"justify-start",
|
|
400
|
+
// `bounded` deliberately emits NO width utility — its width is owned by the
|
|
401
|
+
// `[data-width="bounded"]` rule in control.css, and a utility here would win the layer
|
|
402
|
+
// order and make that token dead. Same reasoning as SelectTrigger.
|
|
403
|
+
width === "auto" && "w-auto",
|
|
404
|
+
width === "full" && "w-full",
|
|
405
|
+
// Reserve trailing room for the single clear-or-chevron overlay rendered below.
|
|
406
|
+
"ui-control-trigger-affixed"
|
|
407
|
+
)
|
|
408
|
+
};
|
|
409
|
+
const chipsRemovable = !disabled && !readOnly;
|
|
410
|
+
const triggerContent = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
411
|
+
prefix === void 0 || prefix === null ? null : /* @__PURE__ */ jsx("span", { "data-slot": "select-prefix", className: "ui-select-prefix", children: prefix }),
|
|
412
|
+
/* @__PURE__ */ jsx(
|
|
413
|
+
"span",
|
|
414
|
+
{
|
|
415
|
+
className: cn(
|
|
416
|
+
"ui-search-select-option-body text-start",
|
|
417
|
+
!hasSelection && "text-muted-foreground"
|
|
418
|
+
),
|
|
419
|
+
children: multiple ? hasSelection ? /* @__PURE__ */ jsxs("span", { className: "ui-search-select-values", "data-slot": "search-select-values", children: [
|
|
420
|
+
visibleChips.map((chip, index) => {
|
|
421
|
+
const close = () => deselect(chip.value);
|
|
422
|
+
return /* @__PURE__ */ jsx(
|
|
423
|
+
"span",
|
|
424
|
+
{
|
|
425
|
+
className: tagRender ? void 0 : "ui-search-select-value",
|
|
426
|
+
"data-slot": "search-select-value",
|
|
427
|
+
children: tagRender ? tagRender({
|
|
428
|
+
value: chip.value,
|
|
429
|
+
label: chip.label,
|
|
430
|
+
index,
|
|
431
|
+
disabled: Boolean(disabled) || Boolean(readOnly),
|
|
432
|
+
onClose: close
|
|
433
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
434
|
+
labelRender ? labelRender({
|
|
435
|
+
value: chip.value,
|
|
436
|
+
label: chip.label,
|
|
437
|
+
option: optionFor(chip.value) ?? void 0
|
|
438
|
+
}) : chip.label,
|
|
439
|
+
chipsRemovable ? /* @__PURE__ */ jsx(
|
|
440
|
+
"button",
|
|
441
|
+
{
|
|
442
|
+
type: "button",
|
|
443
|
+
className: "ui-search-select-value-remove",
|
|
444
|
+
"data-slot": "search-select-value-remove",
|
|
445
|
+
"aria-label": t("ui.tagInput.removeTag", { tag: chip.label }),
|
|
446
|
+
onClick: (event) => {
|
|
447
|
+
event.stopPropagation();
|
|
448
|
+
close();
|
|
449
|
+
},
|
|
450
|
+
children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
|
|
451
|
+
}
|
|
452
|
+
) : null
|
|
453
|
+
] })
|
|
454
|
+
},
|
|
455
|
+
chip.value
|
|
456
|
+
);
|
|
457
|
+
}),
|
|
458
|
+
omittedChips.length > 0 ? /* @__PURE__ */ jsx("span", { className: "ui-search-select-value", "data-slot": "search-select-value-overflow", children: chipOverflow ?? t("dataEntry.selection.overflow", { count: omittedChips.length }) }) : null
|
|
459
|
+
] }) : /* @__PURE__ */ jsx("span", { className: "truncate", children: resolvedPlaceholder }) : value && labelRender ? labelRender({ value, label: currentLabel, option: selectedOption ?? void 0 }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
460
|
+
currentIcon ? /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", "aria-hidden": "true", children: currentIcon }) : null,
|
|
461
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: currentLabel })
|
|
462
|
+
] })
|
|
463
|
+
}
|
|
464
|
+
)
|
|
465
|
+
] });
|
|
305
466
|
return /* @__PURE__ */ jsxs("div", { className: cn("relative", className), children: [
|
|
306
467
|
/* @__PURE__ */ jsxs(
|
|
307
468
|
Popover,
|
|
308
469
|
{
|
|
309
470
|
open,
|
|
310
471
|
onOpenChange: (next) => {
|
|
311
|
-
if (readOnly && next) return;
|
|
472
|
+
if ((readOnly || disabled) && next) return;
|
|
312
473
|
setOpen(next);
|
|
313
474
|
if (!next && autoClearSearchValue) setQuery("");
|
|
314
475
|
},
|
|
315
476
|
children: [
|
|
316
|
-
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
317
|
-
"
|
|
477
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: multiple ? /* @__PURE__ */ jsx(
|
|
478
|
+
"div",
|
|
318
479
|
{
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
"aria-invalid": resolveAriaInvalid(ariaInvalid, status),
|
|
330
|
-
"aria-required": ariaRequired,
|
|
331
|
-
"aria-readonly": readOnly || void 0,
|
|
332
|
-
"aria-busy": loadingProp || void 0,
|
|
333
|
-
disabled,
|
|
334
|
-
"data-testid": dataTestId,
|
|
335
|
-
"data-field": resolvedField,
|
|
336
|
-
"data-value": (multiple ? values.join(",") : value) || void 0,
|
|
337
|
-
"data-mode": multiple ? "multiple" : void 0,
|
|
338
|
-
"data-width": width,
|
|
339
|
-
className: cn(
|
|
340
|
-
controlSurfaceTriggerClass,
|
|
341
|
-
"justify-start",
|
|
342
|
-
// `bounded` deliberately emits NO width utility — its width is owned by the
|
|
343
|
-
// `[data-width="bounded"]` rule in control.css, and a utility here would win the
|
|
344
|
-
// layer order and make that token dead. Same reasoning as SelectTrigger.
|
|
345
|
-
width === "auto" && "w-auto",
|
|
346
|
-
width === "full" && "w-full",
|
|
347
|
-
// Reserve trailing room for the single clear-or-chevron overlay rendered below.
|
|
348
|
-
"ui-control-trigger-affixed"
|
|
349
|
-
),
|
|
350
|
-
children: /* @__PURE__ */ jsx(
|
|
351
|
-
"span",
|
|
352
|
-
{
|
|
353
|
-
className: cn(
|
|
354
|
-
"ui-search-select-option-body text-start",
|
|
355
|
-
!hasSelection && "text-muted-foreground"
|
|
356
|
-
),
|
|
357
|
-
children: multiple ? hasSelection ? (
|
|
358
|
-
// Labels, not removable chips: the trigger IS a <button>, and a per-label remove
|
|
359
|
-
// button inside it would be a button nested in a button (invalid HTML →
|
|
360
|
-
// hydration error). Removal is a second click on the row, or the clear ✕.
|
|
361
|
-
/* @__PURE__ */ jsxs("span", { className: "ui-search-select-values", "data-slot": "search-select-values", children: [
|
|
362
|
-
visibleChips.map((chip) => /* @__PURE__ */ jsx(
|
|
363
|
-
"span",
|
|
364
|
-
{
|
|
365
|
-
className: "ui-search-select-value",
|
|
366
|
-
"data-slot": "search-select-value",
|
|
367
|
-
children: labelRender ? labelRender({
|
|
368
|
-
value: chip.value,
|
|
369
|
-
label: chip.label,
|
|
370
|
-
option: optionFor(chip.value) ?? void 0
|
|
371
|
-
}) : chip.label
|
|
372
|
-
},
|
|
373
|
-
chip.value
|
|
374
|
-
)),
|
|
375
|
-
omittedChips.length > 0 ? /* @__PURE__ */ jsx(
|
|
376
|
-
"span",
|
|
377
|
-
{
|
|
378
|
-
className: "ui-search-select-value",
|
|
379
|
-
"data-slot": "search-select-value-overflow",
|
|
380
|
-
children: chipOverflow ?? t("dataEntry.selection.overflow", { count: omittedChips.length })
|
|
381
|
-
}
|
|
382
|
-
) : null
|
|
383
|
-
] })
|
|
384
|
-
) : /* @__PURE__ */ jsx("span", { className: "truncate", children: resolvedPlaceholder }) : value && labelRender ? labelRender({ value, label: currentLabel, option: selectedOption ?? void 0 }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
385
|
-
currentIcon ? /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", "aria-hidden": "true", children: currentIcon }) : null,
|
|
386
|
-
/* @__PURE__ */ jsx("span", { className: "truncate", children: currentLabel })
|
|
387
|
-
] })
|
|
480
|
+
...triggerProps,
|
|
481
|
+
tabIndex: disabled ? -1 : 0,
|
|
482
|
+
"aria-disabled": disabled || void 0,
|
|
483
|
+
"data-disabled": disabled ? "" : void 0,
|
|
484
|
+
onKeyDown: (event) => {
|
|
485
|
+
if (disabled || readOnly) return;
|
|
486
|
+
if (event.key === "Enter" || event.key === " " || event.key === "ArrowDown") {
|
|
487
|
+
event.preventDefault();
|
|
488
|
+
setOpen(true);
|
|
489
|
+
return;
|
|
388
490
|
}
|
|
389
|
-
|
|
491
|
+
if (event.key === "Backspace" && values.length > 0) {
|
|
492
|
+
event.preventDefault();
|
|
493
|
+
deselect(values[values.length - 1]);
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
children: triggerContent
|
|
390
497
|
}
|
|
391
|
-
) }),
|
|
498
|
+
) : /* @__PURE__ */ jsx("button", { ...triggerProps, type: "button", disabled, children: triggerContent }) }),
|
|
392
499
|
resolvedName ? multiple ? values.map((entry) => /* @__PURE__ */ jsx("input", { type: "hidden", name: resolvedName, value: entry, readOnly: true }, entry)) : /* @__PURE__ */ jsx("input", { type: "hidden", name: resolvedName, value, readOnly: true }) : null,
|
|
393
500
|
/* @__PURE__ */ jsx(
|
|
394
501
|
PopoverContent,
|
|
395
502
|
{
|
|
396
503
|
"aria-label": triggerAriaLabelledby ? void 0 : triggerAriaLabel ?? resolvedPlaceholder,
|
|
397
504
|
"aria-labelledby": triggerAriaLabelledby,
|
|
398
|
-
|
|
505
|
+
side: placement?.startsWith("top") ? "top" : "bottom",
|
|
506
|
+
align: placement?.endsWith("End") ? "end" : "start",
|
|
399
507
|
sideOffset: 4,
|
|
400
508
|
collisionPadding: 12,
|
|
401
509
|
className: "ui-search-select-panel",
|
|
@@ -403,7 +511,7 @@ function SearchSelect(props) {
|
|
|
403
511
|
style: typeof popupMatchSelectWidth === "number" ? {
|
|
404
512
|
"--search-select-panel-inline-size": `${popupMatchSelectWidth}px`
|
|
405
513
|
} : void 0,
|
|
406
|
-
children: /* @__PURE__ */ jsxs(Command, { value, shouldFilter: false, className: "ui-search-select-command", children: [
|
|
514
|
+
children: /* @__PURE__ */ jsx(PopupRender, { render: popupRender, children: /* @__PURE__ */ jsxs(Command, { value, shouldFilter: false, className: "ui-search-select-command", children: [
|
|
407
515
|
/* @__PURE__ */ jsx("div", { className: "ui-search-select-search", children: /* @__PURE__ */ jsx(
|
|
408
516
|
Input,
|
|
409
517
|
{
|
|
@@ -413,7 +521,23 @@ function SearchSelect(props) {
|
|
|
413
521
|
"aria-activedescendant": activeOptionId,
|
|
414
522
|
"aria-label": searchPlaceholder ?? t("dataEntry.searchSelect.search"),
|
|
415
523
|
value: query,
|
|
416
|
-
onChange: (event) =>
|
|
524
|
+
onChange: (event) => {
|
|
525
|
+
const next = event.target.value;
|
|
526
|
+
if (multiple && tokenSeparators?.length && tokenSeparators.some((separator) => next.includes(separator))) {
|
|
527
|
+
selectTokens(next);
|
|
528
|
+
setQuery("");
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
setQuery(next);
|
|
532
|
+
},
|
|
533
|
+
onPaste: (event) => {
|
|
534
|
+
if (!multiple || !tokenSeparators?.length) return;
|
|
535
|
+
const text = event.clipboardData.getData("text");
|
|
536
|
+
if (!tokenSeparators.some((separator) => text.includes(separator))) return;
|
|
537
|
+
event.preventDefault();
|
|
538
|
+
selectTokens(text);
|
|
539
|
+
setQuery("");
|
|
540
|
+
},
|
|
417
541
|
onKeyDown,
|
|
418
542
|
placeholder: searchPlaceholder ?? t("dataEntry.searchSelect.search"),
|
|
419
543
|
className: "ui-search-select-search-input"
|
|
@@ -427,7 +551,13 @@ function SearchSelect(props) {
|
|
|
427
551
|
"aria-multiselectable": multiple || void 0,
|
|
428
552
|
"aria-busy": loading,
|
|
429
553
|
className: "ui-search-select-list",
|
|
430
|
-
|
|
554
|
+
style: listHeight === void 0 ? void 0 : {
|
|
555
|
+
"--search-select-list-max-height": `${listHeight}px`
|
|
556
|
+
},
|
|
557
|
+
onScroll: (event) => {
|
|
558
|
+
onPopupScroll?.(event);
|
|
559
|
+
onScroll(event);
|
|
560
|
+
},
|
|
431
561
|
children: [
|
|
432
562
|
grouped.map((group) => {
|
|
433
563
|
const rows = group.items.map(({ option, index }) => /* @__PURE__ */ jsxs(
|
|
@@ -491,7 +621,7 @@ function SearchSelect(props) {
|
|
|
491
621
|
"data-tone": "destructive",
|
|
492
622
|
children: errorMessage ?? t("dataEntry.searchSelect.error")
|
|
493
623
|
}
|
|
494
|
-
) :
|
|
624
|
+
) : displayOptions.length === 0 ? /* @__PURE__ */ jsx(
|
|
495
625
|
"div",
|
|
496
626
|
{
|
|
497
627
|
role: "option",
|
|
@@ -518,7 +648,7 @@ function SearchSelect(props) {
|
|
|
518
648
|
})
|
|
519
649
|
}
|
|
520
650
|
) : null
|
|
521
|
-
] })
|
|
651
|
+
] }) })
|
|
522
652
|
}
|
|
523
653
|
)
|
|
524
654
|
]
|
|
@@ -546,7 +676,7 @@ function SearchSelect(props) {
|
|
|
546
676
|
onClick: clear,
|
|
547
677
|
children: clearControl.clearIcon ?? /* @__PURE__ */ jsx(X, { className: "ui-control-affix-icon", "aria-hidden": "true" })
|
|
548
678
|
}
|
|
549
|
-
) : /* @__PURE__ */ jsx(
|
|
679
|
+
) : suffixIcon === null ? null : suffixIcon !== void 0 ? /* @__PURE__ */ jsx("span", { "data-slot": "select-suffix", className: "ui-select-suffix", "aria-hidden": "true", children: suffixIcon }) : /* @__PURE__ */ jsx(
|
|
550
680
|
ChevronsUpDown,
|
|
551
681
|
{
|
|
552
682
|
className: "ui-control-affix-icon ui-control-affix-indicator",
|
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import type { ControlStatusProp, ControlVariantProp, ControlWidthProp, SizeProp } from "../../props/vocabulary/index.js";
|
|
3
|
-
import * as SelectPrimitive from "@radix-ui/react-select";
|
|
4
3
|
import type { FieldA11yProps } from "../../lib/field-a11y.js";
|
|
5
4
|
import type { SelectDataProp } from "../../props/components/data-entry.prop.js";
|
|
6
|
-
/** Compound-API props
|
|
5
|
+
/** Compound-API props — the shape the Radix root had, plus the FormField field-a11y contract that
|
|
7
6
|
* this component re-routes to the trigger (see {@link SelectFieldA11yContext}). */
|
|
8
|
-
export type SelectCompoundProp =
|
|
7
|
+
export type SelectCompoundProp = FieldA11yProps & {
|
|
9
8
|
id?: string;
|
|
9
|
+
value?: string;
|
|
10
|
+
defaultValue?: string;
|
|
11
|
+
onValueChange?: (value: string) => void;
|
|
12
|
+
open?: boolean;
|
|
13
|
+
defaultOpen?: boolean;
|
|
14
|
+
onOpenChange?: (open: boolean) => void;
|
|
15
|
+
/**
|
|
16
|
+
* @deprecated Accepted so Radix-era call sites keep compiling. Direction now comes from the
|
|
17
|
+
* ambient locale (`AppProvider` / `I18nProvider`), which react-aria reads itself.
|
|
18
|
+
*/
|
|
19
|
+
dir?: "ltr" | "rtl";
|
|
20
|
+
/** Form field name — a hidden native `<select>` carries the value on submit. */
|
|
21
|
+
name?: string;
|
|
22
|
+
autoComplete?: string;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
/** Native `required`: an empty select blocks a native form submit. */
|
|
25
|
+
required?: boolean;
|
|
26
|
+
form?: string;
|
|
27
|
+
children?: React.ReactNode;
|
|
10
28
|
};
|
|
11
29
|
export type SelectProp = SelectDataProp | SelectCompoundProp;
|
|
12
30
|
/**
|
|
@@ -17,9 +35,16 @@ export type SelectProp = SelectDataProp | SelectCompoundProp;
|
|
|
17
35
|
* optgroup grouping, and `renderOption`.
|
|
18
36
|
*/
|
|
19
37
|
export declare function Select(props: SelectProp): React.JSX.Element;
|
|
20
|
-
export declare function SelectGroup(props: React.
|
|
21
|
-
export declare
|
|
22
|
-
|
|
38
|
+
export declare function SelectGroup({ className, children, ...props }: React.HTMLAttributes<HTMLElement>): React.JSX.Element;
|
|
39
|
+
export declare const SelectValue: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> & {
|
|
40
|
+
/** Shown while nothing is selected. */
|
|
41
|
+
placeholder?: React.ReactNode;
|
|
42
|
+
/** Replaces the selected option's label on the trigger while a value is selected. */
|
|
43
|
+
children?: React.ReactNode;
|
|
44
|
+
/** @deprecated Radix-only; has no effect. */
|
|
45
|
+
asChild?: boolean;
|
|
46
|
+
} & React.RefAttributes<HTMLSpanElement>>;
|
|
47
|
+
export declare const SelectTrigger: React.ForwardRefExoticComponent<React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
23
48
|
/** Control height tier — the shared `--control-height` ladder (antd `size`). */
|
|
24
49
|
size?: SizeProp;
|
|
25
50
|
/** Control surface (antd `variant`). Default `outlined`. */
|
|
@@ -43,27 +68,64 @@ export declare const SelectTrigger: React.ForwardRefExoticComponent<Omit<SelectP
|
|
|
43
68
|
"data-field"?: string;
|
|
44
69
|
/**
|
|
45
70
|
* Normally supplied by `Select` itself — the trigger shows the option's LABEL, and this is the
|
|
46
|
-
* only place the underlying value is readable from the DOM without reaching into
|
|
47
|
-
*
|
|
71
|
+
* only place the underlying value is readable from the DOM without reaching into the aria-hidden
|
|
72
|
+
* native `<select>`.
|
|
48
73
|
*/
|
|
49
74
|
"data-value"?: string;
|
|
75
|
+
/** @deprecated Radix-only; has no effect — the trigger is always the library's own button. */
|
|
76
|
+
asChild?: boolean;
|
|
50
77
|
} & React.RefAttributes<HTMLButtonElement>>;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
78
|
+
type SelectScrollButtonProp = React.HTMLAttributes<HTMLDivElement>;
|
|
79
|
+
/**
|
|
80
|
+
* @deprecated Radix-only. react-aria's listbox is a native scroll container, so there is nothing
|
|
81
|
+
* to render; kept (as a no-op) so existing imports keep compiling until the next major.
|
|
82
|
+
*/
|
|
83
|
+
export declare const SelectScrollUpButton: React.ForwardRefExoticComponent<SelectScrollButtonProp & React.RefAttributes<HTMLDivElement>>;
|
|
84
|
+
/** @deprecated Radix-only — see {@link SelectScrollUpButton}. */
|
|
85
|
+
export declare const SelectScrollDownButton: React.ForwardRefExoticComponent<SelectScrollButtonProp & React.RefAttributes<HTMLDivElement>>;
|
|
86
|
+
type RadixSide = "top" | "right" | "bottom" | "left";
|
|
87
|
+
export declare const SelectContent: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLDivElement>, "dir"> & {
|
|
54
88
|
/**
|
|
55
89
|
* How the popup sizes against its trigger — the DOM half of antd `popupMatchSelectWidth`.
|
|
56
90
|
* Absent = the trigger's width is the floor (antd's `true`); `content` releases that floor;
|
|
57
91
|
* `fixed` pins both edges to `--select-content-inline-size`.
|
|
58
92
|
*/
|
|
59
93
|
"data-popup-match"?: "content" | "fixed";
|
|
94
|
+
side?: RadixSide;
|
|
95
|
+
sideOffset?: number;
|
|
96
|
+
align?: "start" | "center" | "end";
|
|
97
|
+
alignOffset?: number;
|
|
98
|
+
/** `false` keeps the popup on `side` even when it would overflow. */
|
|
99
|
+
avoidCollisions?: boolean;
|
|
100
|
+
collisionPadding?: number;
|
|
101
|
+
/** @deprecated Radix-only. The popup always positions against the trigger (`popper`). */
|
|
102
|
+
position?: "popper" | "item-aligned";
|
|
103
|
+
/** @deprecated Radix-only; has no effect. */
|
|
104
|
+
sticky?: "partial" | "always";
|
|
105
|
+
/** @deprecated Radix-only; has no effect. */
|
|
106
|
+
hideWhenDetached?: boolean;
|
|
107
|
+
/** @deprecated Radix-only; has no effect. */
|
|
108
|
+
onCloseAutoFocus?: (event: Event) => void;
|
|
109
|
+
/** @deprecated Radix-only; has no effect — Escape always closes. */
|
|
110
|
+
onEscapeKeyDown?: (event: KeyboardEvent) => void;
|
|
111
|
+
/** @deprecated Radix-only; has no effect — an outside press always closes. */
|
|
112
|
+
onPointerDownOutside?: (event: Event) => void;
|
|
113
|
+
/** @deprecated Radix-only; has no effect. */
|
|
114
|
+
forceMount?: true;
|
|
60
115
|
} & React.RefAttributes<HTMLDivElement>>;
|
|
61
|
-
export declare const SelectLabel: React.ForwardRefExoticComponent<
|
|
62
|
-
export declare const SelectItem: React.ForwardRefExoticComponent<Omit<
|
|
116
|
+
export declare const SelectLabel: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLElement> & React.RefAttributes<HTMLElement>>;
|
|
117
|
+
export declare const SelectItem: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> & {
|
|
118
|
+
value: string;
|
|
119
|
+
disabled?: boolean;
|
|
120
|
+
/** Text for type-to-select and the hidden native `<option>`; derived from `children` if omitted. */
|
|
121
|
+
textValue?: string;
|
|
63
122
|
/**
|
|
64
123
|
* antd `menuItemSelectedIcon` — a decorative mark on the picked row. Off by default: this
|
|
65
124
|
* library marks the picked row with fill + weight, which costs no width.
|
|
66
125
|
*/
|
|
67
126
|
selectedIcon?: React.ReactNode;
|
|
127
|
+
/** @deprecated Radix-only; has no effect. */
|
|
128
|
+
asChild?: boolean;
|
|
68
129
|
} & React.RefAttributes<HTMLDivElement>>;
|
|
69
|
-
export declare const SelectSeparator: React.ForwardRefExoticComponent<
|
|
130
|
+
export declare const SelectSeparator: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLElement> & React.RefAttributes<HTMLElement>>;
|
|
131
|
+
export {};
|