@godxjp/ui 27.0.0 → 27.3.1
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/dist/components/data-display/badge.d.ts +10 -3
- package/dist/components/data-display/badge.js +14 -3
- package/dist/components/data-display/data-table.d.ts +20 -12
- package/dist/components/data-display/data-table.js +53 -5
- package/dist/components/data-display/range-timeline.d.ts +16 -0
- package/dist/components/data-display/range-timeline.js +115 -80
- package/dist/components/data-display/table.d.ts +16 -0
- package/dist/components/data-display/table.js +2 -0
- package/dist/components/data-entry/checkbox.js +23 -5
- package/dist/components/data-entry/command.d.ts +3 -1
- package/dist/components/data-entry/command.js +10 -2
- package/dist/components/data-entry/form-errors.d.ts +5 -0
- package/dist/components/data-entry/form-errors.js +6 -0
- package/dist/components/data-entry/form-field.js +9 -8
- package/dist/components/data-entry/index.d.ts +1 -0
- package/dist/components/data-entry/search-select.js +145 -136
- package/dist/components/data-entry/select.js +38 -3
- package/dist/components/feedback/dialog.d.ts +4 -3
- package/dist/components/feedback/dialog.js +46 -19
- package/dist/components/feedback/non-modal-layer.d.ts +16 -0
- package/dist/components/feedback/non-modal-layer.js +37 -0
- package/dist/components/feedback/sheet.d.ts +7 -4
- package/dist/components/feedback/sheet.js +63 -40
- package/dist/components/layout/org-switcher.js +1 -1
- package/dist/components/layout/responsive-grid.d.ts +8 -2
- package/dist/components/layout/responsive-grid.js +2 -0
- package/dist/components/query/mutation-feedback.js +2 -4
- package/dist/contracts/measurement.json +1 -1
- package/dist/form/form-field-control.d.ts +1 -1
- package/dist/form/form-field-control.js +32 -23
- package/dist/form/form-root.d.ts +1 -1
- package/dist/form/form-root.js +25 -6
- package/dist/i18n/messages/en.json +2 -1
- package/dist/i18n/messages/ja.json +2 -1
- package/dist/i18n/messages/vi.json +2 -1
- package/dist/props/components/data-display.prop.d.ts +12 -0
- package/dist/props/components/data-entry.prop.d.ts +10 -0
- package/dist/props/components/feedback.prop.d.ts +7 -1
- package/dist/props/components/form.prop.d.ts +52 -3
- package/dist/props/components/index.d.ts +1 -1
- package/dist/props/components/layout.prop.d.ts +7 -0
- package/dist/props/registry.d.ts +26 -3
- package/dist/props/registry.js +25 -2
- package/dist/props/vocabulary/data.prop.d.ts +27 -1
- package/dist/props/vocabulary/index.d.ts +1 -1
- package/dist/styles/control.css +44 -1
- package/dist/styles/data-display-layout.css +58 -1
- package/dist/styles/form-layout.css +21 -0
- package/dist/styles/layout.css +7 -0
- package/dist/styles/navigation-layout.css +5 -1
- package/dist/styles/shell-layout.css +1 -14
- package/dist/styles/table-layout.css +80 -8
- package/dist/tokens/components/control.css +5 -0
- package/dist/tokens/components/data-display.css +6 -0
- package/dist/tokens/components/navigation.css +1 -1
- package/dist/tokens/components/table.css +2 -0
- package/docs/DESIGN-AUTHORITY.md +32 -1
- package/docs/FORMS.md +11 -10
- package/docs/data-display/data-table/examples/server-paged.tsx +156 -0
- package/docs/data-display/data-table/examples/striped-list.tsx +173 -0
- package/docs/data-display/popover.tsx +43 -1
- package/docs/data-display/timeline.tsx +73 -9
- package/docs/data-entry/command.tsx +80 -1
- package/docs/data-entry/form-root.tsx +12 -2
- package/docs/feedback/dialog.tsx +82 -0
- package/docs/feedback/sheet.tsx +84 -0
- package/docs/layout/responsive-grid.tsx +100 -0
- package/package.json +4 -2
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Checkbox as AriaCheckbox } from "react-aria-components";
|
|
5
5
|
import { Check, Minus } from "lucide-react";
|
|
6
6
|
import { cn } from "../../lib/utils.js";
|
|
7
|
-
import { useFieldIdentity } from "../../lib/field-a11y.js";
|
|
7
|
+
import { mergeAriaIds, useFieldIdentity } from "../../lib/field-a11y.js";
|
|
8
|
+
import { Label } from "./label.js";
|
|
8
9
|
import { CheckboxGroup } from "./checkbox-group.js";
|
|
9
10
|
import { withOwnHitTarget } from "./choice-hit-target.js";
|
|
10
11
|
function CheckboxGlyph({ state }) {
|
|
@@ -39,9 +40,14 @@ const CheckboxRoot = React.forwardRef((props, ref) => {
|
|
|
39
40
|
required,
|
|
40
41
|
value,
|
|
41
42
|
tabIndex,
|
|
43
|
+
children,
|
|
42
44
|
"data-field": ownField,
|
|
43
45
|
...rest
|
|
44
46
|
} = props;
|
|
47
|
+
const autoId = React.useId();
|
|
48
|
+
const hasInlineLabel = !(children === void 0 || children === null || children === false || children === "");
|
|
49
|
+
const inputId = props.id ?? (hasInlineLabel ? autoId : void 0);
|
|
50
|
+
const inlineLabelId = `${inputId}-inline-label`;
|
|
45
51
|
const identity = useFieldIdentity({
|
|
46
52
|
id: props.id,
|
|
47
53
|
name: props.name,
|
|
@@ -63,7 +69,10 @@ const CheckboxRoot = React.forwardRef((props, ref) => {
|
|
|
63
69
|
);
|
|
64
70
|
const state = checked ?? uncontrolled;
|
|
65
71
|
const invalid = props["aria-invalid"];
|
|
66
|
-
|
|
72
|
+
const isInvalid = invalid !== void 0 && invalid !== false && invalid !== "false";
|
|
73
|
+
const describedBy = isInvalid ? mergeAriaIds(props["aria-describedby"], props["aria-errormessage"]) : props["aria-describedby"];
|
|
74
|
+
const labelledBy = hasInlineLabel && props["aria-labelledby"] !== void 0 ? mergeAriaIds(props["aria-labelledby"], inlineLabelId) : props["aria-labelledby"];
|
|
75
|
+
const box = /* @__PURE__ */ jsx(
|
|
67
76
|
AriaCheckbox,
|
|
68
77
|
{
|
|
69
78
|
...rest,
|
|
@@ -77,7 +86,10 @@ const CheckboxRoot = React.forwardRef((props, ref) => {
|
|
|
77
86
|
isIndeterminate: state === "indeterminate",
|
|
78
87
|
isDisabled: disabled,
|
|
79
88
|
isRequired: required,
|
|
80
|
-
isInvalid
|
|
89
|
+
isInvalid,
|
|
90
|
+
id: inputId,
|
|
91
|
+
"aria-describedby": describedBy,
|
|
92
|
+
"aria-labelledby": labelledBy,
|
|
81
93
|
excludeFromTabOrder: tabIndex !== void 0 && tabIndex < 0,
|
|
82
94
|
className: cn(
|
|
83
95
|
// `disabled:cursor-not-allowed disabled:opacity-50` and the checked fill are DELETED, not
|
|
@@ -95,7 +107,8 @@ const CheckboxRoot = React.forwardRef((props, ref) => {
|
|
|
95
107
|
// 16px box collapses to nothing. CheckboxVisual already carries the same three for the
|
|
96
108
|
// same reason.
|
|
97
109
|
"peer ui-checkbox data-[invalid]:border-destructive data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground inline-flex shrink-0 items-center justify-center shadow-xs transition-shadow",
|
|
98
|
-
|
|
110
|
+
// With an inline label the row is what a caller styles; the box keeps its own classes.
|
|
111
|
+
hasInlineLabel ? void 0 : className
|
|
99
112
|
),
|
|
100
113
|
render: (domProps) => /* @__PURE__ */ jsx("label", { ...domProps, children: withOwnHitTarget(domProps.children) }),
|
|
101
114
|
onChange: (next) => {
|
|
@@ -105,6 +118,11 @@ const CheckboxRoot = React.forwardRef((props, ref) => {
|
|
|
105
118
|
children: /* @__PURE__ */ jsx("span", { "data-slot": "checkbox-indicator", className: "ui-choice-indicator", children: state ? /* @__PURE__ */ jsx(CheckboxGlyph, { state }) : null })
|
|
106
119
|
}
|
|
107
120
|
);
|
|
121
|
+
if (!hasInlineLabel) return box;
|
|
122
|
+
return /* @__PURE__ */ jsxs("div", { "data-slot": "checkbox-field", className: cn("ui-choice-field", className), children: [
|
|
123
|
+
/* @__PURE__ */ jsx("div", { className: "ui-choice-control", children: box }),
|
|
124
|
+
/* @__PURE__ */ jsx("div", { className: "ui-choice-content", children: /* @__PURE__ */ jsx(Label, { id: inlineLabelId, htmlFor: inputId, className: "ui-choice-label", children }) })
|
|
125
|
+
] });
|
|
108
126
|
});
|
|
109
127
|
CheckboxRoot.displayName = "Checkbox";
|
|
110
128
|
const Checkbox = Object.assign(CheckboxRoot, {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import type { CommandProp } from "../../props/components/data-entry.prop.js";
|
|
3
|
+
export type { CommandProp, CommandProp as CommandProps };
|
|
2
4
|
export declare const Command: React.ForwardRefExoticComponent<Omit<{
|
|
3
5
|
children?: React.ReactNode;
|
|
4
6
|
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
@@ -15,7 +17,7 @@ export declare const Command: React.ForwardRefExoticComponent<Omit<{
|
|
|
15
17
|
loop?: boolean;
|
|
16
18
|
disablePointerSelection?: boolean;
|
|
17
19
|
vimBindings?: boolean;
|
|
18
|
-
} & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
20
|
+
} & React.RefAttributes<HTMLDivElement>, "ref"> & CommandProp & React.RefAttributes<HTMLDivElement>>;
|
|
19
21
|
export declare const CommandInput: React.ForwardRefExoticComponent<Omit<Omit<Pick<Pick<React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "key" | keyof React.InputHTMLAttributes<HTMLInputElement>> & {
|
|
20
22
|
ref?: React.Ref<HTMLInputElement>;
|
|
21
23
|
} & {
|
|
@@ -5,7 +5,7 @@ import { Command as CommandPrimitive } from "cmdk";
|
|
|
5
5
|
import { Search } from "lucide-react";
|
|
6
6
|
import { cn } from "../../lib/utils.js";
|
|
7
7
|
import { controlIconLeadingClass } from "../../lib/control-styles.js";
|
|
8
|
-
const Command = React.forwardRef(({ className, ...props }, ref) => {
|
|
8
|
+
const Command = React.forwardRef(({ className, split = false, ...props }, ref) => {
|
|
9
9
|
const rootRef = React.useRef(null);
|
|
10
10
|
const setRefs = (node) => {
|
|
11
11
|
rootRef.current = node;
|
|
@@ -22,7 +22,15 @@ const Command = React.forwardRef(({ className, ...props }, ref) => {
|
|
|
22
22
|
if (input && expectedId && !input.id) input.id = expectedId;
|
|
23
23
|
else if (!input || input.id !== expectedId) label.removeAttribute("for");
|
|
24
24
|
});
|
|
25
|
-
return /* @__PURE__ */ jsx(
|
|
25
|
+
return /* @__PURE__ */ jsx(
|
|
26
|
+
CommandPrimitive,
|
|
27
|
+
{
|
|
28
|
+
ref: setRefs,
|
|
29
|
+
className: cn("ui-command", className),
|
|
30
|
+
"data-split": split ? "" : void 0,
|
|
31
|
+
...props
|
|
32
|
+
}
|
|
33
|
+
);
|
|
26
34
|
});
|
|
27
35
|
Command.displayName = CommandPrimitive.displayName;
|
|
28
36
|
const CommandInput = React.forwardRef(({ className, ...props }, ref) => (
|
|
@@ -17,6 +17,11 @@ interface FormErrorsRegistryValue {
|
|
|
17
17
|
export declare function useFormErrorsRegistry(): FormErrorsRegistryValue | null;
|
|
18
18
|
/** First message of a bag entry — Laravel arrays surface their first message on the field. */
|
|
19
19
|
export declare function firstBagMessage(entry: string | string[] | undefined): string | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* `true` when an error bag holds at least one non-empty message. A bag with no keys, or keys whose
|
|
22
|
+
* entries are empty (`{ code: [] }`), shows nothing anywhere — so it never counts as "displayed".
|
|
23
|
+
*/
|
|
24
|
+
export declare function errorBagHasMessage(errors: ErrorBagProp | undefined): boolean;
|
|
20
25
|
/**
|
|
21
26
|
* FormErrorsProvider — one shared error registry (error bag + claim set) over a region. 兄弟 Form 群で
|
|
22
27
|
* 1 つのエラーバッグを共有するための公開プロバイダ。Two ways to get one: 1. `Form errors={…}` renders this provider itself
|
|
@@ -12,6 +12,11 @@ function firstBagMessage(entry) {
|
|
|
12
12
|
const message = Array.isArray(entry) ? entry[0] : entry;
|
|
13
13
|
return message ? message : void 0;
|
|
14
14
|
}
|
|
15
|
+
function errorBagHasMessage(errors) {
|
|
16
|
+
return Object.values(errors ?? {}).some(
|
|
17
|
+
(entry) => Array.isArray(entry) ? entry.some(Boolean) : Boolean(entry)
|
|
18
|
+
);
|
|
19
|
+
}
|
|
15
20
|
function FormErrorsProvider({ errors, children }) {
|
|
16
21
|
const [claimed, setClaimed] = React.useState(/* @__PURE__ */ new Map());
|
|
17
22
|
const claim = React.useCallback((key) => {
|
|
@@ -65,6 +70,7 @@ function FormErrors({ errors: errorsProp, title, className }) {
|
|
|
65
70
|
export {
|
|
66
71
|
FormErrors,
|
|
67
72
|
FormErrorsProvider,
|
|
73
|
+
errorBagHasMessage,
|
|
68
74
|
firstBagMessage,
|
|
69
75
|
useClaimErrorKey,
|
|
70
76
|
useFormErrorsRegistry
|
|
@@ -47,6 +47,7 @@ function FormField({
|
|
|
47
47
|
const autoId = React.useId();
|
|
48
48
|
const resolvedId = id ?? autoId;
|
|
49
49
|
const labelId = `${resolvedId}-label`;
|
|
50
|
+
const hasLabel = !(label === void 0 || label === null || label === false || label === "");
|
|
50
51
|
const helperId = helper ? `${resolvedId}-helper` : void 0;
|
|
51
52
|
const helperNode = helper ? /* @__PURE__ */ jsx("p", { id: helperId, className: "ui-form-field-note text-muted-foreground text-xs", children: helper }) : null;
|
|
52
53
|
const validationStatus = error ? "error" : validateStatus;
|
|
@@ -62,8 +63,8 @@ function FormField({
|
|
|
62
63
|
);
|
|
63
64
|
}
|
|
64
65
|
const fieldNameContext = React.useMemo(
|
|
65
|
-
() =>
|
|
66
|
-
[labelId, label]
|
|
66
|
+
() => hasLabel ? { labelId, label: typeof label === "string" ? label : void 0 } : null,
|
|
67
|
+
[hasLabel, labelId, label]
|
|
67
68
|
);
|
|
68
69
|
const fieldIdentityContext = React.useMemo(
|
|
69
70
|
() => fieldKey === void 0 ? null : { emitName: emitFieldNames },
|
|
@@ -88,12 +89,12 @@ function FormField({
|
|
|
88
89
|
// `"data-field": fieldKey` would erase a value the control set for itself.
|
|
89
90
|
"data-field": childProps?.["data-field"] ?? fieldKey,
|
|
90
91
|
...emitFieldNames ? { name: childProps?.name ?? fieldKey } : void 0,
|
|
91
|
-
"aria-labelledby": childProps?.["aria-labelledby"] ?? labelId,
|
|
92
|
+
"aria-labelledby": childProps?.["aria-labelledby"] ?? (hasLabel ? labelId : void 0),
|
|
92
93
|
// Redundant `aria-label` fallback (belt-and-suspenders): the accessible name is the
|
|
93
94
|
// SAME string as the visible label, just reachable even if an aria-labelledby lookup
|
|
94
95
|
// ever comes back empty (id-ref timing, AT quirks). Only when `label` is plain text and
|
|
95
96
|
// the child hasn't already set its own aria-label.
|
|
96
|
-
"aria-label": childProps?.["aria-label"] ?? (typeof label === "string" ? label : void 0),
|
|
97
|
+
"aria-label": childProps?.["aria-label"] ?? (hasLabel && typeof label === "string" ? label : void 0),
|
|
97
98
|
// Helper and error can coexist: helper stays on aria-describedby, the error on
|
|
98
99
|
// aria-errormessage (surfaced when aria-invalid is true).
|
|
99
100
|
"aria-describedby": mergeIds(
|
|
@@ -124,8 +125,8 @@ function FormField({
|
|
|
124
125
|
style: Object.keys(style).length ? style : void 0,
|
|
125
126
|
className: cn("ui-form-field", className),
|
|
126
127
|
children: [
|
|
127
|
-
/* @__PURE__ */ jsxs("div", { "data-slot": "form-field-label", className: "ui-form-field-label", children: [
|
|
128
|
-
/* @__PURE__ */ jsx(
|
|
128
|
+
hasLabel || labelAddon != null ? /* @__PURE__ */ jsxs("div", { "data-slot": "form-field-label", className: "ui-form-field-label", children: [
|
|
129
|
+
hasLabel ? /* @__PURE__ */ jsx(
|
|
129
130
|
Label,
|
|
130
131
|
{
|
|
131
132
|
asChild: true,
|
|
@@ -151,9 +152,9 @@ function FormField({
|
|
|
151
152
|
}
|
|
152
153
|
)
|
|
153
154
|
}
|
|
154
|
-
),
|
|
155
|
+
) : null,
|
|
155
156
|
labelAddon
|
|
156
|
-
] }),
|
|
157
|
+
] }) : null,
|
|
157
158
|
/* @__PURE__ */ jsxs("div", { "data-slot": "form-field-control", className: "ui-form-field-control", children: [
|
|
158
159
|
helperPlacement === "before" ? helperNode : null,
|
|
159
160
|
isStatic ? childWithA11y : /* @__PURE__ */ jsx(FieldNameContext.Provider, { value: fieldNameContext, children: /* @__PURE__ */ jsx(FieldIdentityContext.Provider, { value: fieldIdentityContext, children: /* @__PURE__ */ jsx(
|
|
@@ -49,6 +49,7 @@ export type { TreeSelectProps } from "./tree-select.js";
|
|
|
49
49
|
export { Transfer } from "./transfer.js";
|
|
50
50
|
export type { TransferProps, TransferItemProp } from "./transfer.js";
|
|
51
51
|
export { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "./command.js";
|
|
52
|
+
export type { CommandProps } from "./command.js";
|
|
52
53
|
export { CommandPalette } from "./command-palette.js";
|
|
53
54
|
export type { CommandPaletteGroup, CommandPaletteItem, CommandPaletteLabels, CommandPaletteProps, } from "./command-palette.js";
|
|
54
55
|
export { PasswordInput } from "./password-input.js";
|
|
@@ -511,144 +511,153 @@ function SearchSelect(props) {
|
|
|
511
511
|
style: typeof popupMatchSelectWidth === "number" ? {
|
|
512
512
|
"--search-select-panel-inline-size": `${popupMatchSelectWidth}px`
|
|
513
513
|
} : void 0,
|
|
514
|
-
children: /* @__PURE__ */ jsx(PopupRender, { render: popupRender, children: /* @__PURE__ */ jsxs(
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
"
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
514
|
+
children: /* @__PURE__ */ jsx(PopupRender, { render: popupRender, children: /* @__PURE__ */ jsxs(
|
|
515
|
+
Command,
|
|
516
|
+
{
|
|
517
|
+
value,
|
|
518
|
+
shouldFilter: false,
|
|
519
|
+
className: "ui-search-select-command",
|
|
520
|
+
split: multiple,
|
|
521
|
+
children: [
|
|
522
|
+
/* @__PURE__ */ jsx("div", { className: "ui-search-select-search", children: /* @__PURE__ */ jsx(
|
|
523
|
+
Input,
|
|
524
|
+
{
|
|
525
|
+
autoFocus: true,
|
|
526
|
+
"aria-controls": listId,
|
|
527
|
+
"aria-autocomplete": "list",
|
|
528
|
+
"aria-activedescendant": activeOptionId,
|
|
529
|
+
"aria-label": searchPlaceholder ?? t("dataEntry.searchSelect.search"),
|
|
530
|
+
value: query,
|
|
531
|
+
onChange: (event) => {
|
|
532
|
+
const next = event.target.value;
|
|
533
|
+
if (multiple && tokenSeparators?.length && tokenSeparators.some((separator) => next.includes(separator))) {
|
|
534
|
+
selectTokens(next);
|
|
535
|
+
setQuery("");
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
setQuery(next);
|
|
539
|
+
},
|
|
540
|
+
onPaste: (event) => {
|
|
541
|
+
if (!multiple || !tokenSeparators?.length) return;
|
|
542
|
+
const text = event.clipboardData.getData("text");
|
|
543
|
+
if (!tokenSeparators.some((separator) => text.includes(separator))) return;
|
|
544
|
+
event.preventDefault();
|
|
545
|
+
selectTokens(text);
|
|
546
|
+
setQuery("");
|
|
547
|
+
},
|
|
548
|
+
onKeyDown,
|
|
549
|
+
placeholder: searchPlaceholder ?? t("dataEntry.searchSelect.search"),
|
|
550
|
+
className: "ui-search-select-search-input"
|
|
530
551
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
"data-tone": "destructive",
|
|
622
|
-
children: errorMessage ?? t("dataEntry.searchSelect.error")
|
|
623
|
-
}
|
|
624
|
-
) : displayOptions.length === 0 ? /* @__PURE__ */ jsx(
|
|
625
|
-
"div",
|
|
626
|
-
{
|
|
627
|
-
role: "option",
|
|
628
|
-
"aria-disabled": "true",
|
|
629
|
-
"aria-selected": false,
|
|
630
|
-
className: "ui-search-select-placeholder",
|
|
631
|
-
children: notFoundContent ?? emptyMessage ?? t("dataEntry.searchSelect.empty")
|
|
632
|
-
}
|
|
633
|
-
) : null
|
|
634
|
-
]
|
|
635
|
-
}
|
|
636
|
-
),
|
|
637
|
-
renderLoadMore && hasMore ? /* @__PURE__ */ jsx(
|
|
638
|
-
"div",
|
|
639
|
-
{
|
|
640
|
-
className: "ui-search-select-footer",
|
|
641
|
-
onKeyDown: (event) => event.stopPropagation(),
|
|
642
|
-
children: renderLoadMore({
|
|
643
|
-
hasMore,
|
|
644
|
-
loading,
|
|
645
|
-
loadMore: () => {
|
|
646
|
-
if (!loading) void fetchPage(page + 1, debouncedQuery, true);
|
|
552
|
+
) }),
|
|
553
|
+
/* @__PURE__ */ jsxs(
|
|
554
|
+
"div",
|
|
555
|
+
{
|
|
556
|
+
id: listId,
|
|
557
|
+
role: "listbox",
|
|
558
|
+
"aria-multiselectable": multiple || void 0,
|
|
559
|
+
"aria-busy": loading,
|
|
560
|
+
className: "ui-search-select-list",
|
|
561
|
+
style: listHeight === void 0 ? void 0 : {
|
|
562
|
+
"--search-select-list-max-height": `${listHeight}px`
|
|
563
|
+
},
|
|
564
|
+
onScroll: (event) => {
|
|
565
|
+
onPopupScroll?.(event);
|
|
566
|
+
onScroll(event);
|
|
567
|
+
},
|
|
568
|
+
children: [
|
|
569
|
+
grouped.map((group) => {
|
|
570
|
+
const rows = group.items.map(({ option, index }) => /* @__PURE__ */ jsxs(
|
|
571
|
+
"div",
|
|
572
|
+
{
|
|
573
|
+
id: optionDomId(option.value),
|
|
574
|
+
role: "option",
|
|
575
|
+
"data-testid": optionTestId(option.value),
|
|
576
|
+
"aria-selected": isSelected(option.value),
|
|
577
|
+
"aria-disabled": option.disabled || atMaxCount && !isSelected(option.value) || void 0,
|
|
578
|
+
"data-disabled": option.disabled || atMaxCount && !isSelected(option.value) || void 0,
|
|
579
|
+
className: cn(
|
|
580
|
+
"ui-command-item",
|
|
581
|
+
// Selected = persistent bg-accent + medium weight (NO check icon — saves width),
|
|
582
|
+
// matching the plain SelectItem's `data-[state=checked]` convention; active =
|
|
583
|
+
// hover/keyboard accent. Same bg so selection stays coherent across both Selects.
|
|
584
|
+
isSelected(option.value) && "bg-accent text-foreground font-medium",
|
|
585
|
+
activeIndex === index && "bg-accent text-accent-foreground"
|
|
586
|
+
),
|
|
587
|
+
onMouseEnter: () => !option.disabled && setActiveIndex(index),
|
|
588
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
589
|
+
onClick: () => select(option),
|
|
590
|
+
children: [
|
|
591
|
+
optionRender ? /* @__PURE__ */ jsx("div", { className: "ui-search-select-option-slot", children: optionRender(option, { index }) }) : renderOption ? /* @__PURE__ */ jsx("div", { className: "ui-search-select-option-slot", children: renderOption(option) }) : /* @__PURE__ */ jsxs("div", { className: "ui-search-select-option-body", children: [
|
|
592
|
+
option.icon ? /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", "aria-hidden": "true", children: option.icon }) : null,
|
|
593
|
+
/* @__PURE__ */ jsxs("div", { className: "ui-search-select-option-text", children: [
|
|
594
|
+
/* @__PURE__ */ jsx("span", { className: "ui-search-select-option-label", children: option.label }),
|
|
595
|
+
option.sublabel ? /* @__PURE__ */ jsx("span", { className: "ui-search-select-option-sublabel", children: option.sublabel }) : null
|
|
596
|
+
] })
|
|
597
|
+
] }),
|
|
598
|
+
menuItemSelectedIcon && isSelected(option.value) ? /* @__PURE__ */ jsx(
|
|
599
|
+
"span",
|
|
600
|
+
{
|
|
601
|
+
"data-slot": "search-select-selected-icon",
|
|
602
|
+
className: "ui-search-select-selected-icon",
|
|
603
|
+
"aria-hidden": "true",
|
|
604
|
+
children: menuItemSelectedIcon
|
|
605
|
+
}
|
|
606
|
+
) : null
|
|
607
|
+
]
|
|
608
|
+
},
|
|
609
|
+
option.value
|
|
610
|
+
));
|
|
611
|
+
return group.heading ? /* @__PURE__ */ jsx(CommandGroup, { heading: group.heading, children: rows }, group.heading) : /* @__PURE__ */ jsx(React.Fragment, { children: rows }, "__ungrouped");
|
|
612
|
+
}),
|
|
613
|
+
loading ? /* @__PURE__ */ jsxs("div", { role: "status", className: "ui-search-select-status", children: [
|
|
614
|
+
/* @__PURE__ */ jsx(Loader2, { className: "ui-search-select-spinner animate-spin", "aria-hidden": "true" }),
|
|
615
|
+
loadingMessage ?? t("dataEntry.searchSelect.loading")
|
|
616
|
+
] }) : error ? renderError ? renderError({
|
|
617
|
+
message: errorMessage ?? t("dataEntry.searchSelect.error"),
|
|
618
|
+
// Retry always reloads from the first page (a defined, predictable recovery —
|
|
619
|
+
// NOT a resume of a failed page-N append, which would need to re-derive state).
|
|
620
|
+
retry: () => void fetchPage(1, debouncedQuery, false)
|
|
621
|
+
}) : /* @__PURE__ */ jsx(
|
|
622
|
+
"div",
|
|
623
|
+
{
|
|
624
|
+
role: "option",
|
|
625
|
+
"aria-disabled": "true",
|
|
626
|
+
"aria-selected": false,
|
|
627
|
+
className: "ui-search-select-placeholder",
|
|
628
|
+
"data-tone": "destructive",
|
|
629
|
+
children: errorMessage ?? t("dataEntry.searchSelect.error")
|
|
630
|
+
}
|
|
631
|
+
) : displayOptions.length === 0 ? /* @__PURE__ */ jsx(
|
|
632
|
+
"div",
|
|
633
|
+
{
|
|
634
|
+
role: "option",
|
|
635
|
+
"aria-disabled": "true",
|
|
636
|
+
"aria-selected": false,
|
|
637
|
+
className: "ui-search-select-placeholder",
|
|
638
|
+
children: notFoundContent ?? emptyMessage ?? t("dataEntry.searchSelect.empty")
|
|
639
|
+
}
|
|
640
|
+
) : null
|
|
641
|
+
]
|
|
647
642
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
643
|
+
),
|
|
644
|
+
renderLoadMore && hasMore ? /* @__PURE__ */ jsx(
|
|
645
|
+
"div",
|
|
646
|
+
{
|
|
647
|
+
className: "ui-search-select-footer",
|
|
648
|
+
onKeyDown: (event) => event.stopPropagation(),
|
|
649
|
+
children: renderLoadMore({
|
|
650
|
+
hasMore,
|
|
651
|
+
loading,
|
|
652
|
+
loadMore: () => {
|
|
653
|
+
if (!loading) void fetchPage(page + 1, debouncedQuery, true);
|
|
654
|
+
}
|
|
655
|
+
})
|
|
656
|
+
}
|
|
657
|
+
) : null
|
|
658
|
+
]
|
|
659
|
+
}
|
|
660
|
+
) })
|
|
652
661
|
}
|
|
653
662
|
)
|
|
654
663
|
]
|
|
@@ -55,6 +55,35 @@ function itemTextValue(node) {
|
|
|
55
55
|
}
|
|
56
56
|
return "";
|
|
57
57
|
}
|
|
58
|
+
function useSettledDefaultOpen(rootRef, defaultOpen, open) {
|
|
59
|
+
const [deferred] = React.useState(() => defaultOpen === true && open === void 0);
|
|
60
|
+
const [settledOpen, setSettledOpen] = React.useState(false);
|
|
61
|
+
React.useLayoutEffect(() => {
|
|
62
|
+
if (!deferred) return;
|
|
63
|
+
let cancelled = false;
|
|
64
|
+
const moving = [];
|
|
65
|
+
for (let node = rootRef.current?.parentElement; node; node = node.parentElement) {
|
|
66
|
+
if (typeof node.getAnimations !== "function") break;
|
|
67
|
+
for (const animation of node.getAnimations()) {
|
|
68
|
+
const end = animation.effect?.getComputedTiming().endTime;
|
|
69
|
+
if (animation.playState === "running" && Number.isFinite(Number(end))) {
|
|
70
|
+
moving.push(animation.finished);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (moving.length === 0) {
|
|
75
|
+
setSettledOpen(true);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
void Promise.allSettled(moving).then(() => {
|
|
79
|
+
if (!cancelled) setSettledOpen(true);
|
|
80
|
+
});
|
|
81
|
+
return () => {
|
|
82
|
+
cancelled = true;
|
|
83
|
+
};
|
|
84
|
+
}, [deferred, rootRef]);
|
|
85
|
+
return deferred ? { open: settledOpen, setOpen: setSettledOpen } : null;
|
|
86
|
+
}
|
|
58
87
|
function SelectRoot({
|
|
59
88
|
id,
|
|
60
89
|
value,
|
|
@@ -76,9 +105,12 @@ function SelectRoot({
|
|
|
76
105
|
children
|
|
77
106
|
}) {
|
|
78
107
|
void dir;
|
|
108
|
+
const rootRef = React.useRef(null);
|
|
109
|
+
const settled = useSettledDefaultOpen(rootRef, defaultOpen, open);
|
|
79
110
|
return /* @__PURE__ */ jsx(
|
|
80
111
|
AriaSelect,
|
|
81
112
|
{
|
|
113
|
+
ref: rootRef,
|
|
82
114
|
"data-slot": "select",
|
|
83
115
|
className: "ui-select-root",
|
|
84
116
|
id,
|
|
@@ -87,9 +119,12 @@ function SelectRoot({
|
|
|
87
119
|
onChange: (key) => {
|
|
88
120
|
if (key != null) onValueChange?.(String(key));
|
|
89
121
|
},
|
|
90
|
-
isOpen: open,
|
|
91
|
-
defaultOpen,
|
|
92
|
-
onOpenChange
|
|
122
|
+
isOpen: open ?? settled?.open,
|
|
123
|
+
defaultOpen: settled ? void 0 : defaultOpen,
|
|
124
|
+
onOpenChange: (next) => {
|
|
125
|
+
settled?.setOpen(next);
|
|
126
|
+
onOpenChange?.(next);
|
|
127
|
+
},
|
|
93
128
|
isDisabled: disabled || isDisabled,
|
|
94
129
|
isRequired: required,
|
|
95
130
|
name,
|
|
@@ -12,14 +12,15 @@ interface OverlayOpenProps {
|
|
|
12
12
|
}
|
|
13
13
|
interface DialogRootProps extends OverlayOpenProps, Pick<DialogProp, "variant"> {
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* `false` = hộp thoại không-modal (WAI-ARIA APG cho phép): trang phía sau vẫn tương tác được và
|
|
16
|
+
* vẫn trong cây trợ năng, không khoá cuộn, không màn nền, bấm ra ngoài không đóng; tiêu điểm vào
|
|
17
|
+
* hộp thoại khi mở, Tab ra được, Esc (tiêu điểm ở trong) đóng. Xem đầu tệp, mục gh#696.
|
|
17
18
|
*/
|
|
18
19
|
modal?: boolean;
|
|
19
20
|
children?: React.ReactNode;
|
|
20
21
|
}
|
|
21
22
|
/** `data-slot="dialog"` cũ nằm trên Root của Radix, thứ không dựng thẻ nào — nó chưa từng ra DOM. */
|
|
22
|
-
declare function DialogRoot({ children, modal
|
|
23
|
+
declare function DialogRoot({ children, modal, variant, ...openProps }: DialogRootProps): React.JSX.Element;
|
|
23
24
|
interface DialogTriggerProps extends React.ComponentPropsWithRef<"button"> {
|
|
24
25
|
/** Mượn thẻ của con thay vì dựng `<button>` riêng. */
|
|
25
26
|
asChild?: boolean;
|