@godxjp/ui 21.0.0 → 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/data-table.d.ts +18 -2
- package/dist/components/data-display/data-table.js +4 -0
- package/dist/components/data-display/index.d.ts +1 -1
- package/dist/components/data-display/popover.d.ts +19 -1
- package/dist/components/data-display/popover.js +9 -1
- package/dist/components/data-display/progress.d.ts +37 -0
- package/dist/components/data-display/progress.js +57 -5
- 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 +0 -6
- package/dist/components/data-entry/index.js +0 -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/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 +3 -2
- package/dist/i18n/messages/ja.json +3 -2
- package/dist/i18n/messages/vi.json +3 -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/props/components/data-display.prop.d.ts +7 -0
- package/dist/props/components/data-entry.prop.d.ts +180 -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 +16 -16
- package/dist/props/registry.js +16 -15
- 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 +15 -6
- package/dist/styles/data-display-layout.css +76 -13
- package/dist/styles/focus-ring.css +4 -2
- package/dist/styles/navigation-layout.css +25 -0
- package/dist/styles/shell-layout.css +5 -0
- package/dist/styles/table-layout.css +34 -0
- package/dist/tokens/components/data-display.css +9 -0
- package/dist/tokens/components/navigation.css +6 -0
- package/dist/tokens/components/segmented.css +0 -2
- package/dist/tokens/components/shell.css +2 -0
- package/dist/tokens/components/table.css +3 -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-REPORT.md +3 -6
- package/docs/TOKENS.md +65 -0
- package/docs/data-display/badge.tsx +37 -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-entry/calendar.tsx +2 -2
- 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/antd-parity.md +35 -34
- package/docs/roadmap/parity-audit-data-entry.md +178 -165
- package/docs/roadmap/parity-backlog.md +6 -7
- package/docs/showcase/acme-portal.tsx +7 -0
- package/package.json +11 -23
- package/scripts/ui-audit.mjs +17 -4
- 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-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
|
@@ -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",
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"openGrid": "Open month grid",
|
|
133
133
|
"previousYear": "Previous year",
|
|
134
134
|
"nextYear": "Next year",
|
|
135
|
-
"placeholder": "
|
|
135
|
+
"placeholder": "Select month"
|
|
136
136
|
},
|
|
137
137
|
"branchScope": {
|
|
138
138
|
"label": "Branch scope",
|
|
@@ -347,7 +347,8 @@
|
|
|
347
347
|
"navigation": {
|
|
348
348
|
"tabs": {
|
|
349
349
|
"addTab": "Add tab",
|
|
350
|
-
"removeTab": "Close tab"
|
|
350
|
+
"removeTab": "Close tab",
|
|
351
|
+
"moreTabs": "More tabs"
|
|
351
352
|
},
|
|
352
353
|
"steps": {
|
|
353
354
|
"ariaLabel": "Progress",
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"openGrid": "月選択を開く",
|
|
133
133
|
"previousYear": "前年へ",
|
|
134
134
|
"nextYear": "翌年へ",
|
|
135
|
-
"placeholder": "
|
|
135
|
+
"placeholder": "年月を選択"
|
|
136
136
|
},
|
|
137
137
|
"branchScope": {
|
|
138
138
|
"label": "ブランチ範囲",
|
|
@@ -340,7 +340,8 @@
|
|
|
340
340
|
"navigation": {
|
|
341
341
|
"tabs": {
|
|
342
342
|
"addTab": "タブを追加",
|
|
343
|
-
"removeTab": "タブを閉じる"
|
|
343
|
+
"removeTab": "タブを閉じる",
|
|
344
|
+
"moreTabs": "他のタブ"
|
|
344
345
|
},
|
|
345
346
|
"steps": {
|
|
346
347
|
"ariaLabel": "進行状況",
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"openGrid": "Mở chọn tháng",
|
|
133
133
|
"previousYear": "Năm trước",
|
|
134
134
|
"nextYear": "Năm sau",
|
|
135
|
-
"placeholder": "
|
|
135
|
+
"placeholder": "Chọn tháng"
|
|
136
136
|
},
|
|
137
137
|
"branchScope": {
|
|
138
138
|
"label": "Phạm vi chi nhánh",
|
|
@@ -341,7 +341,8 @@
|
|
|
341
341
|
"navigation": {
|
|
342
342
|
"tabs": {
|
|
343
343
|
"addTab": "Thêm tab",
|
|
344
|
-
"removeTab": "Đóng tab"
|
|
344
|
+
"removeTab": "Đóng tab",
|
|
345
|
+
"moreTabs": "Tab khác"
|
|
345
346
|
},
|
|
346
347
|
"steps": {
|
|
347
348
|
"ariaLabel": "Tiến trình",
|
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
import { type Locale } from "date-fns";
|
|
2
2
|
import type { DatePickerBaseProp, PickerDateFormatProp } from "../../props/components/data-entry.prop.js";
|
|
3
|
+
/** The granularity axis a picker selects at — `DatePickerProp["picker"]`, resolved. */
|
|
4
|
+
export type DatePickerPicker = NonNullable<DatePickerBaseProp["picker"]>;
|
|
3
5
|
/** Display is independent from the canonical value submitted by the field. */
|
|
4
6
|
export declare function formatPickerDate(date: Date | undefined, format: PickerDateFormatProp | undefined, locale: string, withTime?: boolean): string;
|
|
5
7
|
export declare function parsePickerDate(text: string, format: PickerDateFormatProp | undefined, parser?: (text: string) => Date | undefined, withTime?: boolean): Date | undefined;
|
|
6
8
|
export declare function pickerDateAllowed(date: Date, min: Date | undefined, max: Date | undefined, disabled?: (date: Date) => boolean): boolean;
|
|
7
9
|
/** Normalize a reporting period with the active calendar's week convention. */
|
|
8
10
|
export declare function pickerPeriodStart(date: Date, picker: DatePickerBaseProp["picker"], locale: Pick<Locale, "options">): Date;
|
|
11
|
+
/**
|
|
12
|
+
* ISO-8601 at the precision the picker actually selects.
|
|
13
|
+
*
|
|
14
|
+
* The standard defines reduced forms, and a month picker that submits `2026/03` is simply using
|
|
15
|
+
* none of them: `/` is not an ISO separator and a server parsing an ISO date will reject it. Each
|
|
16
|
+
* granularity gets the shortest ISO form that loses nothing:
|
|
17
|
+
* year → `2026` · month → `2026-03` · quarter → `2026-01` (the quarter's first month, which is
|
|
18
|
+
* what the value IS) · week → `2026-W07` (ISO week-numbering year + week) · date → `2026-03-01`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function toIsoPeriod(date: Date | undefined, picker: DatePickerPicker): string;
|
|
21
|
+
/**
|
|
22
|
+
* The inverse of `toIsoPeriod`: read back the reduced ISO form the field displays for this
|
|
23
|
+
* granularity. `date` is left to `parsePickerDate`, which already owns the full `yyyy-MM-dd` path
|
|
24
|
+
* (custom parser, `format` pattern, era display).
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseIsoPeriod(raw: string, picker: DatePickerPicker): Date | undefined;
|
|
@@ -46,9 +46,43 @@ function pickerPeriodStart(date, picker, locale) {
|
|
|
46
46
|
return date;
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
+
function toIsoPeriod(date, picker) {
|
|
50
|
+
if (!date || Number.isNaN(date.getTime())) return "";
|
|
51
|
+
switch (picker) {
|
|
52
|
+
case "year":
|
|
53
|
+
return String(date.getFullYear()).padStart(4, "0");
|
|
54
|
+
case "quarter":
|
|
55
|
+
case "month":
|
|
56
|
+
return toIsoDate(date).slice(0, 7);
|
|
57
|
+
case "week":
|
|
58
|
+
return formatDate(date, "RRRR-'W'II");
|
|
59
|
+
default:
|
|
60
|
+
return toIsoDate(date);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function parseIsoPeriod(raw, picker) {
|
|
64
|
+
const text = raw.trim();
|
|
65
|
+
if (picker === "year") {
|
|
66
|
+
const m = /^(\d{4})$/.exec(text);
|
|
67
|
+
return m ? new Date(Number(m[1]), 0, 1) : void 0;
|
|
68
|
+
}
|
|
69
|
+
if (picker === "month" || picker === "quarter") {
|
|
70
|
+
const m = /^(\d{4})-(\d{1,2})$/.exec(text);
|
|
71
|
+
if (!m) return void 0;
|
|
72
|
+
const month = Number(m[2]);
|
|
73
|
+
return month >= 1 && month <= 12 ? new Date(Number(m[1]), month - 1, 1) : void 0;
|
|
74
|
+
}
|
|
75
|
+
if (picker === "week") {
|
|
76
|
+
const parsed = parse(text, "RRRR-'W'II", new Date(2e3, 0, 1));
|
|
77
|
+
return isValid(parsed) ? parsed : void 0;
|
|
78
|
+
}
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
49
81
|
export {
|
|
50
82
|
formatPickerDate,
|
|
83
|
+
parseIsoPeriod,
|
|
51
84
|
parsePickerDate,
|
|
52
85
|
pickerDateAllowed,
|
|
53
|
-
pickerPeriodStart
|
|
86
|
+
pickerPeriodStart,
|
|
87
|
+
toIsoPeriod
|
|
54
88
|
};
|
package/dist/lib/field-a11y.d.ts
CHANGED
|
@@ -51,6 +51,18 @@ export interface FieldNameContextValue {
|
|
|
51
51
|
/** The label's plain-text content, when it is a string (belt-and-suspenders `aria-label`). */
|
|
52
52
|
label?: string;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Write attributes onto the real `<input>` react-aria hides inside a Checkbox / Radio / Switch.
|
|
56
|
+
*
|
|
57
|
+
* react-aria renders the painted control as a `<label>` and the FOCUS TARGET as a visually hidden
|
|
58
|
+
* `<input>` inside it — then strips every `data-*` off that input (`removeDataAttributes`) and
|
|
59
|
+
* forwards only the aria attributes its own props model knows about. So the two contracts this
|
|
60
|
+
* library puts on the semantic focus target — `data-field` (gh#337, the machine key automation
|
|
61
|
+
* addresses) and the aria state a control has no react-aria prop for — have to be written by hand.
|
|
62
|
+
*
|
|
63
|
+
* Deliberately NOT left on the root as well: one field must resolve to exactly one node.
|
|
64
|
+
*/
|
|
65
|
+
export declare function useMirroredInputAttributes(inputRef: React.RefObject<HTMLInputElement | null>, attributes: Record<string, string | undefined>): void;
|
|
54
66
|
export declare const FieldNameContext: React.Context<FieldNameContextValue | null>;
|
|
55
67
|
/**
|
|
56
68
|
* Resolve the last-resort accessible name for a control's semantic focus target (see
|
package/dist/lib/field-a11y.js
CHANGED
|
@@ -37,6 +37,18 @@ function resolveFieldA11y(props, intrinsicAriaLabel) {
|
|
|
37
37
|
}
|
|
38
38
|
return picked;
|
|
39
39
|
}
|
|
40
|
+
function useMirroredInputAttributes(inputRef, attributes) {
|
|
41
|
+
const key = JSON.stringify(attributes);
|
|
42
|
+
React.useLayoutEffect(() => {
|
|
43
|
+
const input = inputRef.current;
|
|
44
|
+
if (!input) return;
|
|
45
|
+
const entries = JSON.parse(key);
|
|
46
|
+
for (const [name, value] of Object.entries(entries)) {
|
|
47
|
+
if (value === void 0) input.removeAttribute(name);
|
|
48
|
+
else input.setAttribute(name, value);
|
|
49
|
+
}
|
|
50
|
+
}, [inputRef, key]);
|
|
51
|
+
}
|
|
40
52
|
const FieldNameContext = React.createContext(null);
|
|
41
53
|
function useFieldNameFallback(name) {
|
|
42
54
|
const field = React.useContext(FieldNameContext);
|
|
@@ -75,5 +87,6 @@ export {
|
|
|
75
87
|
pickGroupFieldA11y,
|
|
76
88
|
resolveFieldA11y,
|
|
77
89
|
useFieldIdentity,
|
|
78
|
-
useFieldNameFallback
|
|
90
|
+
useFieldNameFallback,
|
|
91
|
+
useMirroredInputAttributes
|
|
79
92
|
};
|
|
@@ -187,6 +187,13 @@ export type BadgeProp = {
|
|
|
187
187
|
icon?: React.ComponentType<{
|
|
188
188
|
className?: string;
|
|
189
189
|
}> | null;
|
|
190
|
+
/**
|
|
191
|
+
* Lining, fixed-width figures — for a chip whose content is a COUNT sitting in a column with
|
|
192
|
+
* other counts. The same axis `Text`, `TableCell` and `StatCard` already carry; its absence here
|
|
193
|
+
* was an asymmetry, not a decision. Off by default, because tabular figures are wider and a chip
|
|
194
|
+
* carrying words should not pay for them.
|
|
195
|
+
*/
|
|
196
|
+
tabular?: boolean;
|
|
190
197
|
className?: ClassNameProp;
|
|
191
198
|
children?: ChildrenProp;
|
|
192
199
|
};
|