@nexub/react 5.6.0 → 5.8.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/accessibility.json +16 -5
- package/dist/components/CommandPalette/CommandPalette.js +2 -1
- package/dist/components/DataTable/DataTable.d.ts +6 -1
- package/dist/components/DataTable/DataTable.js +7 -0
- package/dist/components/DataTable/gridNavigation.d.ts +11 -0
- package/dist/components/DataTable/gridNavigation.js +52 -0
- package/dist/components/DatePicker/DatePicker.js +2 -2
- package/dist/components/FilterBuilder/FilterBuilder.d.ts +3 -32
- package/dist/components/FilterBuilder/FilterBuilder.js +9 -22
- package/package.json +6 -6
package/accessibility.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "5.
|
|
2
|
+
"version": "5.8.0",
|
|
3
3
|
"standard": "WCAG 2.2 Level AA",
|
|
4
4
|
"method": {
|
|
5
|
-
"automated": "axe-core rules tagged wcag2a, wcag2aa, wcag21a, wcag21aa and wcag22aa, run on every Storybook story in light and dark on every pull request; a new violation fails the build.",
|
|
5
|
+
"automated": "axe-core rules tagged wcag2a, wcag2aa, wcag21a, wcag21aa and wcag22aa, run on every Storybook story in light and dark, and on open popups (menus, listboxes, dialogs), on every pull request; a new violation fails the build.",
|
|
6
6
|
"contrast": "Every colour role pair is tested for 4.5:1 (text) and 3:1 (non-text) at standard, medium and high contrast in light and dark.",
|
|
7
|
-
"manual": "Keyboard behaviour and screen-reader output are documented per component
|
|
7
|
+
"manual": "Keyboard behaviour and screen-reader output are documented per component, with step-by-step VoiceOver and NVDA scripts for the complex ones (/foundations/accessibility#screen-reader-scripts); recorded sessions are not published yet."
|
|
8
8
|
},
|
|
9
9
|
"exceptions": [
|
|
10
10
|
{
|
|
@@ -310,8 +310,19 @@
|
|
|
310
310
|
"package": "@nexub/react",
|
|
311
311
|
"automated": "pass",
|
|
312
312
|
"exceptions": [],
|
|
313
|
-
"keyboard": [
|
|
314
|
-
|
|
313
|
+
"keyboard": [
|
|
314
|
+
"Tab: Without `grid`: every control in the table is a tab stop. With `grid`: the table is one tab stop and remembers the last cell.",
|
|
315
|
+
"Arrow keys: With `grid`: move between cells, and between the controls inside a cell. Mirrored in RTL.",
|
|
316
|
+
"Home / End: With `grid`: first or last cell in the row. Ctrl+Home / Ctrl+End: first or last cell in the grid.",
|
|
317
|
+
"Space: Toggles a focused row checkbox.",
|
|
318
|
+
"Alt+Arrow: Moves a focused header when `enableColumnReordering` is on."
|
|
319
|
+
],
|
|
320
|
+
"screenReader": [
|
|
321
|
+
"Name the table with `caption` (or `aria-label`); `captionHidden` keeps it for screen readers only.",
|
|
322
|
+
"Sorting, selection counts and column moves are announced from a polite live region.",
|
|
323
|
+
"DataTableVirtual sets aria-rowcount and aria-rowindex so readers report the full row count.",
|
|
324
|
+
"With `grid`, focus goes to a cell’s first control, or to the cell itself when it has none."
|
|
325
|
+
]
|
|
315
326
|
},
|
|
316
327
|
{
|
|
317
328
|
"slug": "date-picker",
|
|
@@ -9,6 +9,7 @@ import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
|
|
|
9
9
|
import { overlaySurface, scrim } from "../../utils/surface.js";
|
|
10
10
|
import { useClassScrollLock } from "../../hooks/useClassScrollLock.js";
|
|
11
11
|
import { useControllableState } from "../../hooks/useControllableState.js";
|
|
12
|
+
import { useMergedRefs } from "../../hooks/useMergedRefs.js";
|
|
12
13
|
const overlayClasses = cn("fixed inset-0 z-[var(--z-modal)] nx-motion", scrim);
|
|
13
14
|
function ClassLockOverlay() {
|
|
14
15
|
useClassScrollLock();
|
|
@@ -136,7 +137,7 @@ Item.displayName = "CommandPalette.Item";
|
|
|
136
137
|
const Separator = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
137
138
|
Command.Separator,
|
|
138
139
|
{
|
|
139
|
-
ref,
|
|
140
|
+
ref: useMergedRefs(ref, (el) => el?.setAttribute("role", "none")),
|
|
140
141
|
className: cn("-mx-1 my-1 h-px bg-[var(--color-outline-variant)]", className),
|
|
141
142
|
...props
|
|
142
143
|
}
|
|
@@ -64,10 +64,15 @@ export interface DataTableProps<TData> extends React.HTMLAttributes<HTMLDivEleme
|
|
|
64
64
|
onColumnPinningChange?: (pinning: ColumnPinningState) => void;
|
|
65
65
|
/** Cap the table's height. Rows scroll and the header stays in view. */
|
|
66
66
|
maxHeight?: number | string;
|
|
67
|
+
/**
|
|
68
|
+
* ARIA grid keyboard model for tables whose cells hold actions: the table is
|
|
69
|
+
* one tab stop, and Arrow keys, Home/End and Ctrl+Home/End move between cells.
|
|
70
|
+
*/
|
|
71
|
+
grid?: boolean;
|
|
67
72
|
/** Rows to render; set by DataTableVirtual. */
|
|
68
73
|
rowWindow?: DataTableRowWindowHook;
|
|
69
74
|
pagination?: {
|
|
70
75
|
pageSize?: number;
|
|
71
76
|
};
|
|
72
77
|
}
|
|
73
|
-
export declare function DataTable<TData>({ columns, data, enableSorting, enableRowSelection, selectionColumn, rowSelection: rowSelectionProp, onRowSelectionChange, getRowId, bulkActions, density: densityProp, defaultDensity, onDensityChange, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, viewOptions, toolbar, enableColumnResizing, columnSizing: columnSizingProp, onColumnSizingChange, enableColumnReordering, columnOrder: columnOrderProp, onColumnOrderChange, columnPinning: columnPinningProp, onColumnPinningChange, maxHeight, rowWindow, pagination, caption, captionHidden, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: DataTableProps<TData>): React.JSX.Element;
|
|
78
|
+
export declare function DataTable<TData>({ columns, data, enableSorting, enableRowSelection, selectionColumn, rowSelection: rowSelectionProp, onRowSelectionChange, getRowId, bulkActions, density: densityProp, defaultDensity, onDensityChange, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, viewOptions, toolbar, enableColumnResizing, columnSizing: columnSizingProp, onColumnSizingChange, enableColumnReordering, columnOrder: columnOrderProp, onColumnOrderChange, columnPinning: columnPinningProp, onColumnPinningChange, maxHeight, grid, rowWindow, pagination, caption, captionHidden, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: DataTableProps<TData>): React.JSX.Element;
|
|
@@ -20,6 +20,7 @@ import { Table } from "../Table/Table.js";
|
|
|
20
20
|
import { Checkbox } from "../Checkbox/Checkbox.js";
|
|
21
21
|
import { Button } from "../Button/Button.js";
|
|
22
22
|
import { DropdownMenu } from "../DropdownMenu/DropdownMenu.js";
|
|
23
|
+
import { createGridNavigation } from "./gridNavigation.js";
|
|
23
24
|
const alignText = {
|
|
24
25
|
start: "text-start",
|
|
25
26
|
end: "text-end",
|
|
@@ -130,6 +131,7 @@ function DataTable({
|
|
|
130
131
|
columnPinning: columnPinningProp,
|
|
131
132
|
onColumnPinningChange,
|
|
132
133
|
maxHeight,
|
|
134
|
+
grid = false,
|
|
133
135
|
rowWindow,
|
|
134
136
|
pagination,
|
|
135
137
|
caption,
|
|
@@ -174,6 +176,7 @@ function DataTable({
|
|
|
174
176
|
});
|
|
175
177
|
const [announcement, setAnnouncement] = React.useState("");
|
|
176
178
|
const tableRef = React.useRef(null);
|
|
179
|
+
const gridNav = React.useMemo(() => createGridNavigation(dir), [dir]);
|
|
177
180
|
const dragged = React.useRef(null);
|
|
178
181
|
const [paginationState, setPaginationState] = React.useState({
|
|
179
182
|
pageIndex: 0,
|
|
@@ -274,6 +277,7 @@ function DataTable({
|
|
|
274
277
|
t.style.width = enableColumnResizing ? px(table.getTotalSize()) : "";
|
|
275
278
|
const scroller = t.parentElement;
|
|
276
279
|
if (scroller) scroller.style.maxHeight = maxHeight == null ? "" : px(maxHeight);
|
|
280
|
+
if (grid) gridNav.sync(t);
|
|
277
281
|
});
|
|
278
282
|
const placeCell = (column) => (el) => {
|
|
279
283
|
if (!el) return;
|
|
@@ -352,6 +356,9 @@ function DataTable({
|
|
|
352
356
|
"aria-label": ariaLabel,
|
|
353
357
|
"aria-labelledby": ariaLabelledBy,
|
|
354
358
|
"aria-rowcount": rowWin.virtual ? rows.length + 1 : void 0,
|
|
359
|
+
role: grid ? "grid" : void 0,
|
|
360
|
+
onKeyDown: grid ? gridNav.onKeyDown : void 0,
|
|
361
|
+
onFocus: grid ? gridNav.onFocus : void 0,
|
|
355
362
|
className: cn(enableColumnResizing && "table-fixed"),
|
|
356
363
|
children: [
|
|
357
364
|
caption != null && /* @__PURE__ */ jsx(Table.Caption, { className: cn(captionHidden && "sr-only"), children: caption }),
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type * as React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* ARIA grid keyboard model (WAI-ARIA APG "Data Grid"): one tab stop for the
|
|
4
|
+
* whole table; Arrow keys move between cells (and the controls inside them),
|
|
5
|
+
* Home/End to the row's ends, Ctrl+Home/End to the grid's corners.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createGridNavigation(dir: 'ltr' | 'rtl'): {
|
|
8
|
+
sync: (table: HTMLTableElement | null) => void;
|
|
9
|
+
onFocus: (e: React.FocusEvent<HTMLTableElement>) => void;
|
|
10
|
+
onKeyDown: (e: React.KeyboardEvent<HTMLTableElement>) => void;
|
|
11
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const FOCUSABLE = "a[href],button:not(:disabled),input:not(:disabled),select:not(:disabled),textarea:not(:disabled),[role=separator]";
|
|
2
|
+
const TEXT_INPUT = /^(text|search|email|number|tel|url|password|date)$/;
|
|
3
|
+
function stopsByRow(table) {
|
|
4
|
+
return Array.from(table.rows).filter((r) => !r.hasAttribute("aria-hidden")).map(
|
|
5
|
+
(r) => Array.from(r.cells).flatMap((cell) => {
|
|
6
|
+
const inner = Array.from(cell.querySelectorAll(FOCUSABLE));
|
|
7
|
+
return inner.length ? inner : [cell];
|
|
8
|
+
})
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
const cellIndex = (el) => el.closest("td,th").cellIndex;
|
|
12
|
+
function createGridNavigation(dir) {
|
|
13
|
+
let active = null;
|
|
14
|
+
const sync = (table) => {
|
|
15
|
+
if (!table) return;
|
|
16
|
+
const stops = stopsByRow(table).flat();
|
|
17
|
+
if (!active || !stops.includes(active)) active = stops[0] ?? null;
|
|
18
|
+
for (const s of stops) s.tabIndex = s === active ? 0 : -1;
|
|
19
|
+
};
|
|
20
|
+
const onFocus = (e) => {
|
|
21
|
+
active = e.target;
|
|
22
|
+
sync(e.currentTarget);
|
|
23
|
+
};
|
|
24
|
+
const onKeyDown = (e) => {
|
|
25
|
+
if (e.defaultPrevented || e.altKey) return;
|
|
26
|
+
const target = e.target;
|
|
27
|
+
const typing = target.matches("textarea,select,[contenteditable=true]") || target instanceof HTMLInputElement && TEXT_INPUT.test(target.type);
|
|
28
|
+
if (typing && e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
|
|
29
|
+
const rows = stopsByRow(e.currentTarget);
|
|
30
|
+
const r = rows.findIndex((row2) => row2.includes(target));
|
|
31
|
+
if (r < 0) return;
|
|
32
|
+
const row = rows[r];
|
|
33
|
+
const c = row.indexOf(target);
|
|
34
|
+
const forward = dir === "rtl" ? "ArrowLeft" : "ArrowRight";
|
|
35
|
+
const back = dir === "rtl" ? "ArrowRight" : "ArrowLeft";
|
|
36
|
+
const sameColumn = (to) => to && (to.find((s) => cellIndex(s) >= cellIndex(target)) ?? to.at(-1));
|
|
37
|
+
let next;
|
|
38
|
+
if (e.key === forward) next = row[c + 1];
|
|
39
|
+
else if (e.key === back) next = row[c - 1];
|
|
40
|
+
else if (e.key === "ArrowDown") next = sameColumn(rows[r + 1]);
|
|
41
|
+
else if (e.key === "ArrowUp") next = sameColumn(rows[r - 1]);
|
|
42
|
+
else if (e.key === "Home") next = e.ctrlKey ? rows[0][0] : row[0];
|
|
43
|
+
else if (e.key === "End") next = e.ctrlKey ? rows.at(-1).at(-1) : row.at(-1);
|
|
44
|
+
else return;
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
next?.focus();
|
|
47
|
+
};
|
|
48
|
+
return { sync, onFocus, onKeyDown };
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
createGridNavigation
|
|
52
|
+
};
|
|
@@ -29,7 +29,7 @@ function useCalendarLocale(localeProp) {
|
|
|
29
29
|
const locale = localeProp ?? dateLocale;
|
|
30
30
|
const labels = React.useMemo(() => {
|
|
31
31
|
const day = (date, modifiers, withSelected) => {
|
|
32
|
-
let label =
|
|
32
|
+
let label = new Intl.DateTimeFormat(intlLocale, { dateStyle: "full" }).format(date);
|
|
33
33
|
if (modifiers?.today) label = messages.todayDate(label);
|
|
34
34
|
if (withSelected && modifiers?.selected) label = messages.selectedDate(label);
|
|
35
35
|
return label;
|
|
@@ -41,7 +41,7 @@ function useCalendarLocale(localeProp) {
|
|
|
41
41
|
labelDayButton: (date, modifiers) => day(date, modifiers, true),
|
|
42
42
|
labelGridcell: (date, modifiers) => day(date, modifiers, false)
|
|
43
43
|
};
|
|
44
|
-
}, [messages,
|
|
44
|
+
}, [messages, intlLocale]);
|
|
45
45
|
return {
|
|
46
46
|
dayPickerProps: { locale, dir, labels, components: { Chevron: CalendarChevron } },
|
|
47
47
|
format: (date) => formatDate(date, intlLocale, locale)
|
|
@@ -1,36 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import type
|
|
3
|
-
export type FilterFieldType
|
|
4
|
-
export
|
|
5
|
-
export interface FilterField {
|
|
6
|
-
id: string;
|
|
7
|
-
label: string;
|
|
8
|
-
type: FilterFieldType;
|
|
9
|
-
/** Choices for `type: 'select'`. */
|
|
10
|
-
options?: {
|
|
11
|
-
value: string;
|
|
12
|
-
label: string;
|
|
13
|
-
}[];
|
|
14
|
-
}
|
|
15
|
-
export interface FilterRule {
|
|
16
|
-
id: string;
|
|
17
|
-
field: string;
|
|
18
|
-
operator: FilterOperator;
|
|
19
|
-
/** Always a string (dates as YYYY-MM-DD), so the query serialises as-is. */
|
|
20
|
-
value: string;
|
|
21
|
-
}
|
|
22
|
-
export interface FilterGroup {
|
|
23
|
-
id: string;
|
|
24
|
-
combinator: 'and' | 'or';
|
|
25
|
-
rules: Array<FilterRule | FilterGroup>;
|
|
26
|
-
}
|
|
27
|
-
/** Operators offered for each field type, in menu order. */
|
|
28
|
-
export declare const filterOperators: Record<FilterFieldType, FilterOperator[]>;
|
|
29
|
-
export declare const isFilterGroup: (r: FilterRule | FilterGroup) => r is FilterGroup;
|
|
30
|
-
/** An empty top-level group, a starting `value`. */
|
|
31
|
-
export declare const emptyFilter: () => FilterGroup;
|
|
32
|
-
/** Rules with a field, operator and (unless "is empty") a value. Counts nested groups too. */
|
|
33
|
-
export declare function countFilters(group: FilterGroup): number;
|
|
2
|
+
import { countFilters, emptyFilter, filterOperators, isFilterGroup, type FilterField, type FilterFieldType, type FilterGroup, type FilterOperator, type FilterRule } from '@nexub/core';
|
|
3
|
+
export type { FilterFieldType, FilterOperator, FilterField, FilterRule, FilterGroup };
|
|
4
|
+
export { filterOperators, isFilterGroup, emptyFilter, countFilters };
|
|
34
5
|
export interface FilterBuilderProps {
|
|
35
6
|
fields: FilterField[];
|
|
36
7
|
value?: FilterGroup;
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Plus, X } from "@nexub/icons";
|
|
5
|
+
import {
|
|
6
|
+
countFilters,
|
|
7
|
+
emptyFilter,
|
|
8
|
+
filterId as uid,
|
|
9
|
+
filterOperators,
|
|
10
|
+
isFilterGroup,
|
|
11
|
+
newFilterRule
|
|
12
|
+
} from "@nexub/core";
|
|
5
13
|
import { cn } from "../../utils/cn.js";
|
|
6
14
|
import { useControllableState } from "../../hooks/useControllableState.js";
|
|
7
15
|
import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
|
|
@@ -10,22 +18,6 @@ import { IconButton } from "../IconButton/IconButton.js";
|
|
|
10
18
|
import { Input } from "../Input/Input.js";
|
|
11
19
|
import { Select } from "../Select/Select.js";
|
|
12
20
|
import { SegmentedControl } from "../SegmentedControl/SegmentedControl.js";
|
|
13
|
-
const filterOperators = {
|
|
14
|
-
text: ["contains", "equals", "notEquals", "startsWith", "isEmpty"],
|
|
15
|
-
number: ["equals", "notEquals", "greaterThan", "lessThan"],
|
|
16
|
-
select: ["equals", "notEquals"],
|
|
17
|
-
date: ["equals", "before", "after"]
|
|
18
|
-
};
|
|
19
|
-
const isFilterGroup = (r) => "rules" in r;
|
|
20
|
-
let seq = 0;
|
|
21
|
-
const uid = () => `f${(++seq).toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
22
|
-
const emptyFilter = () => ({ id: uid(), combinator: "and", rules: [] });
|
|
23
|
-
function countFilters(group) {
|
|
24
|
-
return group.rules.reduce(
|
|
25
|
-
(n, r) => n + (isFilterGroup(r) ? countFilters(r) : r.operator === "isEmpty" || r.value !== "" ? 1 : 0),
|
|
26
|
-
0
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
21
|
function FilterBuilder({ fields, value: valueProp, defaultValue, onValueChange, maxDepth = 1, className }) {
|
|
30
22
|
const [value = emptyFilter(), setValue] = useControllableState({
|
|
31
23
|
value: valueProp,
|
|
@@ -49,12 +41,7 @@ function GroupView({ group, fields, depth, maxDepth, onChange, onRemove, classNa
|
|
|
49
41
|
const t = useNexubMessages();
|
|
50
42
|
const first = fields[0];
|
|
51
43
|
const setRules = (rules) => onChange({ ...group, rules });
|
|
52
|
-
const newRule = () => (
|
|
53
|
-
id: uid(),
|
|
54
|
-
field: first?.id ?? "",
|
|
55
|
-
operator: filterOperators[first?.type ?? "text"][0],
|
|
56
|
-
value: ""
|
|
57
|
-
});
|
|
44
|
+
const newRule = () => newFilterRule(first);
|
|
58
45
|
return /* @__PURE__ */ jsxs(
|
|
59
46
|
"div",
|
|
60
47
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexub/react",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.8.0",
|
|
4
4
|
"description": "React component library for Nexub Design System — 90+ accessible, themeable UI components built on Radix UI primitives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -81,14 +81,14 @@
|
|
|
81
81
|
"date-fns": "^4.4.0",
|
|
82
82
|
"react-day-picker": "^10.0.2",
|
|
83
83
|
"tailwind-merge": "^3.7.0",
|
|
84
|
-
"@nexub/core": "5.
|
|
85
|
-
"@nexub/icons": "5.
|
|
84
|
+
"@nexub/core": "5.8.0",
|
|
85
|
+
"@nexub/icons": "5.8.0"
|
|
86
86
|
},
|
|
87
87
|
"peerDependencies": {
|
|
88
88
|
"react": ">=19 <20",
|
|
89
89
|
"react-dom": ">=19 <20",
|
|
90
90
|
"@tanstack/react-virtual": "^3.14.22",
|
|
91
|
-
"@nexub/tokens": "^5.
|
|
91
|
+
"@nexub/tokens": "^5.8.0"
|
|
92
92
|
},
|
|
93
93
|
"peerDependenciesMeta": {
|
|
94
94
|
"@tanstack/react-virtual": {
|
|
@@ -120,8 +120,8 @@
|
|
|
120
120
|
"vaul": "1.1.2",
|
|
121
121
|
"vite": "^8.3.5",
|
|
122
122
|
"vitest": "^5.0.5",
|
|
123
|
-
"@nexub/themes": "5.
|
|
124
|
-
"@nexub/tokens": "5.
|
|
123
|
+
"@nexub/themes": "5.8.0",
|
|
124
|
+
"@nexub/tokens": "5.8.0"
|
|
125
125
|
},
|
|
126
126
|
"scripts": {
|
|
127
127
|
"build": "rm -rf dist && tsup && tsc -p tsconfig.build.json && rm -f dist/vendor/*.d.ts && node scripts/check-dts.mjs && tailwindcss -i src/styles.css -o dist/styles.css --minify && cp -R ../themes/src/fonts dist/fonts",
|