@godxjp/ui 27.12.1 → 28.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/data-display/card.js +5 -0
- package/dist/components/navigation/tabs.d.ts +1 -1
- package/dist/components/navigation/tabs.js +54 -6
- package/dist/components/ui/toggle-group.d.ts +22 -0
- package/dist/components/ui/toggle-group.js +59 -3
- package/dist/components/ui/toggle.d.ts +11 -2
- package/dist/components/ui/toggle.js +3 -2
- package/dist/contracts/measurement.json +1 -1
- package/dist/props/components/data-display.prop.d.ts +15 -0
- package/dist/props/components/navigation.prop.d.ts +50 -0
- package/dist/props/registry.d.ts +18 -2
- package/dist/props/registry.js +28 -2
- package/dist/styles/card-layout.css +3 -4
- package/dist/styles/navigation-layout.css +109 -0
- package/dist/tokens/components/navigation.css +18 -0
- package/docs/DESIGN-AUTHORITY.md +33 -0
- package/docs/data-entry/toggle-group.tsx +86 -6
- package/docs/navigation/card-tabs.tsx +344 -0
- package/package.json +2 -2
- package/scripts/ui-audit.mjs +120 -4
|
@@ -97,6 +97,11 @@ const Card = React.forwardRef(
|
|
|
97
97
|
value: item.key,
|
|
98
98
|
label: item.tab,
|
|
99
99
|
disabled: item.disabled,
|
|
100
|
+
// antd's per-tab remove control. Forwarded rather than dropped: `items` is built
|
|
101
|
+
// AFTER `...tabProps` above, so a consumer cannot supply these from outside —
|
|
102
|
+
// this mapping is the only place they can arrive (gh#757).
|
|
103
|
+
closable: item.closable,
|
|
104
|
+
closeIcon: item.closeIcon,
|
|
100
105
|
// The body belongs to the SELECTED tab and to no other: the card has exactly one
|
|
101
106
|
// set of children, so handing them to every panel would render them N times the
|
|
102
107
|
// moment `destroyOnHidden={false}` kept the hidden panels mounted.
|
|
@@ -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, closeIcon, onTabClick, overflow, animated, indicator, moreIcon, onTabScroll, 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, bodied, extra, destroyOnHidden, onEdit, addIcon, hideAdd, closeIcon, onTabClick, overflow, animated, indicator, moreIcon, onTabScroll, 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
|
/**
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
Tabs as AriaTabs
|
|
9
9
|
} from "react-aria-components";
|
|
10
10
|
import { MoreHorizontal, Plus, X } from "lucide-react";
|
|
11
|
+
import { useCounterPill } from "../ui/toggle.js";
|
|
11
12
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
12
13
|
import { useMaxWidthBreakpoint } from "../../lib/breakpoint-token.js";
|
|
13
14
|
import { cn } from "../../lib/utils.js";
|
|
@@ -31,6 +32,15 @@ function triggerSizeClassName(size) {
|
|
|
31
32
|
size === "lg" && "min-h-[var(--tabs-trigger-height-lg)] px-[var(--tabs-trigger-padding-x-lg)] text-[length:var(--tabs-trigger-font-size-lg)]"
|
|
32
33
|
);
|
|
33
34
|
}
|
|
35
|
+
function TabCount({
|
|
36
|
+
count,
|
|
37
|
+
overflowCount,
|
|
38
|
+
showZero,
|
|
39
|
+
countLabel
|
|
40
|
+
}) {
|
|
41
|
+
const { pill } = useCounterPill({ count, overflowCount, showZero, countLabel, slot: "tabs" });
|
|
42
|
+
return pill;
|
|
43
|
+
}
|
|
34
44
|
const TabsFrameContext = React.createContext({
|
|
35
45
|
orientation: "horizontal",
|
|
36
46
|
selectionSuppressed: false,
|
|
@@ -99,6 +109,7 @@ function Tabs({
|
|
|
99
109
|
tabPlacement,
|
|
100
110
|
size = "md",
|
|
101
111
|
centered,
|
|
112
|
+
bodied,
|
|
102
113
|
extra,
|
|
103
114
|
destroyOnHidden = true,
|
|
104
115
|
onEdit,
|
|
@@ -178,12 +189,13 @@ function Tabs({
|
|
|
178
189
|
const showAdd = editable && !hideAdd && Boolean(onEdit);
|
|
179
190
|
const needsBar = Boolean(extraStart || extraEnd || showAdd || collapsible);
|
|
180
191
|
const card = variant === "card" || variant === "editable-card";
|
|
192
|
+
const bodiedCard = card && Boolean(bodied);
|
|
181
193
|
const sizeTriggerClassName = triggerSizeClassName(size);
|
|
182
194
|
const CARD_FACE = {
|
|
183
|
-
top: "rounded-[var(--tabs-card-radius)_var(--tabs-card-radius)_0_0] data-[state=active]:border-b-background",
|
|
184
|
-
bottom: "rounded-[0_0_var(--tabs-card-radius)_var(--tabs-card-radius)] data-[state=active]:border-t-background",
|
|
185
|
-
start: "rounded-s-[var(--tabs-card-radius)] rounded-e-none data-[state=active]:border-e-background",
|
|
186
|
-
end: "rounded-e-[var(--tabs-card-radius)] rounded-s-none data-[state=active]:border-s-background"
|
|
195
|
+
top: "rounded-[var(--tabs-card-radius)_var(--tabs-card-radius)_0_0] data-[state=active]:border-b-[hsl(var(--tabs-panel-background,var(--background)))]",
|
|
196
|
+
bottom: "rounded-[0_0_var(--tabs-card-radius)_var(--tabs-card-radius)] data-[state=active]:border-t-[hsl(var(--tabs-panel-background,var(--background)))]",
|
|
197
|
+
start: "rounded-s-[var(--tabs-card-radius)] rounded-e-none data-[state=active]:border-e-[hsl(var(--tabs-panel-background,var(--background)))]",
|
|
198
|
+
end: "rounded-e-[var(--tabs-card-radius)] rounded-s-none data-[state=active]:border-s-[hsl(var(--tabs-panel-background,var(--background)))]"
|
|
187
199
|
};
|
|
188
200
|
const list = items ? /* @__PURE__ */ jsx(
|
|
189
201
|
TabsList,
|
|
@@ -258,6 +270,16 @@ function Tabs({
|
|
|
258
270
|
// pixel and `justify-content` has nothing left to centre — measured, the leading and
|
|
259
271
|
// trailing gaps were both 0px with `centered` on and off.
|
|
260
272
|
centered && "flex-none",
|
|
273
|
+
// LINE face: same argument as the card face below, and from the same source — antd
|
|
274
|
+
// puts no `flex-grow` on a tab of ANY `type`, so a line tab is content-width too.
|
|
275
|
+
// `card` was fixed on that reasoning (gh#736); `line` was left on the base `flex-1`
|
|
276
|
+
// and kept stretching. Measured by a consumer on a 1200px strip: three tabs at
|
|
277
|
+
// 321px each for labels needing 83 / 84 / 56px, so the active underline ran 321px
|
|
278
|
+
// under a 56px label.
|
|
279
|
+
//
|
|
280
|
+
// `centered` is NOT the escape hatch for this: it centres, which trades one wrong
|
|
281
|
+
// geometry for another, and it is antd's prop for centring rather than for width.
|
|
282
|
+
variant === "line" && "flex-none",
|
|
261
283
|
// The merged edge is the SURFACE colour, never `transparent`: the rail is an inset
|
|
262
284
|
// shadow at the strip's padding-box edge, so a see-through border would let that 1px
|
|
263
285
|
// of rail run straight across the tab it is joined to.
|
|
@@ -271,7 +293,14 @@ function Tabs({
|
|
|
271
293
|
// the × ended up 90px from the tab's own trailing edge (179px on a two-tab
|
|
272
294
|
// strip). antd's is 8px away. This is also why the face read as a free-standing
|
|
273
295
|
// bordered box rather than a tab.
|
|
274
|
-
|
|
296
|
+
//
|
|
297
|
+
// THE ACTIVE FACE IS THE BODY'S SURFACE, from the same knob the body reads
|
|
298
|
+
// (gh#762). The base trigger's `data-[state=active]:bg-background` was right only
|
|
299
|
+
// while the two could not disagree; once a service can retint the body, an active
|
|
300
|
+
// tab still painting `--background` reads as a DIFFERENT box sitting on the panel
|
|
301
|
+
// — which is the "two stacked boxes" complaint in one property. Same utilities
|
|
302
|
+
// group, so tailwind-merge drops the base step instead of stacking two fills.
|
|
303
|
+
card && "flex-none border-[color:hsl(var(--border))] bg-[hsl(var(--tabs-card-background,var(--muted)))] data-[state=active]:bg-[hsl(var(--tabs-panel-background,var(--background)))]",
|
|
275
304
|
card && CARD_FACE[placement]
|
|
276
305
|
),
|
|
277
306
|
children: [
|
|
@@ -285,6 +314,15 @@ function Tabs({
|
|
|
285
314
|
}
|
|
286
315
|
) : null,
|
|
287
316
|
item.label,
|
|
317
|
+
/* @__PURE__ */ jsx(
|
|
318
|
+
TabCount,
|
|
319
|
+
{
|
|
320
|
+
count: item.count,
|
|
321
|
+
overflowCount: item.overflowCount,
|
|
322
|
+
showZero: item.showZero,
|
|
323
|
+
countLabel: item.countLabel
|
|
324
|
+
}
|
|
325
|
+
),
|
|
288
326
|
removable ? (
|
|
289
327
|
// A POINTER SHORTCUT, not a control — and that is measured, not stylistic. antd's own
|
|
290
328
|
// TabNode puts a real <button> beside the role="tab" element; rendered through
|
|
@@ -334,6 +372,7 @@ function Tabs({
|
|
|
334
372
|
"data-placement": placement,
|
|
335
373
|
"data-size": size,
|
|
336
374
|
"data-centered": centered ? "true" : void 0,
|
|
375
|
+
"data-bodied": bodiedCard ? "true" : void 0,
|
|
337
376
|
"data-animated-ink-bar": motion.inkBar ? "true" : "false",
|
|
338
377
|
"data-animated-tab-pane": motion.tabPane ? "true" : "false",
|
|
339
378
|
"data-indicator-size": indicatorSize,
|
|
@@ -363,6 +402,7 @@ function Tabs({
|
|
|
363
402
|
"data-placement": placement,
|
|
364
403
|
"data-size": size,
|
|
365
404
|
"data-centered": centered ? "true" : void 0,
|
|
405
|
+
"data-bodied": bodiedCard ? "true" : void 0,
|
|
366
406
|
"data-animated-ink-bar": motion.inkBar ? "true" : "false",
|
|
367
407
|
"data-animated-tab-pane": motion.tabPane ? "true" : "false",
|
|
368
408
|
"data-indicator-size": indicatorSize,
|
|
@@ -493,7 +533,15 @@ const TabsTrigger = React.forwardRef(
|
|
|
493
533
|
// The line indicator lives in src/styles/navigation-layout.css so it reads --tabs-indicator-*.
|
|
494
534
|
// Selected and focused stay visually distinct (WCAG 2.4.7): selected is a 1px hairline in the
|
|
495
535
|
// border, focused is the 2px ring plus its halo outside it.
|
|
496
|
-
|
|
536
|
+
// `group-data-[variant=line]/tabs-list:flex-none` is the COMPOUND half of gh#757's
|
|
537
|
+
// width fix (gh#762). That fix landed on the `items` renderer only, which is the path
|
|
538
|
+
// the reporter measured; a hand-composed `<TabsList variant="line" className="w-full">`
|
|
539
|
+
// — what a saved-view strip that has to reach its own triggers writes — still ran on
|
|
540
|
+
// the base `flex-1`. Measured on a 1200px strip: three triggers at 394.66px each for
|
|
541
|
+
// labels needing 107 / 95 / 53px, i.e. the SAME defect one API away. antd puts no
|
|
542
|
+
// `flex-grow` on a tab of any `type`, so the rule belongs to the line STRIP rather than
|
|
543
|
+
// to one construction path; the pill strip keeps `flex-1`, which is what fills its box.
|
|
544
|
+
"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: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:flex-none 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: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",
|
|
497
545
|
// The tier the root was given. Without this the compound form ignored `size` outright.
|
|
498
546
|
triggerSizeClassName(size),
|
|
499
547
|
className
|
|
@@ -11,6 +11,28 @@ type ToggleGroupBaseProp = Omit<React.ComponentPropsWithoutRef<"div">, "defaultV
|
|
|
11
11
|
disabled?: boolean;
|
|
12
12
|
/** Which way the arrow keys walk the group. */
|
|
13
13
|
orientation?: "horizontal" | "vertical";
|
|
14
|
+
/**
|
|
15
|
+
* May the group end up with NOTHING selected? Omitted (`false`) it may, which is what a tag
|
|
16
|
+
* filter wants — pressing the selected chip again clears it and reports `""`. Set it and the
|
|
17
|
+
* selected item stays selected when it is pressed again.
|
|
18
|
+
*
|
|
19
|
+
* **It also decides the ARIA role of a `type="single"` group (gh#744)**, because emptiness is
|
|
20
|
+
* exactly what the two roles disagree about. ARIA has no "press again to deselect" for a radio:
|
|
21
|
+
* a radiogroup that has a selection always has exactly one checked item, so a radiogroup that
|
|
22
|
+
* the user just emptied is a state a screen reader cannot read out. Therefore:
|
|
23
|
+
*
|
|
24
|
+
* - omitted → `role="group"` + `aria-pressed` per item (a row of toggle buttons, which MAY be
|
|
25
|
+
* all-off) — this is the default, so no existing single group changes behaviour;
|
|
26
|
+
* - set → `role="radiogroup"` + `role="radio"` / `aria-checked` per item, and the arrow keys
|
|
27
|
+
* move the SELECTION as APG's radio-group pattern requires, not just the focus.
|
|
28
|
+
*
|
|
29
|
+
* The name is React Aria's own (`useToggleGroupState`, which this component is built on) —
|
|
30
|
+
* neither antd nor Radix names the capability. See `docs/DESIGN-AUTHORITY.md`.
|
|
31
|
+
*
|
|
32
|
+
* On `type="multiple"` it only keeps the last remaining item selected; the roles do not move
|
|
33
|
+
* (a row of `aria-pressed` buttons is already the right reading there).
|
|
34
|
+
*/
|
|
35
|
+
disallowEmptySelection?: boolean;
|
|
14
36
|
/**
|
|
15
37
|
* Let the row break onto further lines instead of running past its rail (gh#741). Same name,
|
|
16
38
|
* same boolean shape and same `data-wrap` attribute as `Flex` — one spelling answers "what does
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
useCounterPill
|
|
14
14
|
} from "./toggle.js";
|
|
15
15
|
const ToggleGroupContext = React.createContext({});
|
|
16
|
+
const ARROW_KEYS = /* @__PURE__ */ new Set(["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"]);
|
|
16
17
|
function toSelectedKeys(value) {
|
|
17
18
|
if (value == null) {
|
|
18
19
|
return void 0;
|
|
@@ -39,10 +40,21 @@ const ToggleGroup = React.forwardRef(
|
|
|
39
40
|
defaultValue,
|
|
40
41
|
onValueChange,
|
|
41
42
|
disabled,
|
|
43
|
+
disallowEmptySelection,
|
|
42
44
|
loop: _loop,
|
|
43
45
|
...props
|
|
44
46
|
}, ref) => {
|
|
45
|
-
const
|
|
47
|
+
const radioSemantics = type === "single" && disallowEmptySelection === true;
|
|
48
|
+
const arrowNav = React.useRef(false);
|
|
49
|
+
const groupElement = React.useRef(null);
|
|
50
|
+
const [inToolbar, setInToolbar] = React.useState(false);
|
|
51
|
+
React.useLayoutEffect(() => {
|
|
52
|
+
setInToolbar(!!groupElement.current?.parentElement?.closest('[role="toolbar"]'));
|
|
53
|
+
}, []);
|
|
54
|
+
const context = React.useMemo(
|
|
55
|
+
() => ({ variant, size, shape, radioSemantics, arrowNav, inToolbar }),
|
|
56
|
+
[variant, size, shape, radioSemantics, inToolbar]
|
|
57
|
+
);
|
|
46
58
|
const selectedKeys = React.useMemo(() => toSelectedKeys(value), [value]);
|
|
47
59
|
const defaultSelectedKeys = React.useMemo(() => toSelectedKeys(defaultValue), [defaultValue]);
|
|
48
60
|
const handleSelectionChange = (keys) => {
|
|
@@ -69,7 +81,33 @@ const ToggleGroup = React.forwardRef(
|
|
|
69
81
|
defaultSelectedKeys,
|
|
70
82
|
onSelectionChange: handleSelectionChange,
|
|
71
83
|
isDisabled: disabled,
|
|
72
|
-
|
|
84
|
+
disallowEmptySelection,
|
|
85
|
+
render: (domProps) => /* @__PURE__ */ jsx(
|
|
86
|
+
"div",
|
|
87
|
+
{
|
|
88
|
+
...props,
|
|
89
|
+
...domProps,
|
|
90
|
+
ref: (node) => {
|
|
91
|
+
groupElement.current = node;
|
|
92
|
+
const aria = domProps.ref;
|
|
93
|
+
if (typeof aria === "function") aria(node);
|
|
94
|
+
else if (aria) aria.current = node;
|
|
95
|
+
},
|
|
96
|
+
...type === "single" && !radioSemantics ? { role: "group", "aria-orientation": void 0 } : null,
|
|
97
|
+
onKeyDownCapture: (event) => {
|
|
98
|
+
if (radioSemantics) {
|
|
99
|
+
arrowNav.current = ARROW_KEYS.has(event.key);
|
|
100
|
+
if (arrowNav.current) {
|
|
101
|
+
queueMicrotask(() => {
|
|
102
|
+
arrowNav.current = false;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
domProps.onKeyDownCapture?.(event);
|
|
107
|
+
},
|
|
108
|
+
tabIndex: props.tabIndex ?? domProps.tabIndex
|
|
109
|
+
}
|
|
110
|
+
),
|
|
73
111
|
children: /* @__PURE__ */ jsx(ToggleGroupContext.Provider, { value: context, children })
|
|
74
112
|
}
|
|
75
113
|
);
|
|
@@ -105,6 +143,9 @@ const ToggleGroupItem = React.forwardRef(
|
|
|
105
143
|
});
|
|
106
144
|
const groupState = React.useContext(ToggleGroupStateContext);
|
|
107
145
|
const isPressed = groupState?.selectedKeys.has(value) ?? false;
|
|
146
|
+
const isSingle = groupState?.selectionMode === "single";
|
|
147
|
+
const radioSemantics = context.radioSemantics === true;
|
|
148
|
+
const rovingTabIndex = radioSemantics && !context.inToolbar && groupState && groupState.selectedKeys.size > 0 ? isPressed ? 0 : -1 : void 0;
|
|
108
149
|
return /* @__PURE__ */ jsxs(
|
|
109
150
|
ToggleButton,
|
|
110
151
|
{
|
|
@@ -122,7 +163,22 @@ const ToggleGroupItem = React.forwardRef(
|
|
|
122
163
|
...props,
|
|
123
164
|
id: value,
|
|
124
165
|
isDisabled: disabled,
|
|
125
|
-
render: (domProps) => restoreDomProps(props,
|
|
166
|
+
render: (domProps) => restoreDomProps(props, {
|
|
167
|
+
...domProps,
|
|
168
|
+
...isSingle && !radioSemantics ? { role: void 0, "aria-checked": void 0, "aria-pressed": isPressed } : null,
|
|
169
|
+
...rovingTabIndex == null ? null : { tabIndex: rovingTabIndex },
|
|
170
|
+
// Selection follows focus, as APG requires of a radio group — but only when an arrow
|
|
171
|
+
// key is what moved it. Measured otherwise: RAC's toolbar Tab handler focuses the
|
|
172
|
+
// LAST item on the way out, which silently moved the selection to it.
|
|
173
|
+
...radioSemantics ? {
|
|
174
|
+
onFocus: (event) => {
|
|
175
|
+
if (context.arrowNav?.current && groupState && groupState.selectedKeys.size > 0 && !isPressed) {
|
|
176
|
+
groupState.setSelected(value, true);
|
|
177
|
+
}
|
|
178
|
+
domProps.onFocus?.(event);
|
|
179
|
+
}
|
|
180
|
+
} : null
|
|
181
|
+
}),
|
|
126
182
|
children: [
|
|
127
183
|
children,
|
|
128
184
|
pill
|
|
@@ -42,9 +42,18 @@ type TogglePressedFields = {
|
|
|
42
42
|
/** Fired with the NEW pressed state — RAC `onChange`. */
|
|
43
43
|
onPressedChange?: (pressed: boolean) => void;
|
|
44
44
|
};
|
|
45
|
-
/**
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Counter pill shared by `Toggle`, `ToggleGroupItem` and `Tabs` items. ACCESSIBLE NAME.
|
|
47
|
+
*
|
|
48
|
+
* `slot` names the hooks the pill emits (`data-slot="<slot>-count"` / `.ui-<slot>-count`) so a
|
|
49
|
+
* third component can reuse the whole construction — the Intl formatting, the `overflowCount`
|
|
50
|
+
* cap, and above all the `aria-hidden` + `sr-only` split that keeps «未対応12» out of the
|
|
51
|
+
* accessible name (gh#734) — without a second counting API growing beside this one (gh#762).
|
|
52
|
+
* It defaults to `toggle`, so every existing call emits the identical markup it always has.
|
|
53
|
+
*/
|
|
54
|
+
declare function useCounterPill({ count, overflowCount, showZero, countLabel, ariaLabel, slot, }: ToggleCountFields & {
|
|
47
55
|
ariaLabel?: string;
|
|
56
|
+
slot?: string;
|
|
48
57
|
}): {
|
|
49
58
|
pill: null;
|
|
50
59
|
resolvedAriaLabel: string | undefined;
|
|
@@ -50,7 +50,8 @@ function useCounterPill({
|
|
|
50
50
|
overflowCount = 99,
|
|
51
51
|
showZero = true,
|
|
52
52
|
countLabel,
|
|
53
|
-
ariaLabel
|
|
53
|
+
ariaLabel,
|
|
54
|
+
slot = "toggle"
|
|
54
55
|
}) {
|
|
55
56
|
const { locale } = useTranslation();
|
|
56
57
|
const visible = count != null && (count !== 0 || showZero);
|
|
@@ -65,7 +66,7 @@ function useCounterPill({
|
|
|
65
66
|
const spoken = countLabel ? `${formatted} ${countLabel}` : formatted;
|
|
66
67
|
return {
|
|
67
68
|
pill: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
68
|
-
/* @__PURE__ */ jsx("span", { "data-slot":
|
|
69
|
+
/* @__PURE__ */ jsx("span", { "data-slot": `${slot}-count`, className: `ui-${slot}-count`, "aria-hidden": "true", children: formatted }),
|
|
69
70
|
/* @__PURE__ */ jsx("span", { className: "sr-only", children: `, ${spoken}` })
|
|
70
71
|
] }),
|
|
71
72
|
resolvedAriaLabel: ariaLabel == null ? ariaLabel : `${ariaLabel}, ${spoken}`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "28.1.0",
|
|
4
4
|
"targetSize": {
|
|
5
5
|
"standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
|
|
6
6
|
"min": 24,
|
|
@@ -972,4 +972,19 @@ export type CardTabItemProp = {
|
|
|
972
972
|
tab: React.ReactNode;
|
|
973
973
|
/** Ant Design `disabled` — the tab stays in the strip and in the roving focus, unselectable. */
|
|
974
974
|
disabled?: boolean;
|
|
975
|
+
/**
|
|
976
|
+
* Ant Design `closable` — honoured by `tabProps={{ variant: "editable-card" }}`, where it
|
|
977
|
+
* decides whether THIS tab gets a remove button (gh#757).
|
|
978
|
+
*
|
|
979
|
+
* Without it the strip is all-or-nothing: `editable-card` puts a × on every tab, including the
|
|
980
|
+
* one that is not deletable — a consumer measured 3 of 3 removable where 2 was right. It could
|
|
981
|
+
* not be patched from outside either, because `Card` builds `items` AFTER spreading
|
|
982
|
+
* `...tabProps`, so a hand-written `items` never survived.
|
|
983
|
+
*
|
|
984
|
+
* Default (undefined) stays removable, which is antd's own default and `Tabs`' (`closable !==
|
|
985
|
+
* false`) — this only adds the way to say no.
|
|
986
|
+
*/
|
|
987
|
+
closable?: boolean;
|
|
988
|
+
/** Ant Design `closeIcon` — replaces the default × on this tab's remove button. */
|
|
989
|
+
closeIcon?: React.ReactNode;
|
|
975
990
|
};
|
|
@@ -264,6 +264,32 @@ export type TabItemProp = {
|
|
|
264
264
|
closable?: boolean;
|
|
265
265
|
/** Ant Design `Tab.closeIcon` — replaces the default × on this item's remove button. */
|
|
266
266
|
closeIcon?: React.ReactNode;
|
|
267
|
+
/**
|
|
268
|
+
* The count beside the label — 「未対応 **12**」, the list-screen pattern (gh#762).
|
|
269
|
+
*
|
|
270
|
+
* It is the SAME counter vocabulary `Button` (gh#312) and `Toggle`/`ToggleGroupItem` (gh#734)
|
|
271
|
+
* already publish, drawn by the same helper, so a counted tab and the counted chip under it
|
|
272
|
+
* are one thing at one size and one accessible shape. Formatted with `Intl.NumberFormat` in
|
|
273
|
+
* the active locale — never `String(n)`, never a hand-rolled thousands separator.
|
|
274
|
+
*
|
|
275
|
+
* WHY IT IS A SLOT AND NOT `label: <span>未対応 <Badge count={12} /></span>` (antd's answer):
|
|
276
|
+
* with the number inside the label the PACKAGE owns none of it — not the size, not the gap,
|
|
277
|
+
* not the tone as the tab moves between selected / unselected / disabled — so every consumer
|
|
278
|
+
* lands somewhere different. The slot is what lets the tab state drive the pill.
|
|
279
|
+
*/
|
|
280
|
+
count?: number;
|
|
281
|
+
/** Cap for `count` — beyond it the pill shows `{overflowCount}+` (e.g. `99+`). Default `99`. */
|
|
282
|
+
overflowCount?: number;
|
|
283
|
+
/** Render the pill when `count` is 0. Default `true`, matching Button and Toggle. */
|
|
284
|
+
showZero?: boolean;
|
|
285
|
+
/**
|
|
286
|
+
* Localized description of what the count MEANS, folded into the tab's accessible name so it
|
|
287
|
+
* never announces as label-then-bare-digits. The digits concatenate straight onto the label in
|
|
288
|
+
* the accessibility tree («未対応12», gh#734's defect), so the pill is `aria-hidden` and the
|
|
289
|
+
* spoken clause is an `sr-only` sibling: `countLabel="未対応の課題"` reads
|
|
290
|
+
* 「未対応, 12 未対応の課題」. Already localized when it arrives — pass a `t()` string.
|
|
291
|
+
*/
|
|
292
|
+
countLabel?: string;
|
|
267
293
|
/**
|
|
268
294
|
* Ant Design `Tab.forceRender`. Mounts THIS panel up front and keeps it mounted while another
|
|
269
295
|
* tab is selected, without turning that on for the whole strip the way `destroyOnHidden={false}`
|
|
@@ -404,6 +430,30 @@ export type TabsProp = {
|
|
|
404
430
|
size?: "sm" | "md" | "lg";
|
|
405
431
|
/** Ant Design `centered` — centre the trigger strip on its own inline axis. */
|
|
406
432
|
centered?: boolean;
|
|
433
|
+
/**
|
|
434
|
+
* Draw the PANEL BODY the card strip opens into, so the strip and the panel are ONE object
|
|
435
|
+
* (gh#762): a surface, a border continuous with the rail, and the radius on the two corners
|
|
436
|
+
* away from the strip. The active tab's block-end edge is repainted in that body's own colour,
|
|
437
|
+
* which is what makes it read as opened into the body rather than closed off above it.
|
|
438
|
+
*
|
|
439
|
+
* WHY IT EXISTS. `variant="card"` already repaints the active face's merged edge — antd's
|
|
440
|
+
* `genCardStyle` — but the package shipped no body for it to merge INTO, so the consumer had to
|
|
441
|
+
* supply one and every choice was wrong. Measured on 28.0.0 against a `variant="card"` strip at
|
|
442
|
+
* 1440px: `<Card>` left an 8px gap AND added a second 1px border at a 9.708px radius (two
|
|
443
|
+
* stacked boxes), `<Card variant="borderless">` left the same 8px gap with no line at all, and
|
|
444
|
+
* no box at all left the 8px gap over a transparent panel. The gap is `--tabs-root-gap`, which
|
|
445
|
+
* is correct for every OTHER variant — a body is not a gap tweak, it is the missing half of the
|
|
446
|
+
* card face.
|
|
447
|
+
*
|
|
448
|
+
* HONOURED ONLY BY `card` / `editable-card`, and that is a statement, not a limitation. The
|
|
449
|
+
* pill strip is a free-floating control and the `line` strip's own body is the CARD around it —
|
|
450
|
+
* `<Card tabList>` is that pairing, already built and already joined. Passing `bodied` to any
|
|
451
|
+
* other variant emits no attribute and changes no pixel.
|
|
452
|
+
*
|
|
453
|
+
* OPT-IN, so no strip on 28.0.0 moves. A service that wants every card strip bodied has the
|
|
454
|
+
* `card`-variant defaults as a theme decision, not a prop it must repeat.
|
|
455
|
+
*/
|
|
456
|
+
bodied?: boolean;
|
|
407
457
|
/** Ant Design `tabBarExtraContent`, renamed and made logical. @see TabsExtraProp */
|
|
408
458
|
extra?: TabsExtraProp;
|
|
409
459
|
/**
|
package/dist/props/registry.d.ts
CHANGED
|
@@ -2350,7 +2350,11 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
2350
2350
|
readonly TabsProp: {
|
|
2351
2351
|
readonly group: "navigation";
|
|
2352
2352
|
readonly file: "components/navigation.prop.ts";
|
|
2353
|
-
readonly vocabulary: readonly ["ValueProp", "DefaultValueProp", "OnValueChangeProp", "ClassNameProp", "SizeProp", "ExtraProp"
|
|
2353
|
+
readonly vocabulary: readonly ["ValueProp", "DefaultValueProp", "OnValueChangeProp", "ClassNameProp", "SizeProp", "ExtraProp", {
|
|
2354
|
+
readonly field: "bodied";
|
|
2355
|
+
readonly local: true;
|
|
2356
|
+
readonly reason: "A positive boolean for the card strip's PANEL BODY (gh#762) — an axis on Tabs rather than a `CardTabs` export, because the thing missing was a surface the existing card face already repaints its edge into, not a second component. No vocabulary type: no other component draws a body for its own panel.";
|
|
2357
|
+
}];
|
|
2354
2358
|
};
|
|
2355
2359
|
readonly TabsVariantProp: {
|
|
2356
2360
|
readonly group: "navigation";
|
|
@@ -2414,7 +2418,15 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
2414
2418
|
readonly TabItemProp: {
|
|
2415
2419
|
readonly group: "navigation";
|
|
2416
2420
|
readonly file: "components/navigation.prop.ts";
|
|
2417
|
-
readonly vocabulary: readonly ["ValueProp", "LabelProp", "ChildrenProp", "DisabledProp", "IconProp"
|
|
2421
|
+
readonly vocabulary: readonly ["ValueProp", "LabelProp", "ChildrenProp", "DisabledProp", "IconProp", {
|
|
2422
|
+
readonly field: "count";
|
|
2423
|
+
readonly local: true;
|
|
2424
|
+
readonly reason: "Counter-pill vocabulary shared VERBATIM with ButtonProp and ToggleProp (count/overflowCount/showZero) and drawn by the same `useCounterPill`, so a counted tab, a counted chip and a counted button are one thing — gh#312 / gh#734 / gh#762.";
|
|
2425
|
+
}, {
|
|
2426
|
+
readonly field: "countLabel";
|
|
2427
|
+
readonly local: true;
|
|
2428
|
+
readonly reason: "Localized unit folded into the tab's accessible name («未対応, 12 件の課題»), so the digits never concatenate onto the label — gh#734's measured defect, same field name as Button/Toggle.";
|
|
2429
|
+
}];
|
|
2418
2430
|
};
|
|
2419
2431
|
readonly FormFieldArrayProp: {
|
|
2420
2432
|
readonly group: "form";
|
|
@@ -2471,6 +2483,10 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
2471
2483
|
readonly field: "wrap";
|
|
2472
2484
|
readonly local: true;
|
|
2473
2485
|
readonly reason: "The SAME boolean shorthand for flex-wrap that FlexProp carries, on the row that owns a chip set — one spelling for 'what does this row do when it does not fit' (gh#741). Opt-in, and the same default for every variant: paint must not decide geometry.";
|
|
2486
|
+
}, {
|
|
2487
|
+
readonly field: "disallowEmptySelection";
|
|
2488
|
+
readonly local: true;
|
|
2489
|
+
readonly reason: "React Aria's own name for the capability (useToggleGroupState), which this component is built on; neither antd nor Radix names it (docs/DESIGN-AUTHORITY.md). It is not just a state guard: on type=single it DECIDES the ARIA role, because emptiness is the one thing role=group + aria-pressed and role=radiogroup + aria-checked disagree about (gh#744).";
|
|
2474
2490
|
}];
|
|
2475
2491
|
};
|
|
2476
2492
|
readonly ToggleGroupItemProp: {
|
package/dist/props/registry.js
CHANGED
|
@@ -2735,7 +2735,12 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
2735
2735
|
"OnValueChangeProp",
|
|
2736
2736
|
"ClassNameProp",
|
|
2737
2737
|
"SizeProp",
|
|
2738
|
-
"ExtraProp"
|
|
2738
|
+
"ExtraProp",
|
|
2739
|
+
{
|
|
2740
|
+
field: "bodied",
|
|
2741
|
+
local: true,
|
|
2742
|
+
reason: "A positive boolean for the card strip's PANEL BODY (gh#762) \u2014 an axis on Tabs rather than a `CardTabs` export, because the thing missing was a surface the existing card face already repaints its edge into, not a second component. No vocabulary type: no other component draws a body for its own panel."
|
|
2743
|
+
}
|
|
2739
2744
|
]
|
|
2740
2745
|
},
|
|
2741
2746
|
TabsVariantProp: {
|
|
@@ -2803,7 +2808,23 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
2803
2808
|
TabItemProp: {
|
|
2804
2809
|
group: "navigation",
|
|
2805
2810
|
file: "components/navigation.prop.ts",
|
|
2806
|
-
vocabulary: [
|
|
2811
|
+
vocabulary: [
|
|
2812
|
+
"ValueProp",
|
|
2813
|
+
"LabelProp",
|
|
2814
|
+
"ChildrenProp",
|
|
2815
|
+
"DisabledProp",
|
|
2816
|
+
"IconProp",
|
|
2817
|
+
{
|
|
2818
|
+
field: "count",
|
|
2819
|
+
local: true,
|
|
2820
|
+
reason: "Counter-pill vocabulary shared VERBATIM with ButtonProp and ToggleProp (count/overflowCount/showZero) and drawn by the same `useCounterPill`, so a counted tab, a counted chip and a counted button are one thing \u2014 gh#312 / gh#734 / gh#762."
|
|
2821
|
+
},
|
|
2822
|
+
{
|
|
2823
|
+
field: "countLabel",
|
|
2824
|
+
local: true,
|
|
2825
|
+
reason: "Localized unit folded into the tab's accessible name (\xAB\u672A\u5BFE\u5FDC, 12 \u4EF6\u306E\u8AB2\u984C\xBB), so the digits never concatenate onto the label \u2014 gh#734's measured defect, same field name as Button/Toggle."
|
|
2826
|
+
}
|
|
2827
|
+
]
|
|
2807
2828
|
},
|
|
2808
2829
|
FormFieldArrayProp: {
|
|
2809
2830
|
group: "form",
|
|
@@ -2878,6 +2899,11 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
2878
2899
|
field: "wrap",
|
|
2879
2900
|
local: true,
|
|
2880
2901
|
reason: "The SAME boolean shorthand for flex-wrap that FlexProp carries, on the row that owns a chip set \u2014 one spelling for 'what does this row do when it does not fit' (gh#741). Opt-in, and the same default for every variant: paint must not decide geometry."
|
|
2902
|
+
},
|
|
2903
|
+
{
|
|
2904
|
+
field: "disallowEmptySelection",
|
|
2905
|
+
local: true,
|
|
2906
|
+
reason: "React Aria's own name for the capability (useToggleGroupState), which this component is built on; neither antd nor Radix names it (docs/DESIGN-AUTHORITY.md). It is not just a state guard: on type=single it DECIDES the ARIA role, because emptiness is the one thing role=group + aria-pressed and role=radiogroup + aria-checked disagree about (gh#744)."
|
|
2881
2907
|
}
|
|
2882
2908
|
]
|
|
2883
2909
|
},
|
|
@@ -85,15 +85,14 @@
|
|
|
85
85
|
|
|
86
86
|
[data-slot="card"][data-accent] > [data-slot="card-header"],
|
|
87
87
|
[data-slot="card"][data-accent] > [data-slot="card-content"]:not([data-flush]),
|
|
88
|
+
[data-slot="card"][data-accent] > [data-slot="card-footer"]:not([data-flush]) {
|
|
89
|
+
padding-inline-start: calc(var(--card-space-inset) - var(--card-accent-rail-width));
|
|
90
|
+
}
|
|
88
91
|
|
|
89
92
|
[data-slot="card"] > [data-slot="card-footer"] {
|
|
90
93
|
margin-block-start: auto;
|
|
91
94
|
}
|
|
92
95
|
|
|
93
|
-
[data-slot="card"][data-accent] > [data-slot="card-footer"]:not([data-flush]) {
|
|
94
|
-
padding-inline-start: calc(var(--card-space-inset) - var(--card-accent-rail-width));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
96
|
[data-slot="card"][data-accent][data-accent-placement="perimeter"] {
|
|
98
97
|
border-width: var(--card-accent-perimeter-width);
|
|
99
98
|
border-color: var(--card-accent-color);
|