@godxjp/ui 20.2.1 → 22.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.
- package/dist/app/app-provider.js +2 -1
- package/dist/components/data-display/badge.d.ts +28 -1
- package/dist/components/data-display/badge.js +2 -0
- package/dist/components/data-display/chat-bubble.d.ts +30 -0
- package/dist/components/data-display/chat-bubble.js +229 -0
- package/dist/components/data-display/data-table.d.ts +18 -2
- package/dist/components/data-display/data-table.js +4 -0
- package/dist/components/data-display/descriptions.js +4 -1
- package/dist/components/data-display/index.d.ts +5 -3
- package/dist/components/data-display/index.js +5 -2
- package/dist/components/data-display/popover.d.ts +19 -1
- package/dist/components/data-display/popover.js +12 -3
- package/dist/components/data-display/progress.d.ts +37 -0
- package/dist/components/data-display/progress.js +57 -5
- package/dist/components/data-display/scroll-area.js +26 -1
- package/dist/components/data-display/tree.d.ts +8 -0
- package/dist/components/data-display/tree.js +426 -0
- package/dist/components/data-entry/chat-composer.d.ts +50 -0
- package/dist/components/data-entry/chat-composer.js +163 -0
- package/dist/components/data-entry/chat-suggestion.d.ts +28 -0
- package/dist/components/data-entry/chat-suggestion.js +285 -0
- package/dist/components/data-entry/date-picker.d.ts +10 -3
- package/dist/components/data-entry/date-picker.js +503 -238
- package/dist/components/data-entry/form-field.d.ts +1 -1
- package/dist/components/data-entry/form-field.js +4 -1
- package/dist/components/data-entry/form.js +10 -1
- package/dist/components/data-entry/index.d.ts +4 -6
- package/dist/components/data-entry/index.js +4 -6
- package/dist/components/data-entry/label.d.ts +14 -1
- package/dist/components/data-entry/label.js +6 -1
- package/dist/components/data-entry/radio.d.ts +27 -6
- package/dist/components/data-entry/radio.js +88 -43
- package/dist/components/data-entry/search-select.js +8 -1
- package/dist/components/data-entry/select.js +2 -0
- package/dist/components/data-entry/switch.d.ts +6 -3
- package/dist/components/data-entry/switch.js +34 -11
- package/dist/components/data-entry/textarea.js +3 -1
- package/dist/components/data-entry/tree-utils.d.ts +10 -48
- package/dist/components/data-entry/tree-utils.js +1 -154
- package/dist/components/layout/mobile-shell.d.ts +1 -1
- package/dist/components/layout/mobile-shell.js +2 -0
- package/dist/components/navigation/menubar.d.ts +10 -3
- package/dist/components/navigation/steps.d.ts +1 -1
- package/dist/components/navigation/steps.js +3 -1
- package/dist/components/navigation/tabs-scroll.d.ts +27 -0
- package/dist/components/navigation/tabs-scroll.js +52 -1
- package/dist/components/navigation/tabs.d.ts +2 -2
- package/dist/components/navigation/tabs.js +88 -23
- package/dist/components/ui/index.d.ts +0 -1
- package/dist/components/ui/index.js +0 -1
- package/dist/components/ui/segmented.d.ts +15 -1
- package/dist/components/ui/segmented.js +13 -6
- package/dist/i18n/messages/en.json +48 -2
- package/dist/i18n/messages/ja.json +46 -2
- package/dist/i18n/messages/vi.json +47 -2
- package/dist/lib/datetime/picker-format.d.ts +18 -0
- package/dist/lib/datetime/picker-format.js +35 -1
- package/dist/lib/field-a11y.d.ts +12 -0
- package/dist/lib/field-a11y.js +14 -1
- package/dist/lib/tree.d.ts +53 -0
- package/dist/lib/tree.js +155 -0
- package/dist/props/components/data-display.prop.d.ts +194 -1
- package/dist/props/components/data-entry.prop.d.ts +307 -99
- package/dist/props/components/index.d.ts +1 -1
- package/dist/props/components/layout.prop.d.ts +12 -1
- package/dist/props/components/navigation.prop.d.ts +76 -1
- package/dist/props/registry.d.ts +156 -19
- package/dist/props/registry.js +218 -18
- package/dist/props/vocabulary/data.prop.d.ts +12 -0
- package/dist/props/vocabulary/index.d.ts +2 -2
- package/dist/props/vocabulary/layout.prop.d.ts +11 -0
- package/dist/styles/badge-layout.css +4 -0
- package/dist/styles/card-layout.css +6 -6
- package/dist/styles/control.css +22 -6
- package/dist/styles/data-display-layout.css +385 -68
- package/dist/styles/data-entry-layout.css +64 -0
- package/dist/styles/focus-ring.css +4 -2
- package/dist/styles/navigation-layout.css +25 -0
- package/dist/styles/shell-layout.css +7 -1
- package/dist/styles/table-layout.css +34 -0
- package/dist/tokens/base.css +3 -0
- package/dist/tokens/components/chat-bubble.css +36 -0
- package/dist/tokens/components/chat-composer.css +19 -0
- package/dist/tokens/components/data-display.css +9 -6
- package/dist/tokens/components/descriptions.css +4 -0
- package/dist/tokens/components/navigation.css +6 -0
- package/dist/tokens/components/segmented.css +0 -2
- package/dist/tokens/components/shell.css +3 -0
- package/dist/tokens/components/table.css +3 -0
- package/dist/tokens/components/tree.css +27 -0
- package/dist/tokens/foundation.css +7 -0
- package/docs/COMPONENTS.md +14 -14
- package/docs/CONSUMER-RULES.md +24 -1
- package/docs/FRAME-COVERAGE-LEDGER.md +1 -1
- package/docs/FRAME-COVERAGE-REPORT.md +9 -7
- package/docs/TOKENS.md +65 -0
- package/docs/data-display/badge.tsx +37 -0
- package/docs/data-display/chat-bubble.tsx +397 -0
- package/docs/data-display/data-table/index.tsx +18 -0
- package/docs/data-display/popover.tsx +41 -1
- package/docs/data-display/progress.tsx +28 -0
- package/docs/data-display/timeline.tsx +46 -0
- package/docs/data-display/tree.tsx +394 -0
- package/docs/data-entry/calendar.tsx +2 -2
- package/docs/data-entry/chat-composer.tsx +464 -0
- package/docs/data-entry/chat-suggestion.tsx +301 -0
- package/docs/data-entry/date-picker.tsx +143 -3
- package/docs/data-entry/form/examples/invoice-form.tsx +2 -2
- package/docs/data-entry/form-field/examples/a11y-contract.tsx +3 -5
- package/docs/data-entry/form.tsx +4 -4
- package/docs/data-entry/number-input.tsx +31 -0
- package/docs/data-entry/segmented.tsx +103 -1
- package/docs/layout/mobile-shell.tsx +5 -0
- package/docs/navigation/tabs.tsx +149 -0
- package/docs/roadmap/ai-chat-components.md +207 -0
- package/docs/roadmap/antd-parity.md +155 -0
- package/docs/roadmap/badge-tag-chip-count.md +172 -0
- package/docs/roadmap/list-masonry.md +159 -0
- package/docs/roadmap/parity-audit-data-display-feedback.md +567 -0
- package/docs/roadmap/parity-audit-data-entry.md +357 -0
- package/docs/roadmap/parity-audit-layout-navigation-general.md +464 -0
- package/docs/roadmap/parity-backlog.md +78 -0
- package/docs/roadmap/tree-components.md +151 -0
- package/docs/showcase/acme-portal.tsx +7 -0
- package/docs/showcase/table-tree-rows.tsx +4 -4
- package/package.json +13 -23
- package/scripts/ui-audit.mjs +17 -4
- package/dist/components/data-display/tree-list.d.ts +0 -13
- package/dist/components/data-display/tree-list.js +0 -26
- package/dist/components/data-entry/date-range-picker.d.ts +0 -11
- package/dist/components/data-entry/date-range-picker.js +0 -349
- package/dist/components/data-entry/month-picker.d.ts +0 -10
- package/dist/components/data-entry/month-picker.js +0 -241
- package/dist/components/data-entry/month-range-picker.d.ts +0 -10
- package/dist/components/data-entry/month-range-picker.js +0 -301
- package/dist/components/ui/date-range-picker.d.ts +0 -1
- package/dist/components/ui/date-range-picker.js +0 -2
- package/docs/data-display/tree-list.tsx +0 -107
- package/docs/data-entry/date-range-picker.tsx +0 -175
- package/docs/data-entry/month-picker.tsx +0 -101
- package/docs/data-entry/month-range-picker.tsx +0 -115
|
@@ -1,154 +1 @@
|
|
|
1
|
-
|
|
2
|
-
if (value == null || typeof value === "boolean") return "";
|
|
3
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint") {
|
|
4
|
-
return String(value);
|
|
5
|
-
}
|
|
6
|
-
if (Array.isArray(value)) {
|
|
7
|
-
return value.map((item) => reactNodeText(item)).join("");
|
|
8
|
-
}
|
|
9
|
-
return "";
|
|
10
|
-
}
|
|
11
|
-
function unknownText(value) {
|
|
12
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint") {
|
|
13
|
-
return String(value);
|
|
14
|
-
}
|
|
15
|
-
return "";
|
|
16
|
-
}
|
|
17
|
-
function normalizeTreeOptions(nodes, fieldNames) {
|
|
18
|
-
if (!nodes?.length) return [];
|
|
19
|
-
const labelKey = fieldNames?.label ?? "label";
|
|
20
|
-
const valueKey = fieldNames?.value ?? "value";
|
|
21
|
-
const childrenKey = fieldNames?.children ?? "children";
|
|
22
|
-
return nodes.map((node) => {
|
|
23
|
-
const children = node[childrenKey];
|
|
24
|
-
const value = unknownText(node[valueKey]);
|
|
25
|
-
const label = node[labelKey];
|
|
26
|
-
return {
|
|
27
|
-
value,
|
|
28
|
-
label: label ?? value,
|
|
29
|
-
disabled: Boolean(node.disabled),
|
|
30
|
-
disableCheckbox: Boolean(node.disableCheckbox),
|
|
31
|
-
isLeaf: node.isLeaf,
|
|
32
|
-
children: Array.isArray(children) ? normalizeTreeOptions(children, fieldNames) : void 0
|
|
33
|
-
};
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
function getNodeByPath(options, path) {
|
|
37
|
-
const chain = [];
|
|
38
|
-
let level = options;
|
|
39
|
-
for (const segment of path) {
|
|
40
|
-
const found = level.find((n) => n.value === segment);
|
|
41
|
-
if (!found) break;
|
|
42
|
-
chain.push(found);
|
|
43
|
-
level = found.children ?? [];
|
|
44
|
-
}
|
|
45
|
-
return chain;
|
|
46
|
-
}
|
|
47
|
-
function getOptionsAtPath(options, path) {
|
|
48
|
-
if (!path.length) return options;
|
|
49
|
-
const chain = getNodeByPath(options, path);
|
|
50
|
-
return chain.at(-1)?.children ?? [];
|
|
51
|
-
}
|
|
52
|
-
function formatPathLabels(chain, separator = " / ") {
|
|
53
|
-
return chain.map((n) => reactNodeText(n.label)).join(separator);
|
|
54
|
-
}
|
|
55
|
-
function collectLeafPaths(options, prefix = [], root = options) {
|
|
56
|
-
const out = [];
|
|
57
|
-
for (const node of options) {
|
|
58
|
-
const path = [...prefix, node.value];
|
|
59
|
-
const hasChildren = (node.children?.length ?? 0) > 0;
|
|
60
|
-
if (!hasChildren || node.isLeaf === true) {
|
|
61
|
-
out.push({ path, labels: getNodeByPath(root, path).map((n) => reactNodeText(n.label)) });
|
|
62
|
-
}
|
|
63
|
-
if (hasChildren) out.push(...collectLeafPaths(node.children, path, root));
|
|
64
|
-
}
|
|
65
|
-
return out;
|
|
66
|
-
}
|
|
67
|
-
function collectAllPaths(options, prefix = [], root = options) {
|
|
68
|
-
const out = [];
|
|
69
|
-
for (const node of options) {
|
|
70
|
-
const path = [...prefix, node.value];
|
|
71
|
-
out.push({ path, labels: getNodeByPath(root, path).map((n) => reactNodeText(n.label)) });
|
|
72
|
-
if (node.children?.length) out.push(...collectAllPaths(node.children, path, root));
|
|
73
|
-
}
|
|
74
|
-
return out;
|
|
75
|
-
}
|
|
76
|
-
function pathKey(path) {
|
|
77
|
-
return path.join("\0");
|
|
78
|
-
}
|
|
79
|
-
function pathsEqual(a, b) {
|
|
80
|
-
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
81
|
-
}
|
|
82
|
-
function filterTreeOptions(options, query, filter) {
|
|
83
|
-
const q = query.trim().toLowerCase();
|
|
84
|
-
if (!q) return [];
|
|
85
|
-
const paths = collectLeafPaths(options);
|
|
86
|
-
return paths.filter(({ path }) => {
|
|
87
|
-
const chain = getNodeByPath(options, path);
|
|
88
|
-
if (filter) return filter(query, chain);
|
|
89
|
-
return chain.some((n) => reactNodeText(n.label).toLowerCase().includes(q));
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
function getDescendantValues(node) {
|
|
93
|
-
const values = [node.value];
|
|
94
|
-
for (const child of node.children ?? []) values.push(...getDescendantValues(child));
|
|
95
|
-
return values;
|
|
96
|
-
}
|
|
97
|
-
function flattenVisibleTree(options, expandedKeys, depth = 0) {
|
|
98
|
-
const out = [];
|
|
99
|
-
for (const node of options) {
|
|
100
|
-
const hasChildren = (node.children?.length ?? 0) > 0 && node.isLeaf !== true;
|
|
101
|
-
out.push({ node, depth, hasChildren });
|
|
102
|
-
if (hasChildren && expandedKeys.has(node.value)) {
|
|
103
|
-
out.push(...flattenVisibleTree(node.children, expandedKeys, depth + 1));
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return out;
|
|
107
|
-
}
|
|
108
|
-
function filterVisibleTree(options, query) {
|
|
109
|
-
const q = query.trim().toLowerCase();
|
|
110
|
-
if (!q) return flattenVisibleTree(options, new Set(collectAllExpandableKeys(options)));
|
|
111
|
-
function matches(nodes, depth) {
|
|
112
|
-
return nodes.flatMap((node) => {
|
|
113
|
-
const children = node.isLeaf ? [] : matches(node.children ?? [], depth + 1);
|
|
114
|
-
if (!reactNodeText(node.label).toLowerCase().includes(q) && children.length === 0) return [];
|
|
115
|
-
return [{ node, depth, hasChildren: children.length > 0 }, ...children];
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
return matches(options, 0);
|
|
119
|
-
}
|
|
120
|
-
function collectAllExpandableKeys(options) {
|
|
121
|
-
const keys = [];
|
|
122
|
-
for (const node of options) {
|
|
123
|
-
if ((node.children?.length ?? 0) > 0 && node.isLeaf !== true) {
|
|
124
|
-
keys.push(node.value);
|
|
125
|
-
keys.push(...collectAllExpandableKeys(node.children));
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return keys;
|
|
129
|
-
}
|
|
130
|
-
function findNodeByValue(options, value) {
|
|
131
|
-
for (const node of options) {
|
|
132
|
-
if (node.value === value) return node;
|
|
133
|
-
const nested = node.children ? findNodeByValue(node.children, value) : void 0;
|
|
134
|
-
if (nested) return nested;
|
|
135
|
-
}
|
|
136
|
-
return void 0;
|
|
137
|
-
}
|
|
138
|
-
export {
|
|
139
|
-
collectAllExpandableKeys,
|
|
140
|
-
collectAllPaths,
|
|
141
|
-
collectLeafPaths,
|
|
142
|
-
filterTreeOptions,
|
|
143
|
-
filterVisibleTree,
|
|
144
|
-
findNodeByValue,
|
|
145
|
-
flattenVisibleTree,
|
|
146
|
-
formatPathLabels,
|
|
147
|
-
getDescendantValues,
|
|
148
|
-
getNodeByPath,
|
|
149
|
-
getOptionsAtPath,
|
|
150
|
-
normalizeTreeOptions,
|
|
151
|
-
pathKey,
|
|
152
|
-
pathsEqual,
|
|
153
|
-
reactNodeText
|
|
154
|
-
};
|
|
1
|
+
export * from "../../lib/tree.js";
|
|
@@ -18,4 +18,4 @@ export type { MobileShellProp, MobileShellProp as MobileShellProps, } from "../.
|
|
|
18
18
|
* IS the app's primary navigation). They are the same strings in all three locales, and a fourth
|
|
19
19
|
* identical copy under `layout.mobileShell.*` would be three files of duplication to keep in sync.
|
|
20
20
|
*/
|
|
21
|
-
export declare function MobileShell({ statusBar, header, children, actions, tabBar, height, className, }: MobileShellProp): import("react").JSX.Element;
|
|
21
|
+
export declare function MobileShell({ statusBar, header, children, actions, tabBar, height, width, className, }: MobileShellProp): import("react").JSX.Element;
|
|
@@ -9,6 +9,7 @@ function MobileShell({
|
|
|
9
9
|
actions,
|
|
10
10
|
tabBar,
|
|
11
11
|
height = "viewport",
|
|
12
|
+
width = "fill",
|
|
12
13
|
className
|
|
13
14
|
}) {
|
|
14
15
|
const { t } = useTranslation();
|
|
@@ -17,6 +18,7 @@ function MobileShell({
|
|
|
17
18
|
{
|
|
18
19
|
"data-slot": "mobile-shell",
|
|
19
20
|
"data-height": height === "viewport" ? void 0 : height,
|
|
21
|
+
"data-width": width === "fill" ? void 0 : width,
|
|
20
22
|
className: cn("ui-mobile-shell", className),
|
|
21
23
|
children: [
|
|
22
24
|
(statusBar !== void 0 || header !== void 0) && /* @__PURE__ */ jsxs("header", { className: "ui-mobile-shell-chrome", "aria-label": t("layout.appShell.headerLabel"), children: [
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import * as MenubarPrimitive from "@radix-ui/react-menubar";
|
|
3
3
|
export declare const Menubar: React.ForwardRefExoticComponent<MenubarPrimitive.MenubarProps & React.RefAttributes<HTMLDivElement>>;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Annotated rather than inferred, and that is load-bearing.
|
|
6
|
+
*
|
|
7
|
+
* Radix types this one through `createContextScope`, so its INFERRED type reaches into
|
|
8
|
+
* `@radix-ui/react-context` for a `Scope` — and TypeScript then refuses to emit a declaration it
|
|
9
|
+
* cannot name portably (TS2883) unless that package is a DIRECT dependency. It was one, purely to
|
|
10
|
+
* satisfy this line: nothing in the tree ever imported it. Naming the props here severs that,
|
|
11
|
+
* which is what let the dependency go.
|
|
12
|
+
*/
|
|
13
|
+
export declare const MenubarMenu: (props: React.ComponentProps<typeof MenubarPrimitive.Menu>) => React.ReactNode;
|
|
7
14
|
export declare function MenubarTrigger(props: React.ComponentProps<typeof MenubarPrimitive.Trigger>): React.JSX.Element;
|
|
8
15
|
export declare const MenubarContent: React.ForwardRefExoticComponent<Omit<MenubarPrimitive.MenubarContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
9
16
|
export declare const MenubarItem: React.ForwardRefExoticComponent<Omit<MenubarPrimitive.MenubarItemProps & React.RefAttributes<HTMLDivElement>, "ref"> & {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import type { StepsProp } from "../../props/components/navigation.prop.js";
|
|
3
3
|
export type { StepsProp, StepsProp as StepsProps, StepItemProp, StepStatusProp, StepsSeparatorProp, StepsTypeProp, } from "../../props/components/navigation.prop.js";
|
|
4
|
-
export declare function Steps({ items, value: current, defaultValue, status: currentStatus, orientation, type, size, titlePlacement, separator, percent, onValueChange, className, }: StepsProp): React.JSX.Element;
|
|
4
|
+
export declare function Steps({ items, value: current, defaultValue, status: currentStatus, orientation, type, size, titlePlacement, separator, percent, onValueChange, className, ...props }: StepsProp): React.JSX.Element;
|
|
@@ -50,7 +50,8 @@ function Steps({
|
|
|
50
50
|
separator = "chevron",
|
|
51
51
|
percent,
|
|
52
52
|
onValueChange,
|
|
53
|
-
className
|
|
53
|
+
className,
|
|
54
|
+
...props
|
|
54
55
|
}) {
|
|
55
56
|
const { t } = useTranslation();
|
|
56
57
|
const base = defaultValue;
|
|
@@ -69,6 +70,7 @@ function Steps({
|
|
|
69
70
|
"data-type": type,
|
|
70
71
|
className: cn("flex w-full", inline ? "ui-steps-inline" : "ui-steps-list", className),
|
|
71
72
|
"aria-label": t("navigation.steps.ariaLabel"),
|
|
73
|
+
...props,
|
|
72
74
|
children: items.map((item, index) => {
|
|
73
75
|
const absoluteIndex = base + index;
|
|
74
76
|
const stepStatus = resolveStepStatus(absoluteIndex, current, item.status, currentStatus);
|
|
@@ -34,3 +34,30 @@ export declare function syncActiveTabIntoView(list: HTMLElement | null, behavior
|
|
|
34
34
|
* pre-fix behaviour instead of throwing.
|
|
35
35
|
*/
|
|
36
36
|
export declare function useKeepActiveTabVisible(listRef: React.RefObject<HTMLElement | null>): void;
|
|
37
|
+
/**
|
|
38
|
+
* The values of the triggers that are NOT fully inside the strip's scrollport, in document order.
|
|
39
|
+
*
|
|
40
|
+
* Pure apart from the two rect reads — `values` is the `items` array's own order, and the triggers
|
|
41
|
+
* are matched to it by INDEX rather than by DOM id: `TabsTrigger` hands its `value` to React Aria
|
|
42
|
+
* as the collection KEY, and the rendered `id` attribute is RAC-generated, so the element carries
|
|
43
|
+
* no readable handle back to the item it came from.
|
|
44
|
+
*
|
|
45
|
+
* This is the measurement behind `overflow="menu"`. It is separated from the hook below so it can
|
|
46
|
+
* be exercised on plain numbers — jsdom lays nothing out, so the only honest unit assertions about
|
|
47
|
+
* it are the ones that hand it real geometry.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveHiddenTabValues(list: HTMLElement, values: readonly string[]): string[];
|
|
50
|
+
/**
|
|
51
|
+
* Keeps `onChange` fed with the values currently out of the scrollport, recomputing on the two
|
|
52
|
+
* things that can move them: the strip resizing (a viewport change, a font swap, a tab added or
|
|
53
|
+
* removed) and the strip scrolling (the user swiping, or `useKeepActiveTabVisible` re-pinning a
|
|
54
|
+
* trigger).
|
|
55
|
+
*
|
|
56
|
+
* `values` is joined into the effect key rather than passed as a dependency, so a consumer that
|
|
57
|
+
* rebuilds its `items` array on every render does not re-arm the observers on every render.
|
|
58
|
+
*
|
|
59
|
+
* The first measurement ALWAYS reports, even when nothing is hidden. Suppressing it looks harmless
|
|
60
|
+
* and is not: on a remount the local `last` resets while the caller's state does not, so a strip
|
|
61
|
+
* that had overflowed and then stopped would keep a stale menu forever.
|
|
62
|
+
*/
|
|
63
|
+
export declare function useTabsOverflowValues(listRef: React.RefObject<HTMLElement | null>, values: readonly string[] | undefined, onChange: (hidden: string[]) => void): void;
|
|
@@ -67,10 +67,61 @@ function useKeepActiveTabVisible(listRef) {
|
|
|
67
67
|
};
|
|
68
68
|
}, [listRef]);
|
|
69
69
|
}
|
|
70
|
+
function resolveHiddenTabValues(list, values) {
|
|
71
|
+
const scrollport = list.getBoundingClientRect();
|
|
72
|
+
const triggers = [...list.querySelectorAll('[role="tab"]')];
|
|
73
|
+
const hidden = [];
|
|
74
|
+
triggers.forEach((trigger, index) => {
|
|
75
|
+
const value = values[index];
|
|
76
|
+
if (value === void 0) return;
|
|
77
|
+
if (!isTabFullyVisible(scrollport, trigger.getBoundingClientRect())) hidden.push(value);
|
|
78
|
+
});
|
|
79
|
+
return hidden;
|
|
80
|
+
}
|
|
81
|
+
function useTabsOverflowValues(listRef, values, onChange) {
|
|
82
|
+
const onChangeRef = React.useRef(onChange);
|
|
83
|
+
React.useEffect(() => {
|
|
84
|
+
onChangeRef.current = onChange;
|
|
85
|
+
});
|
|
86
|
+
const key = values === void 0 ? null : values.join(" ");
|
|
87
|
+
React.useEffect(() => {
|
|
88
|
+
if (key === null) return void 0;
|
|
89
|
+
const list = listRef.current;
|
|
90
|
+
if (!list) return void 0;
|
|
91
|
+
const all = key === "" ? [] : key.split(" ");
|
|
92
|
+
let last = null;
|
|
93
|
+
const measure = () => {
|
|
94
|
+
const hidden = resolveHiddenTabValues(list, all);
|
|
95
|
+
const next = hidden.join(" ");
|
|
96
|
+
if (next === last) return;
|
|
97
|
+
last = next;
|
|
98
|
+
onChangeRef.current(hidden);
|
|
99
|
+
};
|
|
100
|
+
measure();
|
|
101
|
+
list.addEventListener("scroll", measure, { passive: true });
|
|
102
|
+
const cleanups = [
|
|
103
|
+
() => {
|
|
104
|
+
list.removeEventListener("scroll", measure);
|
|
105
|
+
}
|
|
106
|
+
];
|
|
107
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
108
|
+
const resizeObserver = new ResizeObserver(measure);
|
|
109
|
+
resizeObserver.observe(list);
|
|
110
|
+
cleanups.push(() => {
|
|
111
|
+
resizeObserver.disconnect();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return () => {
|
|
115
|
+
for (const cleanup of cleanups) cleanup();
|
|
116
|
+
};
|
|
117
|
+
}, [key, listRef]);
|
|
118
|
+
}
|
|
70
119
|
export {
|
|
71
120
|
isTabFullyVisible,
|
|
72
121
|
prefersReducedMotion,
|
|
122
|
+
resolveHiddenTabValues,
|
|
73
123
|
resolveTabScrollTarget,
|
|
74
124
|
syncActiveTabIntoView,
|
|
75
|
-
useKeepActiveTabVisible
|
|
125
|
+
useKeepActiveTabVisible,
|
|
126
|
+
useTabsOverflowValues
|
|
76
127
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import type { TabItemProp, TabsProp } from "../../props/components/navigation.prop.js";
|
|
3
|
-
export type { TabItemProp, TabsProp, TabsVariantProp, TabsPlacementProp, TabsExtraProp, } from "../../props/components/navigation.prop.js";
|
|
3
|
+
export type { TabItemProp, TabsProp, TabsVariantProp, TabsPlacementProp, TabsExtraProp, TabsOverflowProp, } from "../../props/components/navigation.prop.js";
|
|
4
4
|
export type TabsOrientation = "vertical" | "horizontal";
|
|
5
5
|
export type TabsItem = TabItemProp;
|
|
6
6
|
export type TabsProps = Omit<React.ComponentPropsWithoutRef<"div">, "defaultValue" | "dir"> & TabsProp & {
|
|
@@ -8,7 +8,7 @@ export type TabsProps = Omit<React.ComponentPropsWithoutRef<"div">, "defaultValu
|
|
|
8
8
|
dir?: "ltr" | "rtl";
|
|
9
9
|
activationMode?: "manual" | "automatic";
|
|
10
10
|
};
|
|
11
|
-
export declare function Tabs({ className, orientation, items, value, defaultValue, onValueChange, activationMode, dir, variant, tabPlacement, size, centered, extra, destroyOnHidden, onEdit, addIcon, hideAdd, listClassName, contentClassName, children, ...props }: TabsProps): React.JSX.Element;
|
|
11
|
+
export declare function Tabs({ className, orientation, items, value, defaultValue, onValueChange, activationMode, dir, variant, tabPlacement, size, centered, extra, destroyOnHidden, onEdit, addIcon, hideAdd, closeIcon, onTabClick, overflow, listClassName, contentClassName, children, ...props }: TabsProps): React.JSX.Element;
|
|
12
12
|
export declare const TabsList: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
|
|
13
13
|
variant?: "default" | "line";
|
|
14
14
|
/**
|
|
@@ -7,10 +7,16 @@ import {
|
|
|
7
7
|
TabPanel as AriaTabPanel,
|
|
8
8
|
Tabs as AriaTabs
|
|
9
9
|
} from "react-aria-components";
|
|
10
|
-
import { Plus, X } from "lucide-react";
|
|
10
|
+
import { MoreHorizontal, Plus, X } from "lucide-react";
|
|
11
11
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
12
12
|
import { cn } from "../../lib/utils.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
DropdownMenu,
|
|
15
|
+
DropdownMenuContent,
|
|
16
|
+
DropdownMenuItem,
|
|
17
|
+
DropdownMenuTrigger
|
|
18
|
+
} from "./dropdown-menu.js";
|
|
19
|
+
import { useKeepActiveTabVisible, useTabsOverflowValues } from "./tabs-scroll.js";
|
|
14
20
|
const TabsFrameContext = React.createContext({
|
|
15
21
|
orientation: "horizontal",
|
|
16
22
|
selectionSuppressed: false
|
|
@@ -69,6 +75,9 @@ function Tabs({
|
|
|
69
75
|
onEdit,
|
|
70
76
|
addIcon,
|
|
71
77
|
hideAdd,
|
|
78
|
+
closeIcon,
|
|
79
|
+
onTabClick,
|
|
80
|
+
overflow = "scroll",
|
|
72
81
|
listClassName,
|
|
73
82
|
contentClassName,
|
|
74
83
|
children,
|
|
@@ -86,6 +95,19 @@ function Tabs({
|
|
|
86
95
|
[resolvedOrientation, selectionSuppressed]
|
|
87
96
|
);
|
|
88
97
|
const editable = variant === "editable-card";
|
|
98
|
+
const collapsible = items != null && overflow === "menu";
|
|
99
|
+
const listRef = React.useRef(null);
|
|
100
|
+
const [hiddenValues, setHiddenValues] = React.useState([]);
|
|
101
|
+
const itemValues = React.useMemo(
|
|
102
|
+
() => collapsible ? items.map((item) => item.value) : void 0,
|
|
103
|
+
[collapsible, items]
|
|
104
|
+
);
|
|
105
|
+
const handleHiddenChange = React.useCallback((next) => {
|
|
106
|
+
setHiddenValues(
|
|
107
|
+
(prev) => prev.length === next.length && prev.every((value2, index) => value2 === next[index]) ? prev : next
|
|
108
|
+
);
|
|
109
|
+
}, []);
|
|
110
|
+
useTabsOverflowValues(listRef, itemValues, handleHiddenChange);
|
|
89
111
|
const [mirroredValue, setMirroredValue] = React.useState(
|
|
90
112
|
value ?? resolvedDefault
|
|
91
113
|
);
|
|
@@ -101,8 +123,9 @@ function Tabs({
|
|
|
101
123
|
[onValueChange]
|
|
102
124
|
);
|
|
103
125
|
const { start: extraStart, end: extraEnd } = resolveTabsExtra(extra);
|
|
126
|
+
const overflowItems = collapsible ? items.filter((item) => hiddenValues.includes(item.value)) : [];
|
|
104
127
|
const showAdd = editable && !hideAdd && Boolean(onEdit);
|
|
105
|
-
const needsBar = Boolean(extraStart || extraEnd || showAdd);
|
|
128
|
+
const needsBar = Boolean(extraStart || extraEnd || showAdd || collapsible);
|
|
106
129
|
const card = variant === "card" || variant === "editable-card";
|
|
107
130
|
const sizeTriggerClassName = cn(
|
|
108
131
|
size === "sm" && "min-h-[var(--tabs-trigger-height-sm)] px-[var(--tabs-trigger-padding-x-sm)] text-[length:var(--tabs-trigger-font-size-sm)]",
|
|
@@ -112,6 +135,7 @@ function Tabs({
|
|
|
112
135
|
const list = items ? /* @__PURE__ */ jsx(
|
|
113
136
|
TabsList,
|
|
114
137
|
{
|
|
138
|
+
ref: listRef,
|
|
115
139
|
"data-slot": "tabs-list",
|
|
116
140
|
variant: variant === "line" ? "line" : "default",
|
|
117
141
|
className: cn(
|
|
@@ -136,12 +160,18 @@ function Tabs({
|
|
|
136
160
|
// the same block inset — so the two construction paths now agree. A service that
|
|
137
161
|
// wants the active bar parked back on the hairline raises `--tabs-indicator-offset`,
|
|
138
162
|
// which exists for exactly that.
|
|
139
|
-
|
|
163
|
+
// `w-full` is scoped to the HORIZONTAL axis. Unscoped it also applied when the root is
|
|
164
|
+
// vertical, where the strip is a COLUMN beside the panel: `width: 100%` made the strip
|
|
165
|
+
// claim the whole row and the panel collapsed to its own min-content floor. Measured at a
|
|
166
|
+
// 1232px root: strip 1133.72px (92%) / panel 90.28px (7%). And it is not a layer problem —
|
|
167
|
+
// tailwind-merge drops the base `w-fit` as a same-group conflict, so `w-fit` never reached
|
|
168
|
+
// the DOM at all. Scoping puts it back on the axis it was written for.
|
|
169
|
+
variant === "line" && "my-[calc(-1_*_var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width))))] h-auto justify-start border-b px-[var(--tabs-list-line-space-inset)] py-[calc(var(--tabs-list-line-space-inset)_+_var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width))))] data-[orientation=horizontal]:w-full",
|
|
140
170
|
// CARD strip. The list keeps `data-variant="default"` on purpose (a hand-composed
|
|
141
171
|
// <TabsList> must be unaffected), so the card face is selected from the ROOT — but the
|
|
142
172
|
// three properties the base list already claims as utilities (`bg-muted`, `p-1`,
|
|
143
173
|
// `rounded-lg`) have to be replaced with utilities too, or the components layer loses.
|
|
144
|
-
card && "
|
|
174
|
+
card && "items-end justify-start gap-[var(--tabs-card-list-space-gap)] rounded-[var(--tabs-card-list-radius)] p-[var(--tabs-card-list-space-inset)] data-[orientation=horizontal]:w-full data-[variant=default]:bg-transparent",
|
|
145
175
|
// CENTERED. Two independent moves, because the strip has two shapes: the pill/card strip
|
|
146
176
|
// is `w-fit` (auto inline margins centre the BOX) and the line strip is `w-full`
|
|
147
177
|
// (`justify-content` centres its CONTENT). `safe` keeps the overflow rule the strip
|
|
@@ -157,6 +187,7 @@ function Tabs({
|
|
|
157
187
|
value: item.value,
|
|
158
188
|
disabled: item.disabled,
|
|
159
189
|
"aria-keyshortcuts": removable ? "Delete" : void 0,
|
|
190
|
+
onClick: onTabClick ? (event) => onTabClick(item.value, event) : void 0,
|
|
160
191
|
onKeyDown: removable ? (event) => {
|
|
161
192
|
if (event.key !== "Delete" && event.key !== "Backspace") return;
|
|
162
193
|
event.preventDefault();
|
|
@@ -220,7 +251,7 @@ function Tabs({
|
|
|
220
251
|
event.stopPropagation();
|
|
221
252
|
onEdit?.(item.value, "remove");
|
|
222
253
|
},
|
|
223
|
-
children: item.closeIcon ?? /* @__PURE__ */ jsx(X, { className: "ui-tabs-tab-remove-icon", "aria-hidden": "true" })
|
|
254
|
+
children: item.closeIcon ?? closeIcon ?? /* @__PURE__ */ jsx(X, { className: "ui-tabs-tab-remove-icon", "aria-hidden": "true" })
|
|
224
255
|
}
|
|
225
256
|
)
|
|
226
257
|
) : null
|
|
@@ -242,8 +273,8 @@ function Tabs({
|
|
|
242
273
|
"data-centered": centered ? "true" : void 0,
|
|
243
274
|
orientation: resolvedOrientation,
|
|
244
275
|
keyboardActivation: activationMode,
|
|
245
|
-
selectedKey: value,
|
|
246
|
-
defaultSelectedKey: value === void 0 ? resolvedDefault : void 0,
|
|
276
|
+
selectedKey: value ?? (collapsible ? mirroredValue : void 0),
|
|
277
|
+
defaultSelectedKey: value === void 0 && !collapsible ? resolvedDefault : void 0,
|
|
247
278
|
onSelectionChange: (key) => handleValueChange(String(key)),
|
|
248
279
|
className: cn(
|
|
249
280
|
// Structure only. The paint (and the placement flip, which is `order`/`flex-direction`)
|
|
@@ -281,20 +312,42 @@ function Tabs({
|
|
|
281
312
|
children: addIcon ?? /* @__PURE__ */ jsx(Plus, { className: "ui-tabs-add-icon", "aria-hidden": "true" })
|
|
282
313
|
}
|
|
283
314
|
) : null,
|
|
315
|
+
overflowItems.length > 0 ? /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
316
|
+
/* @__PURE__ */ jsx(
|
|
317
|
+
DropdownMenuTrigger,
|
|
318
|
+
{
|
|
319
|
+
className: "ui-tabs-overflow",
|
|
320
|
+
"aria-label": t("navigation.tabs.moreTabs"),
|
|
321
|
+
children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "ui-tabs-overflow-icon", "aria-hidden": "true" })
|
|
322
|
+
}
|
|
323
|
+
),
|
|
324
|
+
/* @__PURE__ */ jsx(DropdownMenuContent, { placement: "bottomEnd", children: overflowItems.map((item) => /* @__PURE__ */ jsx(
|
|
325
|
+
DropdownMenuItem,
|
|
326
|
+
{
|
|
327
|
+
disabled: item.disabled,
|
|
328
|
+
onSelect: () => handleValueChange(item.value),
|
|
329
|
+
children: item.label
|
|
330
|
+
},
|
|
331
|
+
item.value
|
|
332
|
+
)) })
|
|
333
|
+
] }) : null,
|
|
284
334
|
extraEnd ? /* @__PURE__ */ jsx("div", { "data-slot": "tabs-extra", "data-side": "end", className: "ui-tabs-extra", children: extraEnd }) : null
|
|
285
335
|
] }) : list,
|
|
286
|
-
items.map((item) =>
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
336
|
+
items.map((item) => {
|
|
337
|
+
const keepMounted = !destroyOnHidden || item.forceRender === true;
|
|
338
|
+
return /* @__PURE__ */ jsx(
|
|
339
|
+
TabsContent,
|
|
340
|
+
{
|
|
341
|
+
value: item.value,
|
|
342
|
+
"data-slot": "tabs-panel",
|
|
343
|
+
forceMount: keepMounted ? true : void 0,
|
|
344
|
+
hidden: keepMounted ? item.value !== activeValue : void 0,
|
|
345
|
+
className: contentClassName,
|
|
346
|
+
children: item.content
|
|
347
|
+
},
|
|
348
|
+
item.value
|
|
349
|
+
);
|
|
350
|
+
})
|
|
298
351
|
] }) : children
|
|
299
352
|
}
|
|
300
353
|
) });
|
|
@@ -330,7 +383,7 @@ const TabsList = React.forwardRef(
|
|
|
330
383
|
// — so the leading tab sat permanently outside the scrollport (an unreachable control at
|
|
331
384
|
// 320px, WCAG 2.2 SC 2.1.1). `safe` falls back to start alignment exactly when it
|
|
332
385
|
// overflows, and still centres whenever the tabs fit.
|
|
333
|
-
"group/tabs-list text-muted-foreground data-[variant=default]:bg-muted inline-flex w-fit max-w-full min-w-0 items-center justify-center-safe rounded-lg p-1 group-data-[orientation=vertical]/tabs:flex-col data-[orientation=horizontal]:[scrollbar-width:none] data-[orientation=horizontal]:overflow-x-auto data-[orientation=horizontal]:overflow-y-hidden data-[variant=line]:gap-
|
|
386
|
+
"group/tabs-list text-muted-foreground data-[variant=default]:bg-muted inline-flex w-fit max-w-full min-w-0 items-center justify-center-safe rounded-lg p-1 group-data-[orientation=vertical]/tabs:flex-col data-[orientation=horizontal]:[scrollbar-width:none] data-[orientation=horizontal]:overflow-x-auto data-[orientation=horizontal]:overflow-y-hidden data-[variant=line]:gap-[var(--tabs-list-line-space-gap)] data-[variant=line]:rounded-none data-[variant=line]:bg-transparent [&[data-orientation=horizontal]::-webkit-scrollbar]:hidden",
|
|
334
387
|
className
|
|
335
388
|
),
|
|
336
389
|
render: (domProps) => withDomProps("div", "tabs-list", props, domProps, { "data-variant": variant }),
|
|
@@ -341,7 +394,7 @@ const TabsList = React.forwardRef(
|
|
|
341
394
|
);
|
|
342
395
|
TabsList.displayName = "TabsList";
|
|
343
396
|
const TabsTrigger = React.forwardRef(
|
|
344
|
-
({ className, value, disabled, children, onKeyDown, ...props }, ref) => {
|
|
397
|
+
({ className, value, disabled, children, onKeyDown, onClick, ...props }, ref) => {
|
|
345
398
|
const { orientation, selectionSuppressed } = React.useContext(TabsFrameContext);
|
|
346
399
|
return /* @__PURE__ */ jsx(
|
|
347
400
|
AriaTab,
|
|
@@ -368,7 +421,7 @@ const TabsTrigger = React.forwardRef(
|
|
|
368
421
|
// The line indicator lives in src/styles/navigation-layout.css so it reads --tabs-indicator-*.
|
|
369
422
|
// Selected and focused stay visually distinct (WCAG 2.4.7): selected is a 1px hairline in the
|
|
370
423
|
// border, focused is the 2px ring plus its halo outside it.
|
|
371
|
-
"text-muted-foreground ring-offset-background hover:text-foreground ui-focus-ring data-[state=active]:bg-background data-[state=active]:text-foreground group-data-[variant=default]/tabs-list:data-[state=active]:border-primary/25 relative inline-flex flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start group-data-[variant=line]/tabs-list:border-e-0 group-data-[variant=line]/tabs-list:border-b-0 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none",
|
|
424
|
+
"text-muted-foreground ring-offset-background hover:text-foreground ui-focus-ring data-[state=active]:bg-background data-[state=active]:text-foreground group-data-[variant=default]/tabs-list:data-[state=active]:border-primary/25 relative inline-flex flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:flex-none group-data-[orientation=vertical]/tabs:justify-start group-data-[variant=line]/tabs-list:border-e-0 group-data-[variant=line]/tabs-list:border-b-0 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none",
|
|
372
425
|
className
|
|
373
426
|
),
|
|
374
427
|
render: (domProps, renderProps) => withDomProps("button", "tabs-trigger", { type: "button", ...props }, domProps, {
|
|
@@ -379,6 +432,18 @@ const TabsTrigger = React.forwardRef(
|
|
|
379
432
|
"aria-selected": renderProps.isSelected && !selectionSuppressed,
|
|
380
433
|
"data-orientation": orientation,
|
|
381
434
|
disabled,
|
|
435
|
+
// CHAINED, not spread — the same reason `onKeyDown` below is. `withDomProps` lays the
|
|
436
|
+
// caller's raw props down FIRST and RAC's merged ones second, so any handler RAC owns
|
|
437
|
+
// for this event would silently swallow the caller's. Caller first, then RAC's, and
|
|
438
|
+
// only while the caller has not called `preventDefault()`.
|
|
439
|
+
onClick: (event) => {
|
|
440
|
+
onClick?.(event);
|
|
441
|
+
if (!event.defaultPrevented) {
|
|
442
|
+
domProps.onClick?.(
|
|
443
|
+
event
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
},
|
|
382
447
|
onKeyDown: (event) => {
|
|
383
448
|
onKeyDown?.(event);
|
|
384
449
|
if (!event.defaultPrevented) {
|
|
@@ -15,7 +15,6 @@ export * from "./menubar.js";
|
|
|
15
15
|
export * from "./navigation-menu.js";
|
|
16
16
|
export * from "./resizable.js";
|
|
17
17
|
export * from "./date-picker.js";
|
|
18
|
-
export * from "./date-range-picker.js";
|
|
19
18
|
export * from "./dialog.js";
|
|
20
19
|
export { Separator } from "./separator.js";
|
|
21
20
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../feedback/tooltip.js";
|
|
@@ -16,7 +16,6 @@ export * from "./menubar.js";
|
|
|
16
16
|
export * from "./navigation-menu.js";
|
|
17
17
|
export * from "./resizable.js";
|
|
18
18
|
export * from "./date-picker.js";
|
|
19
|
-
export * from "./date-range-picker.js";
|
|
20
19
|
export * from "./dialog.js";
|
|
21
20
|
import { Separator } from "./separator.js";
|
|
22
21
|
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../feedback/tooltip.js";
|
|
@@ -40,7 +40,7 @@ export type SegmentedProp = {
|
|
|
40
40
|
export type SegmentedProps = SegmentedProp;
|
|
41
41
|
/**
|
|
42
42
|
* Segmented — one-of-N from a small, closed, always-visible set. The established enterprise
|
|
43
|
-
* `Segmented` control (docs/DESIGN-AUTHORITY.md) drawn on
|
|
43
|
+
* `Segmented` control (docs/DESIGN-AUTHORITY.md) drawn on react-aria-components' RadioGroup,
|
|
44
44
|
* which is this repo's authority for behaviour primitives.
|
|
45
45
|
*
|
|
46
46
|
* WHY NOT `ToggleGroup`. A ToggleGroup is a row of PRESSED buttons: `aria-pressed`, independently
|
|
@@ -53,5 +53,19 @@ export type SegmentedProps = SegmentedProp;
|
|
|
53
53
|
* The primitive gives roving tabindex, arrow-key traversal (RTL-aware), the radiogroup/radio roles
|
|
54
54
|
* and the hidden input a native form submit needs. This file adds that geometry and nothing
|
|
55
55
|
* else — the focus mark comes from `ui-focus-ring`, the ONE source in styles/focus-ring.css.
|
|
56
|
+
*
|
|
57
|
+
* "RTL-aware" IS true now, and it used not to be. On Radix it was: Radix read `dir` from its own
|
|
58
|
+
* `DirectionProvider`, this repo rendered none, and it never looked at `<html dir>` — measured in
|
|
59
|
+
* jsdom, with `document.documentElement.dir = "rtl"` ArrowLeft on the first option went to the
|
|
60
|
+
* LAST one, i.e. straight LTR traversal under a reversed layout, so the key that moves your eye
|
|
61
|
+
* left moved the selection right. The repair then was to hand Radix the ambient React Aria
|
|
62
|
+
* direction explicitly.
|
|
63
|
+
*
|
|
64
|
+
* On `react-aria-components` that hand-off is gone, because the primitive reads the SAME
|
|
65
|
+
* `useLocale()` itself. One source of truth, fed by `AppProvider`, nothing threaded through the
|
|
66
|
+
* call site — and `src/__tests__/rtl-arrow-direction.test.tsx` holds it: under an `ar-AE` locale
|
|
67
|
+
* ArrowLeft advances and ArrowRight retreats, and `<html dir="rtl">` alone still does not.
|
|
68
|
+
* `orientation` decides WHICH pair of arrows moves the focus; the locale decides which END of the
|
|
69
|
+
* row each of them means.
|
|
56
70
|
*/
|
|
57
71
|
export declare const Segmented: React.ForwardRefExoticComponent<SegmentedProp & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import
|
|
4
|
+
import { Radio as AriaRadio, RadioGroup as AriaRadioGroup } from "react-aria-components";
|
|
5
5
|
import { cn } from "../../lib/utils.js";
|
|
6
6
|
const Segmented = React.forwardRef(function Segmented2({
|
|
7
7
|
options,
|
|
@@ -18,7 +18,7 @@ const Segmented = React.forwardRef(function Segmented2({
|
|
|
18
18
|
...props
|
|
19
19
|
}, ref) {
|
|
20
20
|
return /* @__PURE__ */ jsx(
|
|
21
|
-
|
|
21
|
+
AriaRadioGroup,
|
|
22
22
|
{
|
|
23
23
|
ref,
|
|
24
24
|
id,
|
|
@@ -29,17 +29,24 @@ const Segmented = React.forwardRef(function Segmented2({
|
|
|
29
29
|
orientation: vertical ? "vertical" : "horizontal",
|
|
30
30
|
value,
|
|
31
31
|
defaultValue,
|
|
32
|
-
onValueChange,
|
|
33
|
-
disabled,
|
|
32
|
+
onChange: onValueChange,
|
|
33
|
+
isDisabled: disabled,
|
|
34
34
|
name,
|
|
35
35
|
...props,
|
|
36
36
|
children: options.map((option) => /* @__PURE__ */ jsxs(
|
|
37
|
-
|
|
37
|
+
AriaRadio,
|
|
38
38
|
{
|
|
39
39
|
value: option.value,
|
|
40
|
-
|
|
40
|
+
isDisabled: option.disabled,
|
|
41
41
|
"data-slot": "segmented-item",
|
|
42
42
|
className: "ui-segmented-item ui-focus-ring",
|
|
43
|
+
render: (domProps, state) => /* @__PURE__ */ jsx(
|
|
44
|
+
"label",
|
|
45
|
+
{
|
|
46
|
+
...domProps,
|
|
47
|
+
"data-state": state.isSelected ? "checked" : "unchecked"
|
|
48
|
+
}
|
|
49
|
+
),
|
|
43
50
|
children: [
|
|
44
51
|
option.icon == null ? null : /* @__PURE__ */ jsx(
|
|
45
52
|
"span",
|