@godxjp/ui 28.6.0 → 28.7.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/data-table.d.ts +10 -2
- package/dist/components/data-display/data-table.js +12 -5
- package/dist/components/data-display/scroll-area.js +16 -2
- package/dist/components/data-display/service-launcher-card.d.ts +25 -0
- package/dist/components/data-display/table.d.ts +38 -1
- package/dist/components/data-display/table.js +57 -39
- package/dist/components/data-entry/form-field.js +29 -12
- package/dist/components/navigation/app-setting-picker.js +20 -3
- package/dist/components/ui/table.js +1 -0
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +5 -1
- package/dist/i18n/messages/ja.json +5 -1
- package/dist/i18n/messages/vi.json +5 -1
- package/dist/lib/hooks.d.ts +23 -0
- package/dist/lib/hooks.js +27 -0
- package/dist/props/components/data-display.prop.d.ts +14 -0
- package/dist/props/registry.d.ts +1 -1
- package/dist/props/registry.js +2 -1
- package/dist/styles/form-layout.css +22 -0
- package/dist/styles/layout.css +0 -1
- package/dist/styles/navigation-layout.css +18 -19
- package/dist/tokens/components/form.css +2 -0
- package/dist/tokens/components/navigation.css +2 -0
- package/docs/COMPOSITION-VS-COMPONENT.md +3 -0
- package/docs/DESIGN-AUTHORITY.md +25 -5
- package/docs/data-display/scroll-area.tsx +31 -0
- package/docs/data-display/service-launcher-card.tsx +275 -115
- package/package.json +5 -4
- package/scripts/visual-audit.mjs +29 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { flexRender, type ColumnVisibilityState, type OnChangeFn, type PaginationState } from "@tanstack/react-table";
|
|
3
|
-
import type { BreakpointProp, ColumnDefProp, DensityProp, OnColumnFilterChangeProp, OnRowProp, RowToneProp, SortDirectionProp, SortStateProp, TableExpandableProp, TablePaginationProp, TablePresetProp, TableRowSelectionProp, TableScrollProp, TableStickyProp, TableSummaryProp } from "../../props/vocabulary/index.js";
|
|
3
|
+
import type { BreakpointProp, ColumnDefProp, DensityProp, LabelProp, OnColumnFilterChangeProp, OnRowProp, RowToneProp, SortDirectionProp, SortStateProp, TableExpandableProp, TablePaginationProp, TablePresetProp, TableRowSelectionProp, TableScrollProp, TableStickyProp, TableSummaryProp } from "../../props/vocabulary/index.js";
|
|
4
4
|
export type Density = DensityProp;
|
|
5
5
|
/**
|
|
6
6
|
* Lean column definition — the simple, common-case column API. `render` shapes a cell; `sortable`
|
|
@@ -93,6 +93,14 @@ interface DataTableProps<T> {
|
|
|
93
93
|
preset?: TablePresetProp;
|
|
94
94
|
/** Default `"sm"`. Ignored while `preset` is `"default"`. */
|
|
95
95
|
collapseBelow?: BreakpointProp;
|
|
96
|
+
/**
|
|
97
|
+
* Accessible name for the horizontal-scroll REGION (the `tabindex="0"` wrapper a keyboard user
|
|
98
|
+
* lands on), not for the `<table>`. Optional: left out, the region takes the localized
|
|
99
|
+
* `dataTable.scrollRegion` default, so no consumer has to invent a name for every list. The
|
|
100
|
+
* region is announced only while it HAS overflow to reach. Same contract as `Table label`.
|
|
101
|
+
* (gh#817)
|
|
102
|
+
*/
|
|
103
|
+
label?: LabelProp;
|
|
96
104
|
/**
|
|
97
105
|
* Per-row className for state-based row tinting (e.g. flag an invalid or empty record). Returned
|
|
98
106
|
* classes are appended last, so they win over the built-in hover/selected fills.
|
|
@@ -146,7 +154,7 @@ interface DataTableProps<T> {
|
|
|
146
154
|
className?: string;
|
|
147
155
|
children?: React.ReactNode;
|
|
148
156
|
}
|
|
149
|
-
export declare function DataTable<T>({ data, columns, getRowId, getRowLabel, selectable, selected: controlledSelected, onSelectChange, onRowClick, density: controlledDensity, onDensityChange, sort, onSortChange, globalFilter: controlledGlobalFilter, onGlobalFilterChange, pagination: paginationProp, onPaginationChange, rowCount, columnVisibility: controlledVisibility, onColumnVisibilityChange, manualSorting, manualFiltering, manualPagination, loading, empty, error, denied, onRetry, striped, hoverable, stickyHeader, preset, collapseBelow, rowClassName, rowTone, rowSelection, expandable, summary, scroll, sticky, onRow, bordered, showSorterTooltip, sortDirections, onFilterChange, className, children, }: DataTableProps<T>): React.JSX.Element;
|
|
157
|
+
export declare function DataTable<T>({ data, columns, getRowId, getRowLabel, selectable, selected: controlledSelected, onSelectChange, onRowClick, density: controlledDensity, onDensityChange, sort, onSortChange, globalFilter: controlledGlobalFilter, onGlobalFilterChange, pagination: paginationProp, onPaginationChange, rowCount, columnVisibility: controlledVisibility, onColumnVisibilityChange, manualSorting, manualFiltering, manualPagination, loading, empty, error, denied, onRetry, striped, hoverable, stickyHeader, preset, collapseBelow, label, rowClassName, rowTone, rowSelection, expandable, summary, scroll, sticky, onRow, bordered, showSorterTooltip, sortDirections, onFilterChange, className, children, }: DataTableProps<T>): React.JSX.Element;
|
|
150
158
|
export declare namespace DataTable {
|
|
151
159
|
export var Toolbar: ({ children, className, }: {
|
|
152
160
|
children?: React.ReactNode;
|
|
@@ -65,9 +65,11 @@ import {
|
|
|
65
65
|
TableCell,
|
|
66
66
|
TableHead,
|
|
67
67
|
TableHeader,
|
|
68
|
-
TableRow
|
|
68
|
+
TableRow,
|
|
69
|
+
scrollRegionLabel
|
|
69
70
|
} from "../data-display/table.js";
|
|
70
71
|
import { cn } from "../../lib/utils.js";
|
|
72
|
+
import { useScrollsHorizontally } from "../../lib/hooks.js";
|
|
71
73
|
import { densityClass } from "../../lib/variants.js";
|
|
72
74
|
import {
|
|
73
75
|
controlIconSmClass,
|
|
@@ -230,6 +232,7 @@ function DataTable({
|
|
|
230
232
|
stickyHeader = true,
|
|
231
233
|
preset = "default",
|
|
232
234
|
collapseBelow = "sm",
|
|
235
|
+
label,
|
|
233
236
|
rowClassName,
|
|
234
237
|
rowTone,
|
|
235
238
|
rowSelection,
|
|
@@ -450,6 +453,7 @@ function DataTable({
|
|
|
450
453
|
stickyHeader: sticky === void 0 ? stickyHeader : sticky !== false,
|
|
451
454
|
preset,
|
|
452
455
|
collapseBelow,
|
|
456
|
+
label,
|
|
453
457
|
rowClassName,
|
|
454
458
|
rowTone,
|
|
455
459
|
bordered,
|
|
@@ -774,6 +778,7 @@ DataTable.Content = function DataTableContent() {
|
|
|
774
778
|
stickyHeader,
|
|
775
779
|
preset,
|
|
776
780
|
collapseBelow,
|
|
781
|
+
label,
|
|
777
782
|
rowClassName,
|
|
778
783
|
rowTone,
|
|
779
784
|
bordered,
|
|
@@ -816,6 +821,7 @@ DataTable.Content = function DataTableContent() {
|
|
|
816
821
|
}, [missingHeaderNames]);
|
|
817
822
|
const selectLabelColumn = visibleColumns.find((col) => col.priority === "primary") ?? visibleColumns[0];
|
|
818
823
|
const scrollRef = React.useRef(null);
|
|
824
|
+
const scrolls = useScrollsHorizontally(scrollRef, !presetAttr);
|
|
819
825
|
const [hasOverflowEnd, setHasOverflowEnd] = React.useState(false);
|
|
820
826
|
React.useEffect(() => {
|
|
821
827
|
const el = scrollRef.current;
|
|
@@ -923,7 +929,7 @@ DataTable.Content = function DataTableContent() {
|
|
|
923
929
|
"data-scroll-x": scroll?.x !== void 0 ? "" : void 0,
|
|
924
930
|
"data-scroll-y": scroll?.y !== void 0 ? "" : void 0,
|
|
925
931
|
"aria-busy": loading,
|
|
926
|
-
|
|
932
|
+
...scrolls ? { role: "group", "aria-label": scrollRegionLabel(label, t), tabIndex: 0 } : {},
|
|
927
933
|
children: /* @__PURE__ */ jsx(
|
|
928
934
|
"div",
|
|
929
935
|
{
|
|
@@ -936,6 +942,7 @@ DataTable.Content = function DataTableContent() {
|
|
|
936
942
|
Table,
|
|
937
943
|
{
|
|
938
944
|
scrollable: preset !== "default",
|
|
945
|
+
label,
|
|
939
946
|
preset,
|
|
940
947
|
collapseBelow,
|
|
941
948
|
bordered,
|
|
@@ -982,7 +989,7 @@ DataTable.Content = function DataTableContent() {
|
|
|
982
989
|
) : null;
|
|
983
990
|
const headerEmpty = isEmptyHeader(col.header);
|
|
984
991
|
const headerContent = headerEmpty && col.ariaLabel ? /* @__PURE__ */ jsx("span", { className: "sr-only", children: col.ariaLabel }) : col.header;
|
|
985
|
-
const
|
|
992
|
+
const label2 = /* @__PURE__ */ jsxs("span", { className: "ui-data-table-sort-label", children: [
|
|
986
993
|
headerContent,
|
|
987
994
|
sortIndicator
|
|
988
995
|
] });
|
|
@@ -1000,7 +1007,7 @@ DataTable.Content = function DataTableContent() {
|
|
|
1000
1007
|
onClick: () => {
|
|
1001
1008
|
onHeaderClick(col);
|
|
1002
1009
|
},
|
|
1003
|
-
children:
|
|
1010
|
+
children: label2
|
|
1004
1011
|
}
|
|
1005
1012
|
);
|
|
1006
1013
|
return /* @__PURE__ */ jsxs(
|
|
@@ -1027,7 +1034,7 @@ DataTable.Content = function DataTableContent() {
|
|
|
1027
1034
|
isSortable ? tooltipOn ? /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
1028
1035
|
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: sortButton }),
|
|
1029
1036
|
/* @__PURE__ */ jsx(TooltipContent, { children: sortHint })
|
|
1030
|
-
] }) : sortButton :
|
|
1037
|
+
] }) : sortButton : label2,
|
|
1031
1038
|
col.filters ? /* @__PURE__ */ jsx(ColumnFilterMenu, { column: col }) : null
|
|
1032
1039
|
]
|
|
1033
1040
|
},
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { useTranslation } from "../../i18n/use-translation.js";
|
|
5
|
+
import { useScrollsOnAxis } from "../../lib/hooks.js";
|
|
4
6
|
import { cn } from "../../lib/utils.js";
|
|
5
7
|
const ANCHOR_OFFSET_TOKEN = "--scroll-area-anchor-offset";
|
|
6
8
|
const ANCHOR_OFFSET_FALLBACK_PX = 48;
|
|
@@ -121,12 +123,16 @@ const ScrollArea = React.forwardRef(
|
|
|
121
123
|
onAnchoredChange,
|
|
122
124
|
orientation = "vertical",
|
|
123
125
|
scrollbar = "auto",
|
|
126
|
+
label,
|
|
124
127
|
...props
|
|
125
128
|
}, ref) => {
|
|
129
|
+
const { t } = useTranslation();
|
|
126
130
|
const [viewport, setViewport] = React.useState(null);
|
|
131
|
+
const viewportHandle = React.useRef(null);
|
|
127
132
|
const attachViewport = React.useCallback(
|
|
128
133
|
(node) => {
|
|
129
134
|
setViewport(node);
|
|
135
|
+
viewportHandle.current = node;
|
|
130
136
|
for (const target of [ref, viewportRef]) {
|
|
131
137
|
if (typeof target === "function") target(node);
|
|
132
138
|
else if (target) target.current = node;
|
|
@@ -135,9 +141,17 @@ const ScrollArea = React.forwardRef(
|
|
|
135
141
|
[ref, viewportRef]
|
|
136
142
|
);
|
|
137
143
|
useBottomAnchor(viewport, anchor === "bottom", anchorOffset, onAnchoredChange);
|
|
144
|
+
const scrolls = useScrollsOnAxis(viewportHandle, true, orientation);
|
|
145
|
+
const regionLabel = typeof label === "string" && label.trim() !== "" ? label : t("dataDisplay.scrollArea.region");
|
|
138
146
|
return (
|
|
139
147
|
// `tabIndex={0}` keeps the scroll viewport keyboard-reachable so overflowing content can be
|
|
140
|
-
// scrolled without a pointer (WCAG 2.1.1 / axe scrollable-region-focusable).
|
|
148
|
+
// scrolled without a pointer (WCAG 2.1.1 / axe scrollable-region-focusable). That stop must
|
|
149
|
+
// not be ANONYMOUS: unroled and unnamed it announces nothing, so it carries `role="group"`
|
|
150
|
+
// plus an accessible name (gh#821, the same defect gh#817 fixed in Table). `group`, not
|
|
151
|
+
// `region`: a named `region` IS a landmark, and a page with several scroll areas would ship
|
|
152
|
+
// several identically-named landmarks (axe `landmark-unique`). All three attributes come and
|
|
153
|
+
// go together with the overflow, because a focus stop that scrolls nothing is noise — and
|
|
154
|
+
// they precede `{...props}`, so a consumer's own `role`/`aria-label`/`tabIndex` still wins.
|
|
141
155
|
//
|
|
142
156
|
// NO `dir` IS STAMPED HERE, deliberately. Radix's Root called `useDirection(dir)`, which
|
|
143
157
|
// falls back to the literal "ltr" and writes it onto the element — and a `dir` attribute is
|
|
@@ -150,12 +164,12 @@ const ScrollArea = React.forwardRef(
|
|
|
150
164
|
"div",
|
|
151
165
|
{
|
|
152
166
|
ref: attachViewport,
|
|
153
|
-
tabIndex: 0,
|
|
154
167
|
"data-slot": "scroll-area-viewport",
|
|
155
168
|
"data-anchor": anchor,
|
|
156
169
|
"data-orientation": orientation,
|
|
157
170
|
"data-scrollbar": scrollbar,
|
|
158
171
|
className: cn("ui-scroll-area", className),
|
|
172
|
+
...scrolls ? { role: "group", "aria-label": regionLabel, tabIndex: 0 } : {},
|
|
159
173
|
...props,
|
|
160
174
|
children: /* @__PURE__ */ jsx("div", { "data-slot": "scroll-area-content", className: "ui-scroll-area-content", children })
|
|
161
175
|
}
|
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RECORDED EXCEPTION to the Framework-Component Test (gh#814).
|
|
3
|
+
*
|
|
4
|
+
* `docs/COMPOSITION-VS-COMPONENT.md` §2 says all seven criteria must PASS and any FAIL makes a
|
|
5
|
+
* thing a composition pattern. This file FAILS two of them, and stays anyway:
|
|
6
|
+
*
|
|
7
|
+
* C2 (encapsulates reusable BEHAVIOR) — FAIL. It owns no state, no keyboard handling and no
|
|
8
|
+
* focus management; its only ARIA is three static attributes. §2 calls this "pure static
|
|
9
|
+
* layout/visual arrangement".
|
|
10
|
+
* C3 (not expressible by composing primitives) — FAIL. Its own imports are the answer: this is
|
|
11
|
+
* `Card` + `CardContent` + `Badge` + one Lucide glyph, and §2's heuristic — "could I build
|
|
12
|
+
* this right now from existing primitives + token overrides?" — is answered yes.
|
|
13
|
+
*
|
|
14
|
+
* It is RETAINED because `src/components/layout/app-launcher.tsx` consumes it: it is an internal
|
|
15
|
+
* building block of a component that does pass the test, so deleting it is not on the table, and
|
|
16
|
+
* removing a public export is breaking. Keeping it public was the cheaper call.
|
|
17
|
+
*
|
|
18
|
+
* SO: a consumer reaching for a service tile should compose `Card` + `CardContent` + `Badge`
|
|
19
|
+
* themselves, which is what C3 says. Reach for `ServiceLauncherCard` only to match `AppLauncher`'s
|
|
20
|
+
* own tiles. Do not cite it as precedent for adding another static tile to `src/components/` —
|
|
21
|
+
* the test still means what it says, and this is the exception that was argued and written down
|
|
22
|
+
* rather than the rule.
|
|
23
|
+
*
|
|
24
|
+
* Ledger, options considered and the decision: gh#814.
|
|
25
|
+
*/
|
|
1
26
|
import * as React from "react";
|
|
2
27
|
import type { LucideIcon } from "lucide-react";
|
|
3
28
|
import type { HeadingLevelProp, ToneProp } from "../../props/vocabulary/index.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import type { BreakpointProp, FlushProp, TableCellIndentProp, TableColumnPriorityProp, TablePresetProp, WidthProp } from "../../props/vocabulary/index.js";
|
|
2
|
+
import type { BreakpointProp, FlushProp, LabelProp, TableCellIndentProp, TableColumnPriorityProp, TablePresetProp, WidthProp } from "../../props/vocabulary/index.js";
|
|
3
3
|
export type TableProps = React.HTMLAttributes<HTMLTableElement> & {
|
|
4
4
|
/**
|
|
5
5
|
* Whether the Table owns its own horizontal-scroll region (default `true`). When `true` a table
|
|
@@ -58,7 +58,29 @@ export type TableProps = React.HTMLAttributes<HTMLTableElement> & {
|
|
|
58
58
|
};
|
|
59
59
|
/** Defaults to `"sm"` (40rem). Ignored while `preset` is `"default"`. */
|
|
60
60
|
collapseBelow?: BreakpointProp;
|
|
61
|
+
/**
|
|
62
|
+
* Accessible name for the horizontal-scroll REGION — the `tabindex="0"` wrapper a keyboard user
|
|
63
|
+
* lands on to scroll a table wider than its container, NOT the `<table>` itself (pass `aria-label`
|
|
64
|
+
* for that; it reaches the table element as it always has).
|
|
65
|
+
*
|
|
66
|
+
* Optional on purpose. A consumer is never forced to invent a name for every table: left out, the
|
|
67
|
+
* region takes the localized `dataTable.scrollRegion` default ("Scrollable table"), which is what
|
|
68
|
+
* a screen-reader user needs to hear anyway — that the arrow keys now scroll something. Pass a
|
|
69
|
+
* plain string when the page can say WHICH table ("Pending approvals"); a non-string node cannot
|
|
70
|
+
* be an `aria-label`, so it falls back to the default (the PermissionMatrix `label` contract).
|
|
71
|
+
*
|
|
72
|
+
* The region is only announced while it HAS overflow to reach: no overflow, no tab stop, no role
|
|
73
|
+
* and no name, because a focus stop that scrolls nothing is noise. (gh#817)
|
|
74
|
+
*/
|
|
75
|
+
label?: LabelProp;
|
|
61
76
|
};
|
|
77
|
+
/**
|
|
78
|
+
* The scroll region's accessible name. A `label` is only usable as an `aria-label` when it is a
|
|
79
|
+
* plain string, and no consumer is obliged to supply one at all — so anything else takes the
|
|
80
|
+
* localized default, which still tells the user what the stop is for. Shared with `DataTable`, so
|
|
81
|
+
* both tables name their region the same way.
|
|
82
|
+
*/
|
|
83
|
+
export declare function scrollRegionLabel(label: LabelProp | undefined, t: (key: string) => string): string;
|
|
62
84
|
export declare const Table: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLTableElement> & {
|
|
63
85
|
/**
|
|
64
86
|
* Whether the Table owns its own horizontal-scroll region (default `true`). When `true` a table
|
|
@@ -117,6 +139,21 @@ export declare const Table: React.ForwardRefExoticComponent<React.HTMLAttributes
|
|
|
117
139
|
};
|
|
118
140
|
/** Defaults to `"sm"` (40rem). Ignored while `preset` is `"default"`. */
|
|
119
141
|
collapseBelow?: BreakpointProp;
|
|
142
|
+
/**
|
|
143
|
+
* Accessible name for the horizontal-scroll REGION — the `tabindex="0"` wrapper a keyboard user
|
|
144
|
+
* lands on to scroll a table wider than its container, NOT the `<table>` itself (pass `aria-label`
|
|
145
|
+
* for that; it reaches the table element as it always has).
|
|
146
|
+
*
|
|
147
|
+
* Optional on purpose. A consumer is never forced to invent a name for every table: left out, the
|
|
148
|
+
* region takes the localized `dataTable.scrollRegion` default ("Scrollable table"), which is what
|
|
149
|
+
* a screen-reader user needs to hear anyway — that the arrow keys now scroll something. Pass a
|
|
150
|
+
* plain string when the page can say WHICH table ("Pending approvals"); a non-string node cannot
|
|
151
|
+
* be an `aria-label`, so it falls back to the default (the PermissionMatrix `label` contract).
|
|
152
|
+
*
|
|
153
|
+
* The region is only announced while it HAS overflow to reach: no overflow, no tab stop, no role
|
|
154
|
+
* and no name, because a focus stop that scrolls nothing is noise. (gh#817)
|
|
155
|
+
*/
|
|
156
|
+
label?: LabelProp;
|
|
120
157
|
} & React.RefAttributes<HTMLTableElement>>;
|
|
121
158
|
export declare const TableHeader: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLTableSectionElement> & React.RefAttributes<HTMLTableSectionElement>>;
|
|
122
159
|
export declare const TableBody: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLTableSectionElement> & React.RefAttributes<HTMLTableSectionElement>>;
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
3
|
import * as React from "react";
|
|
4
|
+
import { useTranslation } from "../../i18n/use-translation.js";
|
|
3
5
|
import { tableHeadHeightClass } from "../../lib/control-styles.js";
|
|
6
|
+
import { useScrollsHorizontally } from "../../lib/hooks.js";
|
|
4
7
|
import { cn } from "../../lib/utils.js";
|
|
8
|
+
function scrollRegionLabel(label, t) {
|
|
9
|
+
return typeof label === "string" && label.trim() !== "" ? label : t("dataTable.scrollRegion");
|
|
10
|
+
}
|
|
5
11
|
const Table = React.forwardRef(
|
|
6
12
|
({
|
|
7
13
|
className,
|
|
@@ -11,45 +17,56 @@ const Table = React.forwardRef(
|
|
|
11
17
|
preset = "default",
|
|
12
18
|
collapseBelow = "sm",
|
|
13
19
|
columnWidths,
|
|
20
|
+
label,
|
|
14
21
|
...props
|
|
15
|
-
}, ref) =>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
{
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
22
|
+
}, ref) => {
|
|
23
|
+
const { t } = useTranslation();
|
|
24
|
+
const scrollRef = React.useRef(null);
|
|
25
|
+
const scrolls = useScrollsHorizontally(scrollRef, scrollable);
|
|
26
|
+
return (
|
|
27
|
+
// A table wider than its container scrolls horizontally in this wrapper; keep it
|
|
28
|
+
// keyboard-reachable so it can be scrolled without a pointer (WCAG 2.1.1 / axe
|
|
29
|
+
// scrollable-region-focusable). That stop must not be ANONYMOUS: an unnamed, unroled focus
|
|
30
|
+
// stop announces nothing at all, so it carries `role="group"` + an accessible name (gh#817).
|
|
31
|
+
// `group`, not `region`: a named `region` IS a landmark, and a page with three tables would
|
|
32
|
+
// then ship three same-named landmarks (axe `landmark-unique`) — the reason the role was left
|
|
33
|
+
// off in the first place. `group` is announced, takes a name, and is not a landmark.
|
|
34
|
+
// When `scrollable` is false an ancestor owns the scroll region, so this is a bare
|
|
35
|
+
// positioning box (no `overflow`, no tab stop) to avoid a redundant nested scroller.
|
|
36
|
+
// step attribute; with `preset="default"` neither is emitted, so the box is byte-identical.
|
|
37
|
+
/* @__PURE__ */ jsx(
|
|
38
|
+
"div",
|
|
39
|
+
{
|
|
40
|
+
ref: scrollRef,
|
|
41
|
+
className: cn(
|
|
42
|
+
scrollable ? "relative w-full overflow-auto" : "relative w-full",
|
|
43
|
+
preset === "action-collection" && "ui-table-collection",
|
|
44
|
+
preset === "stacked-record-collection" && "ui-table-stacked-collection"
|
|
45
|
+
),
|
|
46
|
+
"data-preset": preset === "default" ? void 0 : preset,
|
|
47
|
+
"data-collapse-below": preset === "default" ? void 0 : collapseBelow,
|
|
48
|
+
"data-column-widths": columnWidths ? "" : void 0,
|
|
49
|
+
style: columnWidths ? {
|
|
50
|
+
"--table-action-collection-actions-width": columnWidths.actions,
|
|
51
|
+
"--table-action-collection-actions-width-compact": columnWidths.actionsCompact,
|
|
52
|
+
"--table-action-collection-meta-width-compact": columnWidths.metaCompact,
|
|
53
|
+
"--table-action-collection-min-inline-size-compact": columnWidths.minInlineSizeCompact
|
|
54
|
+
} : void 0,
|
|
55
|
+
...scrolls ? { role: "group", "aria-label": scrollRegionLabel(label, t), tabIndex: 0 } : {},
|
|
56
|
+
children: /* @__PURE__ */ jsx(
|
|
57
|
+
"table",
|
|
58
|
+
{
|
|
59
|
+
ref,
|
|
60
|
+
"data-slot": "table",
|
|
61
|
+
"data-striped": striped === void 0 ? void 0 : striped ? "" : "false",
|
|
62
|
+
className: cn("w-full caption-bottom", bordered && "ui-table-bordered", className),
|
|
63
|
+
...props
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
53
70
|
);
|
|
54
71
|
Table.displayName = "Table";
|
|
55
72
|
const TableHeader = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("thead", { ref, className: cn("[&_tr]:border-b", className), ...props }));
|
|
@@ -140,5 +157,6 @@ export {
|
|
|
140
157
|
TableCell,
|
|
141
158
|
TableHead,
|
|
142
159
|
TableHeader,
|
|
143
|
-
TableRow
|
|
160
|
+
TableRow,
|
|
161
|
+
scrollRegionLabel
|
|
144
162
|
};
|
|
@@ -4,7 +4,6 @@ import * as React from "react";
|
|
|
4
4
|
import { CheckCircle, CircleAlert, LoaderCircle, TriangleAlert } from "lucide-react";
|
|
5
5
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
6
6
|
import { Label } from "../data-entry/label.js";
|
|
7
|
-
import { controlIconClass } from "../../lib/control-styles.js";
|
|
8
7
|
import { cn } from "../../lib/utils.js";
|
|
9
8
|
import { FieldIdentityContext, FieldNameContext, mergeAriaIds } from "../../lib/field-a11y.js";
|
|
10
9
|
import { useOptionalAppContext } from "../../app/app-provider.js";
|
|
@@ -107,7 +106,16 @@ function FormField({
|
|
|
107
106
|
errorId
|
|
108
107
|
),
|
|
109
108
|
"aria-required": required ? true : childProps?.["aria-required"],
|
|
110
|
-
"aria-invalid": validationStatus === "error" ? true : childProps?.["aria-invalid"]
|
|
109
|
+
"aria-invalid": validationStatus === "error" ? true : childProps?.["aria-invalid"],
|
|
110
|
+
/* THE BOX SHOWS ITS OWN STATE, not just the line under it. `error` already reached the
|
|
111
|
+
* control through `aria-invalid`, but `warning` reached nothing — so a warned field drew an
|
|
112
|
+
* ordinary border and the only signal was a sentence below it. `.ui-control[data-status]`
|
|
113
|
+
* has painted both edges for a long time; FormField simply never passed the value down.
|
|
114
|
+
*
|
|
115
|
+
* Only `error` and `warning` are sent: `ControlStatusProp` is exactly those two, and
|
|
116
|
+
* `success` / `validating` have no control-level representation today. A child that sets its
|
|
117
|
+
* own `status` keeps it. */
|
|
118
|
+
...validationStatus === "error" || validationStatus === "warning" ? { status: childProps?.status ?? validationStatus } : {}
|
|
111
119
|
}) : children;
|
|
112
120
|
const style = {};
|
|
113
121
|
if (labelWidth != null)
|
|
@@ -166,16 +174,25 @@ function FormField({
|
|
|
166
174
|
children: childWithA11y
|
|
167
175
|
}
|
|
168
176
|
) }) }),
|
|
169
|
-
hasFeedback && validationStatus ? /* @__PURE__ */ jsxs(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
hasFeedback && validationStatus ? /* @__PURE__ */ jsxs(
|
|
178
|
+
"span",
|
|
179
|
+
{
|
|
180
|
+
id: feedbackId,
|
|
181
|
+
role: "status",
|
|
182
|
+
"data-status": validationStatus,
|
|
183
|
+
className: "ui-form-feedback ui-inline-xs text-xs",
|
|
184
|
+
children: [
|
|
185
|
+
feedback ?? /* @__PURE__ */ jsx(
|
|
186
|
+
FeedbackIcon,
|
|
187
|
+
{
|
|
188
|
+
"aria-hidden": "true",
|
|
189
|
+
className: validationStatus === "validating" ? "motion-safe:animate-spin" : void 0
|
|
190
|
+
}
|
|
191
|
+
),
|
|
192
|
+
t(`dataEntry.form.${validationStatus}`)
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
) : null,
|
|
179
196
|
helperPlacement === "after" ? helperNode : null,
|
|
180
197
|
error ? /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "ui-form-field-note text-error-strong text-xs", children: error }) : null
|
|
181
198
|
] })
|
|
@@ -186,10 +186,27 @@ const AppSettingPicker = React.forwardRef(
|
|
|
186
186
|
)
|
|
187
187
|
) : (
|
|
188
188
|
// Labeled: sized to a per-kind width from `sm` up; below `sm` it hugs its content and
|
|
189
|
-
// A form field that wants a full-width control passes
|
|
190
|
-
// `className="w-full"`, which wins over `w-auto`.
|
|
191
189
|
// `compact` drops the per-kind width entirely so the trigger hugs its value.
|
|
192
|
-
|
|
190
|
+
//
|
|
191
|
+
// The width is a UTILITY READING A TOKEN, for the third time in this file's
|
|
192
|
+
// history and for the same reason (gh#366 icon, gh#375 Select, gh#819 here):
|
|
193
|
+
// @layer utilities is ordered after @layer components, so the `w-auto` that
|
|
194
|
+
// stood here outranked `.ui-app-setting-picker-trigger[data-kind]`'s
|
|
195
|
+
// `inline-size` and every one of the eight per-kind tokens was inert — a
|
|
196
|
+
// consumer who retuned --app-setting-picker-density-width saw nothing move.
|
|
197
|
+
// `--app-setting-picker-trigger-width` is what `data-kind` resolves in
|
|
198
|
+
// navigation-layout.css (a custom property, which no utility competes with);
|
|
199
|
+
// it defaults to `auto`, so below the breakpoint and under `compact` this is
|
|
200
|
+
// byte-for-byte the old `w-auto` behaviour.
|
|
201
|
+
//
|
|
202
|
+
// It stays a SINGLE unprefixed `w-*` (not `sm:w-[…]` per kind): `cn` is
|
|
203
|
+
// tailwind-merge, so a form field that wants a full-width control still passes
|
|
204
|
+
// `className="w-full"` and REPLACES this one. A `sm:`-variant utility would
|
|
205
|
+
// survive that merge and quietly ignore the consumer above 40rem.
|
|
206
|
+
cn(
|
|
207
|
+
"w-[length:var(--app-setting-picker-trigger-width)] max-w-full",
|
|
208
|
+
!isCompact && "ui-app-setting-picker-trigger"
|
|
209
|
+
)
|
|
193
210
|
),
|
|
194
211
|
// Compact re-tiers the box through tokens (--app-setting-picker-compact-*); the height
|
|
195
212
|
// still comes from the official --control-height-sm tier, never a literal. The gap is a
|
|
@@ -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": "28.
|
|
3
|
+
"version": "28.7.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,
|
|
@@ -325,6 +325,9 @@
|
|
|
325
325
|
"ariaLabel": "Progress",
|
|
326
326
|
"breakdownSeparator": ", "
|
|
327
327
|
},
|
|
328
|
+
"scrollArea": {
|
|
329
|
+
"region": "Scrollable region"
|
|
330
|
+
},
|
|
328
331
|
"tree": {
|
|
329
332
|
"expand": "Expand",
|
|
330
333
|
"collapse": "Collapse",
|
|
@@ -537,7 +540,8 @@
|
|
|
537
540
|
"sortAscending": "Sort ascending",
|
|
538
541
|
"sortDescending": "Sort descending",
|
|
539
542
|
"sortCancel": "Cancel sort",
|
|
540
|
-
"selectColumn": "Select"
|
|
543
|
+
"selectColumn": "Select",
|
|
544
|
+
"scrollRegion": "Scrollable table"
|
|
541
545
|
},
|
|
542
546
|
"dataGrid": {
|
|
543
547
|
"search": "Search",
|
|
@@ -318,6 +318,9 @@
|
|
|
318
318
|
"ariaLabel": "進捗",
|
|
319
319
|
"breakdownSeparator": "、"
|
|
320
320
|
},
|
|
321
|
+
"scrollArea": {
|
|
322
|
+
"region": "スクロール可能な領域"
|
|
323
|
+
},
|
|
321
324
|
"tree": {
|
|
322
325
|
"expand": "展開",
|
|
323
326
|
"collapse": "折りたたむ",
|
|
@@ -524,7 +527,8 @@
|
|
|
524
527
|
"sortAscending": "昇順に並べ替え",
|
|
525
528
|
"sortDescending": "降順に並べ替え",
|
|
526
529
|
"sortCancel": "並べ替えを解除",
|
|
527
|
-
"selectColumn": "選択"
|
|
530
|
+
"selectColumn": "選択",
|
|
531
|
+
"scrollRegion": "スクロール可能な表"
|
|
528
532
|
},
|
|
529
533
|
"dataGrid": {
|
|
530
534
|
"search": "検索",
|
|
@@ -319,6 +319,9 @@
|
|
|
319
319
|
"ariaLabel": "Tiến độ",
|
|
320
320
|
"breakdownSeparator": ", "
|
|
321
321
|
},
|
|
322
|
+
"scrollArea": {
|
|
323
|
+
"region": "Vùng có thể cuộn"
|
|
324
|
+
},
|
|
322
325
|
"tree": {
|
|
323
326
|
"expand": "Mở rộng",
|
|
324
327
|
"collapse": "Thu gọn",
|
|
@@ -525,7 +528,8 @@
|
|
|
525
528
|
"sortAscending": "Sắp xếp tăng dần",
|
|
526
529
|
"sortDescending": "Sắp xếp giảm dần",
|
|
527
530
|
"sortCancel": "Bỏ sắp xếp",
|
|
528
|
-
"selectColumn": "Chọn"
|
|
531
|
+
"selectColumn": "Chọn",
|
|
532
|
+
"scrollRegion": "Bảng có thể cuộn"
|
|
529
533
|
},
|
|
530
534
|
"dataGrid": {
|
|
531
535
|
"search": "Tìm kiếm",
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
1
2
|
/**
|
|
2
3
|
* Returns a debounced view of `value`, updated only after `delay` ms of no
|
|
3
4
|
* change. Use for search inputs to avoid querying on every keystroke.
|
|
@@ -37,3 +38,25 @@ export declare function useIsMobile(): boolean;
|
|
|
37
38
|
* @param element the scroll container itself (state, not a ref, so the effect re-runs when it mounts)
|
|
38
39
|
*/
|
|
39
40
|
export declare function useScrollableRegionTabIndex(element: HTMLElement | null): void;
|
|
41
|
+
/**
|
|
42
|
+
* Does this scroll box actually have somewhere to scroll, on the axes it is allowed to scroll on?
|
|
43
|
+
*
|
|
44
|
+
* Drives BOTH halves of a scroll region: the `tabindex="0"` a keyboard user needs to reach the
|
|
45
|
+
* overflow (WCAG 2.1.1 / axe `scrollable-region-focusable`) AND the `role`/name that stop needs in
|
|
46
|
+
* order not to be an anonymous one (gh#817 for the table, gh#821 for `ScrollArea`). A stop that
|
|
47
|
+
* scrolls nothing is pure noise, and a name on it is noise too, so both are withheld until there is
|
|
48
|
+
* overflow to reach.
|
|
49
|
+
*
|
|
50
|
+
* The measurement may only ever REMOVE the stop, never withhold it on a guess: a box that has not
|
|
51
|
+
* been laid out reports 0 for `clientWidth`/`clientHeight` (the server render, jsdom, a
|
|
52
|
+
* `display:none` ancestor, the frame before first layout), and that is not evidence that nothing
|
|
53
|
+
* overflows. Reading it as "no overflow" would strand the overflow from every keyboard user — a
|
|
54
|
+
* worse failure than an extra tab stop — so an unmeasured box counts as scrolling.
|
|
55
|
+
*
|
|
56
|
+
* `axis` is the box's OWN `overflow`, not a preference: an axis it does not scroll on is `hidden`
|
|
57
|
+
* there, so overflow on that axis is CLIPPED rather than reachable, and measuring it would keep a
|
|
58
|
+
* tab stop that scrolls nothing. `ScrollArea`'s `orientation` is exactly this union.
|
|
59
|
+
*/
|
|
60
|
+
export declare function useScrollsOnAxis(ref: RefObject<HTMLElement | null>, enabled: boolean, axis: "horizontal" | "vertical" | "both"): boolean;
|
|
61
|
+
/** A table's wrapper scrolls on one axis only, so it asks the one question it has (gh#817). */
|
|
62
|
+
export declare function useScrollsHorizontally(ref: RefObject<HTMLElement | null>, enabled: boolean): boolean;
|
package/dist/lib/hooks.js
CHANGED
|
@@ -104,11 +104,38 @@ function useScrollableRegionTabIndex(element) {
|
|
|
104
104
|
};
|
|
105
105
|
}, [element]);
|
|
106
106
|
}
|
|
107
|
+
function useScrollsOnAxis(ref, enabled, axis) {
|
|
108
|
+
const [scrolls, setScrolls] = useState(true);
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const el = ref.current;
|
|
111
|
+
if (!enabled || !el) return void 0;
|
|
112
|
+
const overflows = (client, scroll) => client === 0 || scroll - client > 1;
|
|
113
|
+
const update = () => {
|
|
114
|
+
setScrolls(
|
|
115
|
+
axis !== "vertical" && overflows(el.clientWidth, el.scrollWidth) || axis !== "horizontal" && overflows(el.clientHeight, el.scrollHeight)
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
update();
|
|
119
|
+
if (typeof ResizeObserver === "undefined") return void 0;
|
|
120
|
+
const observer = new ResizeObserver(update);
|
|
121
|
+
observer.observe(el);
|
|
122
|
+
if (el.firstElementChild) observer.observe(el.firstElementChild);
|
|
123
|
+
return () => {
|
|
124
|
+
observer.disconnect();
|
|
125
|
+
};
|
|
126
|
+
}, [ref, enabled, axis]);
|
|
127
|
+
return enabled && scrolls;
|
|
128
|
+
}
|
|
129
|
+
function useScrollsHorizontally(ref, enabled) {
|
|
130
|
+
return useScrollsOnAxis(ref, enabled, "horizontal");
|
|
131
|
+
}
|
|
107
132
|
export {
|
|
108
133
|
useControlledLatch,
|
|
109
134
|
useDebouncedValue,
|
|
110
135
|
useIsMobile,
|
|
111
136
|
useMediaQuery,
|
|
112
137
|
useScrollableRegionTabIndex,
|
|
138
|
+
useScrollsHorizontally,
|
|
139
|
+
useScrollsOnAxis,
|
|
113
140
|
useTimeoutFlag
|
|
114
141
|
};
|