@planningcenter/tapestry 4.0.0-rc.12 → 4.0.0-rc.14
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/DataTable/DataTable.d.ts +34 -2
- package/dist/components/DataTable/DataTable.d.ts.map +1 -1
- package/dist/components/DataTable/DataTable.js +95 -28
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/DataTable/ResultsCount.d.ts +49 -0
- package/dist/components/DataTable/ResultsCount.d.ts.map +1 -0
- package/dist/components/DataTable/ResultsCount.js +76 -0
- package/dist/components/DataTable/ResultsCount.js.map +1 -0
- package/dist/components/DataTable/TableActions.d.ts +41 -0
- package/dist/components/DataTable/TableActions.d.ts.map +1 -0
- package/dist/components/DataTable/TableActions.js +47 -0
- package/dist/components/DataTable/TableActions.js.map +1 -0
- package/dist/reactRender.css +1013 -1001
- package/dist/reactRender.css.map +1 -1
- package/dist/reactRenderLegacy.css +1013 -1001
- package/dist/reactRenderLegacy.css.map +1 -1
- package/dist/unstable.css +183 -171
- package/dist/unstable.css.map +1 -1
- package/package.json +2 -2
|
@@ -3,6 +3,9 @@ import type { CombineAriaPropsWithCustomProps } from "../../utilities/reactAriaP
|
|
|
3
3
|
import React, { type ReactNode } from "react";
|
|
4
4
|
import { type TableProps as AriaTableProps } from "react-aria-components/Table";
|
|
5
5
|
import type { DataTableColumn } from "./DataTableColumn";
|
|
6
|
+
import { type ResultsNoun } from "./ResultsCount";
|
|
7
|
+
import { type TableAction } from "./TableActions";
|
|
8
|
+
export type { ResultsNoun, TableAction };
|
|
6
9
|
export type Sort = {
|
|
7
10
|
column: string;
|
|
8
11
|
direction: "asc" | "desc";
|
|
@@ -29,6 +32,11 @@ export interface DataTableProps {
|
|
|
29
32
|
* openable in a new tab). Works alongside `onRowClick` and `selectable`.
|
|
30
33
|
*/
|
|
31
34
|
getRowLink?: (row: unknown) => string;
|
|
35
|
+
/**
|
|
36
|
+
* An `id` for the table element. When omitted, a stable generated id is used
|
|
37
|
+
* so the results count can be linked to the table via `aria-describedby`.
|
|
38
|
+
*/
|
|
39
|
+
id?: string;
|
|
32
40
|
/**
|
|
33
41
|
* Async loading state for the table body.
|
|
34
42
|
* - `"loading"`: While `data` is empty, replaces the empty state with a
|
|
@@ -46,6 +54,23 @@ export interface DataTableProps {
|
|
|
46
54
|
onRowClick?: (row: unknown) => void;
|
|
47
55
|
/** Callback invoked when the sort state changes. */
|
|
48
56
|
onSortChange?: (sort: Sort) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Displays the number of results (the length of `data`) above the table,
|
|
59
|
+
* followed by a noun that matches the count. Pass a string to use it as the
|
|
60
|
+
* singular noun and append `"s"` for the plural (e.g. `"result"` →
|
|
61
|
+
* `"results"`), or a `[singular, plural]` tuple to supply an irregular plural
|
|
62
|
+
* explicitly (e.g. `["person", "people"]`). Pass `true` to use `"result"`.
|
|
63
|
+
* When `false` or omitted, no count is rendered. The count is formatted with
|
|
64
|
+
* `Intl.NumberFormat` and the singular form is used only when the count is
|
|
65
|
+
* exactly `1`. When the table is `selectable` and rows are selected, the
|
|
66
|
+
* count instead reflects the selection, e.g. `"2 people selected"`. The count
|
|
67
|
+
* is linked to the table with `aria-describedby` so screen readers announce
|
|
68
|
+
* it as a description of the table. When a string or `[singular, plural]`
|
|
69
|
+
* tuple is provided, the noun also supplies the table's `aria-label` in its
|
|
70
|
+
* plural form (e.g. `"items"` or `"people"`) unless an `aria-label` is passed
|
|
71
|
+
* directly, which always takes precedence; `true` never provides a label.
|
|
72
|
+
*/
|
|
73
|
+
resultsNoun?: ResultsNoun;
|
|
49
74
|
/** Ref to the scroll container the sticky header should track. Takes priority over `scrollContainerSelector`. Only used when `stickyHeader` is true. */
|
|
50
75
|
scrollContainerRef?: React.RefObject<HTMLElement>;
|
|
51
76
|
/** CSS selector for the scroll container the sticky header should track. Only used when `stickyHeader` is true. Defaults to ".content-wrap" */
|
|
@@ -56,6 +81,14 @@ export interface DataTableProps {
|
|
|
56
81
|
sortBy?: Sort;
|
|
57
82
|
/** Enables a sticky header that tracks an outer scroll container. Progressive enhancement: Relies on `animation-timeline: scroll()`, which isn't supported in all browsers (e.g. Firefox) — the header scrolls with the table body instead of sticking in those cases. */
|
|
58
83
|
stickyHeader?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Table actions rendered as an `IconButton` group directly after the results
|
|
86
|
+
* count, above the table. Each entry becomes an icon button whose `callback`
|
|
87
|
+
* receives the ids of the currently selected rows. Unlike the original RFC,
|
|
88
|
+
* these are not gated on `selectable` — actions such as print or export can
|
|
89
|
+
* operate without a row selection.
|
|
90
|
+
*/
|
|
91
|
+
tableActions?: TableAction[];
|
|
59
92
|
}
|
|
60
93
|
type AriaTablePropsToOmit = "children" | "slot";
|
|
61
94
|
type AriaTablePropsToInclude = never;
|
|
@@ -74,8 +107,7 @@ export type DataTableElementProps = CombineAriaPropsWithCustomProps<AriaTablePro
|
|
|
74
107
|
* @component
|
|
75
108
|
*/
|
|
76
109
|
export declare const DataTable: {
|
|
77
|
-
({ className, columns, data, emptyState, errorState, getRowId, getRowLink, loadingState, scrollContainerRef, scrollContainerSelector, selectable, stickyHeader, sortBy, onRowClick, onSortChange, ...restProps }: DataTableElementProps): React.JSX.Element;
|
|
110
|
+
({ className, columns, data, emptyState, errorState, getRowId, getRowLink, id, loadingState, resultsNoun, scrollContainerRef, scrollContainerSelector, selectable, stickyHeader, sortBy, tableActions, onRowClick, onSortChange, ...restProps }: DataTableElementProps): React.JSX.Element;
|
|
78
111
|
displayName: string;
|
|
79
112
|
};
|
|
80
|
-
export {};
|
|
81
113
|
//# sourceMappingURL=DataTable.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DataTable.d.ts","sourceRoot":"","sources":["../../../src/components/DataTable/DataTable.tsx"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAIpB,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,2BAA2B,CAAA;
|
|
1
|
+
{"version":3,"file":"DataTable.d.ts","sourceRoot":"","sources":["../../../src/components/DataTable/DataTable.tsx"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAIpB,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,2BAA2B,CAAA;AAGhF,OAAO,KAAK,EAAE,EACZ,KAAK,SAAS,EAKf,MAAM,OAAO,CAAA;AACd,OAAO,EAWL,KAAK,UAAU,IAAI,cAAc,EAClC,MAAM,6BAA6B,CAAA;AAEpC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAGxD,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,KAAK,WAAW,EAAgB,MAAM,gBAAgB,CAAA;AAE/D,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,CAAA;AAExC,MAAM,MAAM,IAAI,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAA;CAAE,CAAA;AAEhE,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;AAEhE,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,OAAO,EAAE,eAAe,EAAE,CAAA;IAC1B,oCAAoC;IACpC,IAAI,EAAE,OAAO,EAAE,CAAA;IACf,iEAAiE;IACjE,UAAU,CAAC,EAAE,SAAS,CAAA;IACtB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,SAAS,CAAA;IACtB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,GAAG,MAAM,CAAA;IAC5C;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,CAAA;IACrC;;;OAGG;IACH,EAAE,CAAC,EAAE,MAAM,CAAA;IACX;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;IACnC,oDAAoD;IACpD,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;IACnC;;;;;;;;;;;;;;;OAeG;IACH,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,wJAAwJ;IACxJ,kBAAkB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,CAAA;IACjD,+IAA+I;IAC/I,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,6FAA6F;IAC7F,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,IAAI,CAAA;IACb,0QAA0Q;IAC1Q,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,WAAW,EAAE,CAAA;CAC7B;AAID,KAAK,oBAAoB,GAAG,UAAU,GAAG,MAAM,CAAA;AAE/C,KAAK,uBAAuB,GAAG,KAAK,CAAA;AAEpC,MAAM,MAAM,qBAAqB,GAAG,+BAA+B,CACjE,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,uBAAuB,CACxB,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,SAAS;qPAoBnB,qBAAqB;;CAqMvB,CAAA"}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import LoadingSpinner from '../internal/LoadingSpinner.js';
|
|
2
2
|
import Icon from '../../utilities/Icon.js';
|
|
3
|
+
import { useId } from '../../utilities/useId.js';
|
|
3
4
|
import classNames from 'classnames';
|
|
4
|
-
import React__default, { useMemo,
|
|
5
|
+
import React__default, { useMemo, useState, useRef, useLayoutEffect } from 'react';
|
|
5
6
|
import { Table, TableHeader, Column, Checkbox, TableBody, Row, Cell } from 'react-aria-components/Table';
|
|
6
7
|
import { prepareColumn } from './DataTableColumnTypes.js';
|
|
7
8
|
import { getRowKey } from './DataTableRow.js';
|
|
9
|
+
import { hasResultsCount, resolveResultsNounLabel, ResultsCount } from './ResultsCount.js';
|
|
10
|
+
import { TableActions } from './TableActions.js';
|
|
8
11
|
|
|
9
12
|
const DEFAULT_ERROR_STATE = "Something went wrong.";
|
|
10
13
|
/**
|
|
@@ -20,7 +23,7 @@ const DEFAULT_ERROR_STATE = "Something went wrong.";
|
|
|
20
23
|
*
|
|
21
24
|
* @component
|
|
22
25
|
*/
|
|
23
|
-
const DataTable = ({ className, columns, data, emptyState, errorState, getRowId, getRowLink, loadingState = "idle", scrollContainerRef, scrollContainerSelector = ".content-wrap", selectable, stickyHeader, sortBy, onRowClick, onSortChange, ...restProps }) => {
|
|
26
|
+
const DataTable = ({ className, columns, data, emptyState, errorState, getRowId, getRowLink, id, loadingState = "idle", resultsNoun, scrollContainerRef, scrollContainerSelector = ".content-wrap", selectable, stickyHeader, sortBy, tableActions, onRowClick, onSortChange, ...restProps }) => {
|
|
24
27
|
const rowsAreInteractive = Boolean(onRowClick || getRowLink);
|
|
25
28
|
const combinedClassName = classNames("tds-data-table", className);
|
|
26
29
|
const sortDescriptor = useSortDescriptor(sortBy);
|
|
@@ -43,33 +46,65 @@ const DataTable = ({ className, columns, data, emptyState, errorState, getRowId,
|
|
|
43
46
|
const preparedColumns = useMemo(() => columns.map(prepareColumn),
|
|
44
47
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
45
48
|
[columnKey]);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
49
|
+
// Selection is owned internally — the component tracks its own selected row
|
|
50
|
+
// keys rather than exposing `selectedKeys`/`onSelectedChange`. The results
|
|
51
|
+
// count and the React Aria `<Table>` both read from this single source of
|
|
52
|
+
// truth.
|
|
53
|
+
const [selectedKeys, setSelectedKeys] = useState([]);
|
|
54
|
+
const selectableProps = useSelectableProps({
|
|
55
|
+
data,
|
|
56
|
+
getRowId,
|
|
57
|
+
onSelectedChange: setSelectedKeys,
|
|
58
|
+
selectable,
|
|
59
|
+
selectedKeys,
|
|
60
|
+
});
|
|
61
|
+
// Associate the visible results count with the table so screen readers
|
|
62
|
+
// announce it as a description of the table. `Table` doesn't support a real
|
|
63
|
+
// `<caption>` (its children go through a collection builder), so we link the
|
|
64
|
+
// count via `aria-describedby`, merging with any caller-supplied value.
|
|
65
|
+
const stableId = useId();
|
|
66
|
+
const tableId = id || `tds-data-table-${stableId}`;
|
|
67
|
+
const resultsCountId = `${tableId}-results-count`;
|
|
68
|
+
const describedBy = hasResultsCount(resultsNoun)
|
|
69
|
+
? [restProps["aria-describedby"], resultsCountId].filter(Boolean).join(" ")
|
|
70
|
+
: restProps["aria-describedby"];
|
|
71
|
+
// A caller-supplied `aria-label` always wins; otherwise the results noun
|
|
72
|
+
// provides the table's accessible name so a label is present whenever a noun
|
|
73
|
+
// is given. Array nouns are transformed to their plural form.
|
|
74
|
+
const ariaLabel = restProps["aria-label"] || resolveResultsNounLabel(resultsNoun);
|
|
75
|
+
const hasActions = (tableActions?.length ?? 0) > 0;
|
|
76
|
+
const showHeader = Boolean(resultsNoun || hasActions);
|
|
77
|
+
return (React__default.createElement(React__default.Fragment, null,
|
|
78
|
+
showHeader && (React__default.createElement("div", { className: "tds-data-table-header-actions" },
|
|
79
|
+
React__default.createElement(ResultsCount, { id: resultsCountId, resultsNoun: resultsNoun, selectable: selectable, selectedCount: selectedKeys.length, totalCount: data.length }),
|
|
80
|
+
hasActions && (React__default.createElement(TableActions, { actions: tableActions, resultsNoun: resultsNoun, selectedKeys: selectedKeys })))),
|
|
81
|
+
React__default.createElement("div", { ref: wrapperRef, className: classNames("tds-data-table-wrapper", {
|
|
82
|
+
"tds-data-table-wrapper--sticky-header": hasScrollContainer,
|
|
83
|
+
}) },
|
|
84
|
+
React__default.createElement("div", { className: "tds-data-table-scroll" },
|
|
85
|
+
React__default.createElement(Table, { ...restProps, ...selectableProps, ...(ariaLabel ? { "aria-label": ariaLabel } : {}), ...(describedBy ? { "aria-describedby": describedBy } : {}), ...{ id: tableId }, className: combinedClassName, sortDescriptor: sortDescriptor, onSortChange: (sort) => {
|
|
86
|
+
onSortChange?.(serializeSort(sort));
|
|
87
|
+
} },
|
|
88
|
+
React__default.createElement(TableHeader, { className: "tds-data-table-header" },
|
|
89
|
+
selectable && (React__default.createElement(Column, { className: "tds-data-table-column tds-data-table-column--align-center" },
|
|
67
90
|
React__default.createElement(Checkbox, { slot: "selection", className: "tds-data-table-selectable-checkbox" }))),
|
|
68
|
-
preparedColumns.map((
|
|
69
|
-
[`tds-data-table-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
91
|
+
preparedColumns.map(({ align, column }, index) => (React__default.createElement(Column, { key: column.name, id: column.name, isRowHeader: index === 0, className: classNames("tds-data-table-column", {
|
|
92
|
+
[`tds-data-table-column--align-${align}`]: align !== "start",
|
|
93
|
+
}), allowsSorting: column.sortable },
|
|
94
|
+
column.hideLabel ? (React__default.createElement("span", { className: "tds-data-table-column-label--hidden" }, column.headerLabel)) : (column.headerLabel),
|
|
95
|
+
column.sortable && (React__default.createElement(Icon, { className: "tds-data-table-column-icon tds-data-table-column-icon--sort", symbol: "general#up-caret", "aria-hidden": true })))))),
|
|
96
|
+
React__default.createElement(TableBody, { className: "tds-data-table-body", renderEmptyState: renderEmptyState }, data.map((row, rowIndex) => {
|
|
97
|
+
const rowKey = getRowKey(row, rowIndex, getRowId);
|
|
98
|
+
return (React__default.createElement(Row, { key: rowKey, id: rowKey, href: getRowLink?.(row), onAction: onRowClick ? () => onRowClick(row) : undefined, className: classNames("tds-data-table-row", {
|
|
99
|
+
"tds-data-table-row--interactive": rowsAreInteractive,
|
|
100
|
+
}) },
|
|
101
|
+
selectable && (React__default.createElement(Cell, { className: "tds-data-table-cell tds-data-table-cell--align-center" },
|
|
102
|
+
React__default.createElement(Checkbox, { slot: "selection", className: "tds-data-table-selectable-checkbox" }))),
|
|
103
|
+
preparedColumns.map((prepared) => (React__default.createElement(Cell, { key: prepared.column.name, "data-pii": prepared.column.pii || undefined, className: classNames("tds-data-table-cell", {
|
|
104
|
+
[`tds-data-table-cell--align-${prepared.align}`]: prepared.align !== "start",
|
|
105
|
+
"tds-data-table-cell--numeric": prepared.isNumeric,
|
|
106
|
+
}) }, prepared.renderCell(row))))));
|
|
107
|
+
})))))));
|
|
73
108
|
};
|
|
74
109
|
DataTable.displayName = "DataTable";
|
|
75
110
|
/**
|
|
@@ -107,6 +142,38 @@ function serializeSort(sort) {
|
|
|
107
142
|
return { column: sort.column, direction: "desc" };
|
|
108
143
|
return { column: sort.column, direction: "asc" };
|
|
109
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Resolves the `<Table>` selection props, driving React Aria's `<Table>` off
|
|
147
|
+
* the component's internal selection state. `onSelectedChange` (the internal
|
|
148
|
+
* state setter) always receives a plain array of keys: React Aria's `"all"`
|
|
149
|
+
* selection sentinel (used for infinite-loading collections, which this
|
|
150
|
+
* component doesn't have) is resolved to the concrete keys of the currently
|
|
151
|
+
* loaded rows.
|
|
152
|
+
*/
|
|
153
|
+
function useSelectableProps({ data, getRowId, onSelectedChange, selectable, selectedKeys, }) {
|
|
154
|
+
const selectedKeysSet = useMemo(() => new Set(selectedKeys), [selectedKeys]);
|
|
155
|
+
const { rowKeys, rowOrder } = useMemo(() => {
|
|
156
|
+
const rowKeys = data.map((row, index) => getRowKey(row, index, getRowId));
|
|
157
|
+
return {
|
|
158
|
+
rowKeys,
|
|
159
|
+
rowOrder: new Map(rowKeys.map((key, index) => [key, index])),
|
|
160
|
+
};
|
|
161
|
+
}, [data, getRowId]);
|
|
162
|
+
if (!selectable)
|
|
163
|
+
return { selectionMode: "none" };
|
|
164
|
+
return {
|
|
165
|
+
onSelectionChange: (keys) => {
|
|
166
|
+
// Keys not found in `rowOrder` (e.g. a previously selected row no longer
|
|
167
|
+
// in `data`) sort to the end rather than being dropped.
|
|
168
|
+
const resolvedKeys = keys === "all"
|
|
169
|
+
? [...rowKeys]
|
|
170
|
+
: Array.from(keys).sort((a, b) => (rowOrder.get(a) ?? Infinity) - (rowOrder.get(b) ?? Infinity));
|
|
171
|
+
onSelectedChange(resolvedKeys);
|
|
172
|
+
},
|
|
173
|
+
selectedKeys: selectedKeysSet,
|
|
174
|
+
selectionMode: "multiple",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
110
177
|
function useStickyHeader(stickyHeader, scrollContainerRef, scrollContainerSelector) {
|
|
111
178
|
const wrapperRef = useRef(null);
|
|
112
179
|
const [hasScrollContainer, setHasScrollContainer] = useState(false);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DataTable.js","sources":["../../../src/components/DataTable/DataTable.tsx"],"sourcesContent":["import \"./index.css\"\n\nimport { LoadingSpinner } from \"@components/internal\"\nimport Icon from \"@utilities/Icon\"\nimport type { CombineAriaPropsWithCustomProps } from \"@utilities/reactAriaProps\"\nimport classNames from \"classnames\"\nimport React, {\n type ReactNode,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\"\nimport {\n Cell,\n Checkbox,\n Column,\n Row,\n type SortDescriptor,\n Table,\n TableBody,\n TableHeader,\n type TableProps as AriaTableProps,\n} from \"react-aria-components/Table\"\n\nimport type { DataTableColumn } from \"./DataTableColumn\"\nimport { prepareColumn, type PreparedColumn } from \"./DataTableColumnTypes\"\nimport { getRowKey } from \"./DataTableRow\"\n\nexport type Sort = { column: string; direction: \"asc\" | \"desc\" }\n\nexport type DataTableLoadingState = \"error\" | \"idle\" | \"loading\"\n\nexport interface DataTableProps {\n /**\n * Columns define what data is displayed and how. Pass a stable reference\n * (e.g. a module constant or memoized value); columns are prepared per array\n * identity, so an inline array rebuilds each column's formatter every render.\n */\n columns: DataTableColumn[]\n /** The array of rows to display. */\n data: unknown[]\n /** Rendered in place of the table body when there is no data. */\n emptyState?: ReactNode\n /** Rendered in place of the table body when `loadingState` is `\"error\"`. */\n errorState?: ReactNode\n /** Returns a stable identity for each row. Defaults to `row.id`. */\n getRowId?: (row: unknown) => number | string\n /**\n * Returns a URL to navigate to when the row is clicked. Rows with a link\n * render as navigable elements (clickable, keyboard-focusable, and\n * openable in a new tab). Works alongside `onRowClick` and `selectable`.\n */\n getRowLink?: (row: unknown) => string\n /**\n * Async loading state for the table body.\n * - `\"loading\"`: While `data` is empty, replaces the empty state with a\n * centered spinner.\n * - `\"error\"`: While `data` is empty, replaces the empty state with\n * `errorState` (or a default error message).\n * - `\"idle\"` (default): Normal rendering.\n */\n loadingState?: DataTableLoadingState\n /**\n * Called with the row when it is clicked. Rows with a click handler render\n * as interactive elements (clickable and keyboard-focusable). When\n * `getRowLink` is also set, both run and the row navigates.\n */\n onRowClick?: (row: unknown) => void\n /** Callback invoked when the sort state changes. */\n onSortChange?: (sort: Sort) => void\n /** Ref to the scroll container the sticky header should track. Takes priority over `scrollContainerSelector`. Only used when `stickyHeader` is true. */\n scrollContainerRef?: React.RefObject<HTMLElement>\n /** CSS selector for the scroll container the sticky header should track. Only used when `stickyHeader` is true. Defaults to \".content-wrap\" */\n scrollContainerSelector?: string\n /** Enables row selection via checkboxes, including a \"select all\" checkbox in the header. */\n selectable?: boolean\n /** The current sort state for the table. */\n sortBy?: Sort\n /** Enables a sticky header that tracks an outer scroll container. Progressive enhancement: Relies on `animation-timeline: scroll()`, which isn't supported in all browsers (e.g. Firefox) — the header scrolls with the table body instead of sticking in those cases. */\n stickyHeader?: boolean\n}\n\nconst DEFAULT_ERROR_STATE = \"Something went wrong.\"\n\ntype AriaTablePropsToOmit = \"children\" | \"slot\"\n\ntype AriaTablePropsToInclude = never\n\nexport type DataTableElementProps = CombineAriaPropsWithCustomProps<\n AriaTableProps,\n DataTableProps,\n AriaTablePropsToOmit,\n AriaTablePropsToInclude\n>\n\n/**\n * A table for displaying tabular data. Built on React Aria's `Table`.\n *\n * Each column declares a `type` that controls how its cell values are\n * formatted: `string`, `number`, `currency`, `date`, `datetime`, `boolean`,\n * `enum`, and `custom`. A column's `render` function always takes precedence\n * over the built-in type formatting.\n *\n * Sorting, filtering, pagination, and other RFC features are not yet\n * implemented.\n *\n * @component\n */\nexport const DataTable = ({\n className,\n columns,\n data,\n emptyState,\n errorState,\n getRowId,\n getRowLink,\n loadingState = \"idle\",\n scrollContainerRef,\n scrollContainerSelector = \".content-wrap\",\n selectable,\n stickyHeader,\n sortBy,\n onRowClick,\n onSortChange,\n ...restProps\n}: DataTableElementProps) => {\n const rowsAreInteractive = Boolean(onRowClick || getRowLink)\n const combinedClassName = classNames(\"tds-data-table\", className)\n const sortDescriptor = useSortDescriptor(sortBy)\n const { hasScrollContainer, wrapperRef } = useStickyHeader(\n stickyHeader,\n scrollContainerRef,\n scrollContainerSelector\n )\n\n // React Aria only invokes `renderEmptyState` when the collection is empty, so\n // the loading spinner and error message only take over the body while `data`\n // is empty; once rows exist they always render.\n const emptyStateContent = getEmptyStateContent({\n emptyState,\n errorState,\n loadingState,\n })\n const renderEmptyState = emptyStateContent\n ? () => emptyStateContent\n : undefined\n\n // Keyed on column names rather than the `columns` array identity so a fresh\n // `columns` literal each render doesn't rebuild every formatter. `columns` is\n // intentionally omitted from the deps.\n const columnKey = columns.map((c) => `${c.name}/${c.type}`).join(\"|\")\n const preparedColumns = useMemo<PreparedColumn[]>(\n () => columns.map(prepareColumn),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [columnKey]\n )\n\n return (\n <div\n ref={wrapperRef}\n className={classNames(\"tds-data-table-wrapper\", {\n \"tds-data-table-wrapper--sticky-header\": hasScrollContainer,\n })}\n >\n <div className=\"tds-data-table-scroll\">\n <Table\n {...restProps}\n selectionMode={selectable ? \"multiple\" : \"none\"}\n className={combinedClassName}\n sortDescriptor={sortDescriptor}\n onSortChange={(sort) => {\n onSortChange?.(serializeSort(sort))\n }}\n >\n <TableHeader className=\"tds-data-table-header\">\n {/*\n Rendered as a sibling before the `preparedColumns.map` below\n (not folded into it) so the map's `index` still starts at 0 for\n the first *data* column — that's what keeps\n `isRowHeader={index === 0}` pointing at the first data column\n instead of this selection column.\n */}\n {selectable && (\n <Column className=\"tds-data-table-column tds-data-table-column--align-center\">\n <Checkbox\n slot=\"selection\"\n className=\"tds-data-table-selectable-checkbox\"\n />\n </Column>\n )}\n {preparedColumns.map(({ align, column }, index) => (\n <Column\n key={column.name}\n id={column.name}\n isRowHeader={index === 0}\n className={classNames(\"tds-data-table-column\", {\n [`tds-data-table-column--align-${align}`]: align !== \"start\",\n })}\n allowsSorting={column.sortable}\n >\n {column.hideLabel ? (\n <span className=\"tds-data-table-column-label--hidden\">\n {column.headerLabel}\n </span>\n ) : (\n column.headerLabel\n )}\n {column.sortable && (\n <Icon\n className=\"tds-data-table-column-icon tds-data-table-column-icon--sort\"\n symbol=\"general#up-caret\"\n aria-hidden={true}\n />\n )}\n </Column>\n ))}\n </TableHeader>\n <TableBody\n className=\"tds-data-table-body\"\n renderEmptyState={renderEmptyState}\n >\n {data.map((row, rowIndex) => {\n const rowKey = getRowKey(row, rowIndex, getRowId)\n return (\n <Row\n key={rowKey}\n id={rowKey}\n href={getRowLink?.(row)}\n onAction={onRowClick ? () => onRowClick(row) : undefined}\n className={classNames(\"tds-data-table-row\", {\n \"tds-data-table-row--interactive\": rowsAreInteractive,\n })}\n >\n {selectable && (\n <Cell className=\"tds-data-table-cell tds-data-table-cell--align-center\">\n <Checkbox\n slot=\"selection\"\n className=\"tds-data-table-selectable-checkbox\"\n />\n </Cell>\n )}\n {preparedColumns.map((prepared) => (\n <Cell\n key={prepared.column.name}\n data-pii={prepared.column.pii || undefined}\n className={classNames(\"tds-data-table-cell\", {\n [`tds-data-table-cell--align-${prepared.align}`]:\n prepared.align !== \"start\",\n \"tds-data-table-cell--numeric\": prepared.isNumeric,\n })}\n >\n {prepared.renderCell(row)}\n </Cell>\n ))}\n </Row>\n )\n })}\n </TableBody>\n </Table>\n </div>\n </div>\n )\n}\n\nDataTable.displayName = \"DataTable\"\n\n/**\n * Resolves the content rendered in the table body's empty-state slot based on\n * `loadingState`, falling back to `emptyState`. Returns `null` when nothing\n * should render (idle with no `emptyState`).\n */\nfunction getEmptyStateContent({\n emptyState,\n errorState,\n loadingState,\n}: {\n emptyState: ReactNode\n errorState: ReactNode\n loadingState: DataTableLoadingState\n}): ReactNode {\n if (loadingState === \"loading\") {\n return (\n <div className=\"tds-data-table-loading\" role=\"status\">\n <span className=\"tds-data-table-cell-label--hidden\">Loading</span>\n <span className=\"tds-data-table-loading-spinner\" aria-hidden=\"true\">\n <LoadingSpinner />\n </span>\n </div>\n )\n }\n\n // Error and empty share the same container; they differ only in content.\n const content =\n loadingState === \"error\" ? (errorState ?? DEFAULT_ERROR_STATE) : emptyState\n if (content == null) return null\n\n return <div className=\"tds-data-table-empty\">{content}</div>\n}\n\nfunction useSortDescriptor(\n sortBy: Sort | undefined\n): SortDescriptor | undefined {\n return useMemo(() => {\n if (!sortBy) return undefined\n return deserializeSort(sortBy)\n }, [sortBy])\n}\n\nfunction deserializeSort(sort: Sort): SortDescriptor {\n if (sort.direction === \"desc\")\n return { column: sort.column, direction: \"descending\" }\n return { column: sort.column, direction: \"ascending\" }\n}\n\nfunction serializeSort(sort: SortDescriptor): Sort {\n if (sort.direction === \"descending\")\n return { column: sort.column as string, direction: \"desc\" }\n return { column: sort.column as string, direction: \"asc\" }\n}\n\nfunction useStickyHeader(\n stickyHeader: boolean | undefined,\n scrollContainerRef: React.RefObject<HTMLElement> | undefined,\n scrollContainerSelector: string | undefined\n) {\n const wrapperRef = useRef<HTMLDivElement>(null)\n const [hasScrollContainer, setHasScrollContainer] = useState(false)\n\n useLayoutEffect(() => {\n setHasScrollContainer(false)\n if (!stickyHeader) return\n if (typeof ResizeObserver === \"undefined\") return\n const wrapper = wrapperRef.current\n if (!wrapper) return\n\n const scrollContainer = getScrollContainer(\n scrollContainerRef,\n scrollContainerSelector,\n wrapper\n )\n if (!scrollContainer) return\n\n setHasScrollContainer(true)\n\n let thead: HTMLElement | null = null\n\n const resizeObserver = new ResizeObserver(() => {\n if (!thead) {\n thead = wrapper.querySelector<HTMLElement>(\".tds-data-table-header\")\n if (!thead) return\n scrollContainer.style.setProperty(\n \"scroll-timeline-name\",\n \"--_sticky-header-scroll\"\n )\n }\n\n const { top: wrapperTop, bottom: wrapperBottom } =\n wrapper.getBoundingClientRect()\n const { top: scrollContainerTop } =\n scrollContainer.getBoundingClientRect()\n // Derive headerTop from the untransformed wrapper to avoid reading the\n // scroll-driven translateY that getBoundingClientRect() includes on thead.\n const headerTop = wrapperTop + wrapper.clientTop\n const start = scrollContainer.scrollTop + (headerTop - scrollContainerTop)\n const max = Math.max(0, wrapperBottom - headerTop - thead.offsetHeight)\n wrapper.style.setProperty(\"--_sticky-header-scroll-start\", `${start}px`)\n wrapper.style.setProperty(\"--_sticky-header-scroll-max\", `${max}px`)\n })\n\n resizeObserver.observe(wrapper)\n resizeObserver.observe(scrollContainer)\n\n return () => {\n resizeObserver.disconnect()\n wrapper.style.removeProperty(\"--_sticky-header-scroll-start\")\n wrapper.style.removeProperty(\"--_sticky-header-scroll-max\")\n }\n }, [stickyHeader, scrollContainerRef, scrollContainerSelector])\n\n return { hasScrollContainer, wrapperRef }\n}\n\nfunction getScrollContainer(\n scrollContainerRef: React.RefObject<HTMLElement> | undefined,\n scrollContainerSelector: string | undefined,\n wrapper: HTMLElement\n): HTMLElement | undefined {\n const element =\n scrollContainerRef?.current ??\n getScrollContainerFromSelector(scrollContainerSelector, wrapper)\n if (element instanceof Window) return\n return element\n}\n\nfunction getScrollContainerFromSelector(\n scrollContainerSelector: string | undefined,\n wrapper: HTMLElement\n): HTMLElement | undefined {\n return scrollContainerSelector\n ? (wrapper.parentElement?.closest<HTMLElement>(scrollContainerSelector) ??\n undefined)\n : undefined\n}\n"],"names":["React"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DataTable.js","sources":["../../../src/components/DataTable/DataTable.tsx"],"sourcesContent":["import \"./index.css\"\n\nimport { LoadingSpinner } from \"@components/internal\"\nimport Icon from \"@utilities/Icon\"\nimport type { CombineAriaPropsWithCustomProps } from \"@utilities/reactAriaProps\"\nimport { useId } from \"@utilities/useId\"\nimport classNames from \"classnames\"\nimport React, {\n type ReactNode,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\"\nimport {\n Cell,\n Checkbox,\n Column,\n type Key,\n Row,\n type Selection,\n type SortDescriptor,\n Table,\n TableBody,\n TableHeader,\n type TableProps as AriaTableProps,\n} from \"react-aria-components/Table\"\n\nimport type { DataTableColumn } from \"./DataTableColumn\"\nimport { prepareColumn, type PreparedColumn } from \"./DataTableColumnTypes\"\nimport { getRowKey } from \"./DataTableRow\"\nimport {\n hasResultsCount,\n resolveResultsNounLabel,\n ResultsCount,\n type ResultsNoun,\n} from \"./ResultsCount\"\nimport { type TableAction, TableActions } from \"./TableActions\"\n\nexport type { ResultsNoun, TableAction }\n\nexport type Sort = { column: string; direction: \"asc\" | \"desc\" }\n\nexport type DataTableLoadingState = \"error\" | \"idle\" | \"loading\"\n\nexport interface DataTableProps {\n /**\n * Columns define what data is displayed and how. Pass a stable reference\n * (e.g. a module constant or memoized value); columns are prepared per array\n * identity, so an inline array rebuilds each column's formatter every render.\n */\n columns: DataTableColumn[]\n /** The array of rows to display. */\n data: unknown[]\n /** Rendered in place of the table body when there is no data. */\n emptyState?: ReactNode\n /** Rendered in place of the table body when `loadingState` is `\"error\"`. */\n errorState?: ReactNode\n /** Returns a stable identity for each row. Defaults to `row.id`. */\n getRowId?: (row: unknown) => number | string\n /**\n * Returns a URL to navigate to when the row is clicked. Rows with a link\n * render as navigable elements (clickable, keyboard-focusable, and\n * openable in a new tab). Works alongside `onRowClick` and `selectable`.\n */\n getRowLink?: (row: unknown) => string\n /**\n * An `id` for the table element. When omitted, a stable generated id is used\n * so the results count can be linked to the table via `aria-describedby`.\n */\n id?: string\n /**\n * Async loading state for the table body.\n * - `\"loading\"`: While `data` is empty, replaces the empty state with a\n * centered spinner.\n * - `\"error\"`: While `data` is empty, replaces the empty state with\n * `errorState` (or a default error message).\n * - `\"idle\"` (default): Normal rendering.\n */\n loadingState?: DataTableLoadingState\n /**\n * Called with the row when it is clicked. Rows with a click handler render\n * as interactive elements (clickable and keyboard-focusable). When\n * `getRowLink` is also set, both run and the row navigates.\n */\n onRowClick?: (row: unknown) => void\n /** Callback invoked when the sort state changes. */\n onSortChange?: (sort: Sort) => void\n /**\n * Displays the number of results (the length of `data`) above the table,\n * followed by a noun that matches the count. Pass a string to use it as the\n * singular noun and append `\"s\"` for the plural (e.g. `\"result\"` →\n * `\"results\"`), or a `[singular, plural]` tuple to supply an irregular plural\n * explicitly (e.g. `[\"person\", \"people\"]`). Pass `true` to use `\"result\"`.\n * When `false` or omitted, no count is rendered. The count is formatted with\n * `Intl.NumberFormat` and the singular form is used only when the count is\n * exactly `1`. When the table is `selectable` and rows are selected, the\n * count instead reflects the selection, e.g. `\"2 people selected\"`. The count\n * is linked to the table with `aria-describedby` so screen readers announce\n * it as a description of the table. When a string or `[singular, plural]`\n * tuple is provided, the noun also supplies the table's `aria-label` in its\n * plural form (e.g. `\"items\"` or `\"people\"`) unless an `aria-label` is passed\n * directly, which always takes precedence; `true` never provides a label.\n */\n resultsNoun?: ResultsNoun\n /** Ref to the scroll container the sticky header should track. Takes priority over `scrollContainerSelector`. Only used when `stickyHeader` is true. */\n scrollContainerRef?: React.RefObject<HTMLElement>\n /** CSS selector for the scroll container the sticky header should track. Only used when `stickyHeader` is true. Defaults to \".content-wrap\" */\n scrollContainerSelector?: string\n /** Enables row selection via checkboxes, including a \"select all\" checkbox in the header. */\n selectable?: boolean\n /** The current sort state for the table. */\n sortBy?: Sort\n /** Enables a sticky header that tracks an outer scroll container. Progressive enhancement: Relies on `animation-timeline: scroll()`, which isn't supported in all browsers (e.g. Firefox) — the header scrolls with the table body instead of sticking in those cases. */\n stickyHeader?: boolean\n /**\n * Table actions rendered as an `IconButton` group directly after the results\n * count, above the table. Each entry becomes an icon button whose `callback`\n * receives the ids of the currently selected rows. Unlike the original RFC,\n * these are not gated on `selectable` — actions such as print or export can\n * operate without a row selection.\n */\n tableActions?: TableAction[]\n}\n\nconst DEFAULT_ERROR_STATE = \"Something went wrong.\"\n\ntype AriaTablePropsToOmit = \"children\" | \"slot\"\n\ntype AriaTablePropsToInclude = never\n\nexport type DataTableElementProps = CombineAriaPropsWithCustomProps<\n AriaTableProps,\n DataTableProps,\n AriaTablePropsToOmit,\n AriaTablePropsToInclude\n>\n\n/**\n * A table for displaying tabular data. Built on React Aria's `Table`.\n *\n * Each column declares a `type` that controls how its cell values are\n * formatted: `string`, `number`, `currency`, `date`, `datetime`, `boolean`,\n * `enum`, and `custom`. A column's `render` function always takes precedence\n * over the built-in type formatting.\n *\n * Sorting, filtering, pagination, and other RFC features are not yet\n * implemented.\n *\n * @component\n */\nexport const DataTable = ({\n className,\n columns,\n data,\n emptyState,\n errorState,\n getRowId,\n getRowLink,\n id,\n loadingState = \"idle\",\n resultsNoun,\n scrollContainerRef,\n scrollContainerSelector = \".content-wrap\",\n selectable,\n stickyHeader,\n sortBy,\n tableActions,\n onRowClick,\n onSortChange,\n ...restProps\n}: DataTableElementProps) => {\n const rowsAreInteractive = Boolean(onRowClick || getRowLink)\n const combinedClassName = classNames(\"tds-data-table\", className)\n const sortDescriptor = useSortDescriptor(sortBy)\n const { hasScrollContainer, wrapperRef } = useStickyHeader(\n stickyHeader,\n scrollContainerRef,\n scrollContainerSelector\n )\n\n // React Aria only invokes `renderEmptyState` when the collection is empty, so\n // the loading spinner and error message only take over the body while `data`\n // is empty; once rows exist they always render.\n const emptyStateContent = getEmptyStateContent({\n emptyState,\n errorState,\n loadingState,\n })\n const renderEmptyState = emptyStateContent\n ? () => emptyStateContent\n : undefined\n\n // Keyed on column names rather than the `columns` array identity so a fresh\n // `columns` literal each render doesn't rebuild every formatter. `columns` is\n // intentionally omitted from the deps.\n const columnKey = columns.map((c) => `${c.name}/${c.type}`).join(\"|\")\n const preparedColumns = useMemo<PreparedColumn[]>(\n () => columns.map(prepareColumn),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [columnKey]\n )\n\n // Selection is owned internally — the component tracks its own selected row\n // keys rather than exposing `selectedKeys`/`onSelectedChange`. The results\n // count and the React Aria `<Table>` both read from this single source of\n // truth.\n const [selectedKeys, setSelectedKeys] = useState<(number | string)[]>([])\n const selectableProps = useSelectableProps({\n data,\n getRowId,\n onSelectedChange: setSelectedKeys,\n selectable,\n selectedKeys,\n })\n\n // Associate the visible results count with the table so screen readers\n // announce it as a description of the table. `Table` doesn't support a real\n // `<caption>` (its children go through a collection builder), so we link the\n // count via `aria-describedby`, merging with any caller-supplied value.\n const stableId = useId()\n const tableId = id || `tds-data-table-${stableId}`\n const resultsCountId = `${tableId}-results-count`\n const describedBy = hasResultsCount(resultsNoun)\n ? [restProps[\"aria-describedby\"], resultsCountId].filter(Boolean).join(\" \")\n : restProps[\"aria-describedby\"]\n\n // A caller-supplied `aria-label` always wins; otherwise the results noun\n // provides the table's accessible name so a label is present whenever a noun\n // is given. Array nouns are transformed to their plural form.\n const ariaLabel =\n restProps[\"aria-label\"] || resolveResultsNounLabel(resultsNoun)\n\n const hasActions = (tableActions?.length ?? 0) > 0\n const showHeader = Boolean(resultsNoun || hasActions)\n\n return (\n <>\n {showHeader && (\n <div className=\"tds-data-table-header-actions\">\n <ResultsCount\n id={resultsCountId}\n resultsNoun={resultsNoun}\n selectable={selectable}\n selectedCount={selectedKeys.length}\n totalCount={data.length}\n />\n {hasActions && (\n <TableActions\n actions={tableActions!} // Safe to assert non-null here because of `hasActions`\n resultsNoun={resultsNoun}\n selectedKeys={selectedKeys}\n />\n )}\n </div>\n )}\n <div\n ref={wrapperRef}\n className={classNames(\"tds-data-table-wrapper\", {\n \"tds-data-table-wrapper--sticky-header\": hasScrollContainer,\n })}\n >\n <div className=\"tds-data-table-scroll\">\n <Table\n {...restProps}\n {...selectableProps}\n {...(ariaLabel ? { \"aria-label\": ariaLabel } : {})}\n {...(describedBy ? { \"aria-describedby\": describedBy } : {})}\n // RAC's `Table` forwards `id` to the underlying table element at\n // runtime (via `filterDOMProps({ global: true })`) but omits `id`\n // from its prop types, so spread it through a cast.\n {...({ id: tableId } as Partial<AriaTableProps>)}\n className={combinedClassName}\n sortDescriptor={sortDescriptor}\n onSortChange={(sort) => {\n onSortChange?.(serializeSort(sort))\n }}\n >\n <TableHeader className=\"tds-data-table-header\">\n {/*\n Rendered as a sibling before the `preparedColumns.map` below\n (not folded into it) so the map's `index` still starts at 0 for\n the first *data* column — that's what keeps\n `isRowHeader={index === 0}` pointing at the first data column\n instead of this selection column.\n */}\n {selectable && (\n <Column className=\"tds-data-table-column tds-data-table-column--align-center\">\n <Checkbox\n slot=\"selection\"\n className=\"tds-data-table-selectable-checkbox\"\n />\n </Column>\n )}\n {preparedColumns.map(({ align, column }, index) => (\n <Column\n key={column.name}\n id={column.name}\n isRowHeader={index === 0}\n className={classNames(\"tds-data-table-column\", {\n [`tds-data-table-column--align-${align}`]:\n align !== \"start\",\n })}\n allowsSorting={column.sortable}\n >\n {column.hideLabel ? (\n <span className=\"tds-data-table-column-label--hidden\">\n {column.headerLabel}\n </span>\n ) : (\n column.headerLabel\n )}\n {column.sortable && (\n <Icon\n className=\"tds-data-table-column-icon tds-data-table-column-icon--sort\"\n symbol=\"general#up-caret\"\n aria-hidden={true}\n />\n )}\n </Column>\n ))}\n </TableHeader>\n <TableBody\n className=\"tds-data-table-body\"\n renderEmptyState={renderEmptyState}\n >\n {data.map((row, rowIndex) => {\n const rowKey = getRowKey(row, rowIndex, getRowId)\n return (\n <Row\n key={rowKey}\n id={rowKey}\n href={getRowLink?.(row)}\n onAction={onRowClick ? () => onRowClick(row) : undefined}\n className={classNames(\"tds-data-table-row\", {\n \"tds-data-table-row--interactive\": rowsAreInteractive,\n })}\n >\n {selectable && (\n <Cell className=\"tds-data-table-cell tds-data-table-cell--align-center\">\n <Checkbox\n slot=\"selection\"\n className=\"tds-data-table-selectable-checkbox\"\n />\n </Cell>\n )}\n {preparedColumns.map((prepared) => (\n <Cell\n key={prepared.column.name}\n data-pii={prepared.column.pii || undefined}\n className={classNames(\"tds-data-table-cell\", {\n [`tds-data-table-cell--align-${prepared.align}`]:\n prepared.align !== \"start\",\n \"tds-data-table-cell--numeric\": prepared.isNumeric,\n })}\n >\n {prepared.renderCell(row)}\n </Cell>\n ))}\n </Row>\n )\n })}\n </TableBody>\n </Table>\n </div>\n </div>\n </>\n )\n}\n\nDataTable.displayName = \"DataTable\"\n\n/**\n * Resolves the content rendered in the table body's empty-state slot based on\n * `loadingState`, falling back to `emptyState`. Returns `null` when nothing\n * should render (idle with no `emptyState`).\n */\nfunction getEmptyStateContent({\n emptyState,\n errorState,\n loadingState,\n}: {\n emptyState: ReactNode\n errorState: ReactNode\n loadingState: DataTableLoadingState\n}): ReactNode {\n if (loadingState === \"loading\") {\n return (\n <div className=\"tds-data-table-loading\" role=\"status\">\n <span className=\"tds-data-table-cell-label--hidden\">Loading</span>\n <span className=\"tds-data-table-loading-spinner\" aria-hidden=\"true\">\n <LoadingSpinner />\n </span>\n </div>\n )\n }\n\n // Error and empty share the same container; they differ only in content.\n const content =\n loadingState === \"error\" ? (errorState ?? DEFAULT_ERROR_STATE) : emptyState\n if (content == null) return null\n\n return <div className=\"tds-data-table-empty\">{content}</div>\n}\n\nfunction useSortDescriptor(\n sortBy: Sort | undefined\n): SortDescriptor | undefined {\n return useMemo(() => {\n if (!sortBy) return undefined\n return deserializeSort(sortBy)\n }, [sortBy])\n}\n\nfunction deserializeSort(sort: Sort): SortDescriptor {\n if (sort.direction === \"desc\")\n return { column: sort.column, direction: \"descending\" }\n return { column: sort.column, direction: \"ascending\" }\n}\n\nfunction serializeSort(sort: SortDescriptor): Sort {\n if (sort.direction === \"descending\")\n return { column: sort.column as string, direction: \"desc\" }\n return { column: sort.column as string, direction: \"asc\" }\n}\n\n/**\n * Resolves the `<Table>` selection props, driving React Aria's `<Table>` off\n * the component's internal selection state. `onSelectedChange` (the internal\n * state setter) always receives a plain array of keys: React Aria's `\"all\"`\n * selection sentinel (used for infinite-loading collections, which this\n * component doesn't have) is resolved to the concrete keys of the currently\n * loaded rows.\n */\nfunction useSelectableProps({\n data,\n getRowId,\n onSelectedChange,\n selectable,\n selectedKeys,\n}: {\n data: unknown[]\n getRowId: ((row: unknown) => number | string) | undefined\n onSelectedChange: (keys: (number | string)[]) => void\n selectable: boolean | undefined\n selectedKeys: (number | string)[]\n}): Pick<\n AriaTableProps,\n \"onSelectionChange\" | \"selectedKeys\" | \"selectionMode\"\n> {\n const selectedKeysSet = useMemo(\n () => new Set<Key>(selectedKeys),\n [selectedKeys]\n )\n const { rowKeys, rowOrder } = useMemo(() => {\n const rowKeys = data.map((row, index) => getRowKey(row, index, getRowId))\n return {\n rowKeys,\n rowOrder: new Map<Key, number>(rowKeys.map((key, index) => [key, index])),\n }\n }, [data, getRowId])\n\n if (!selectable) return { selectionMode: \"none\" }\n\n return {\n onSelectionChange: (keys: Selection) => {\n // Keys not found in `rowOrder` (e.g. a previously selected row no longer\n // in `data`) sort to the end rather than being dropped.\n const resolvedKeys =\n keys === \"all\"\n ? [...rowKeys]\n : Array.from(keys).sort(\n (a, b) =>\n (rowOrder.get(a) ?? Infinity) - (rowOrder.get(b) ?? Infinity)\n )\n onSelectedChange(resolvedKeys)\n },\n selectedKeys: selectedKeysSet,\n selectionMode: \"multiple\",\n }\n}\n\nfunction useStickyHeader(\n stickyHeader: boolean | undefined,\n scrollContainerRef: React.RefObject<HTMLElement> | undefined,\n scrollContainerSelector: string | undefined\n) {\n const wrapperRef = useRef<HTMLDivElement>(null)\n const [hasScrollContainer, setHasScrollContainer] = useState(false)\n\n useLayoutEffect(() => {\n setHasScrollContainer(false)\n if (!stickyHeader) return\n if (typeof ResizeObserver === \"undefined\") return\n const wrapper = wrapperRef.current\n if (!wrapper) return\n\n const scrollContainer = getScrollContainer(\n scrollContainerRef,\n scrollContainerSelector,\n wrapper\n )\n if (!scrollContainer) return\n\n setHasScrollContainer(true)\n\n let thead: HTMLElement | null = null\n\n const resizeObserver = new ResizeObserver(() => {\n if (!thead) {\n thead = wrapper.querySelector<HTMLElement>(\".tds-data-table-header\")\n if (!thead) return\n scrollContainer.style.setProperty(\n \"scroll-timeline-name\",\n \"--_sticky-header-scroll\"\n )\n }\n\n const { top: wrapperTop, bottom: wrapperBottom } =\n wrapper.getBoundingClientRect()\n const { top: scrollContainerTop } =\n scrollContainer.getBoundingClientRect()\n // Derive headerTop from the untransformed wrapper to avoid reading the\n // scroll-driven translateY that getBoundingClientRect() includes on thead.\n const headerTop = wrapperTop + wrapper.clientTop\n const start = scrollContainer.scrollTop + (headerTop - scrollContainerTop)\n const max = Math.max(0, wrapperBottom - headerTop - thead.offsetHeight)\n wrapper.style.setProperty(\"--_sticky-header-scroll-start\", `${start}px`)\n wrapper.style.setProperty(\"--_sticky-header-scroll-max\", `${max}px`)\n })\n\n resizeObserver.observe(wrapper)\n resizeObserver.observe(scrollContainer)\n\n return () => {\n resizeObserver.disconnect()\n wrapper.style.removeProperty(\"--_sticky-header-scroll-start\")\n wrapper.style.removeProperty(\"--_sticky-header-scroll-max\")\n }\n }, [stickyHeader, scrollContainerRef, scrollContainerSelector])\n\n return { hasScrollContainer, wrapperRef }\n}\n\nfunction getScrollContainer(\n scrollContainerRef: React.RefObject<HTMLElement> | undefined,\n scrollContainerSelector: string | undefined,\n wrapper: HTMLElement\n): HTMLElement | undefined {\n const element =\n scrollContainerRef?.current ??\n getScrollContainerFromSelector(scrollContainerSelector, wrapper)\n if (element instanceof Window) return\n return element\n}\n\nfunction getScrollContainerFromSelector(\n scrollContainerSelector: string | undefined,\n wrapper: HTMLElement\n): HTMLElement | undefined {\n return scrollContainerSelector\n ? (wrapper.parentElement?.closest<HTMLElement>(scrollContainerSelector) ??\n undefined)\n : undefined\n}\n"],"names":["React"],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A noun for the results count. A string uses it as the singular and appends
|
|
4
|
+
* `"s"` for the plural; a `[singular, plural]` tuple supplies an irregular
|
|
5
|
+
* plural; `true` defaults the noun to `"result"`; `false` renders no count.
|
|
6
|
+
*/
|
|
7
|
+
export type ResultsNoun = boolean | string | [singular: string, plural: string];
|
|
8
|
+
export interface ResultsCountProps {
|
|
9
|
+
/** An `id` for the rendered element so the table can link it via `aria-describedby`. */
|
|
10
|
+
id: string;
|
|
11
|
+
/**
|
|
12
|
+
* The noun (and whether to render a count at all). See {@link ResultsNoun}.
|
|
13
|
+
* When `false` or omitted, nothing renders.
|
|
14
|
+
*/
|
|
15
|
+
resultsNoun?: ResultsNoun;
|
|
16
|
+
/** Whether the table is selectable. */
|
|
17
|
+
selectable?: boolean;
|
|
18
|
+
/** The number of currently selected rows. */
|
|
19
|
+
selectedCount?: number;
|
|
20
|
+
/** The total number of results (typically `data.length`). */
|
|
21
|
+
totalCount: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Whether a {@link ResultsCount} will render anything for the given
|
|
25
|
+
* `resultsNoun`. A count is shown only when a noun is supplied, so callers can
|
|
26
|
+
* use this to decide whether to link the count via `aria-describedby`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function hasResultsCount(resultsNoun: ResultsNoun | undefined): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Resolves a {@link ResultsNoun} to a plain string usable as the table's
|
|
31
|
+
* `aria-label`, e.g. `"people"` or `"items"`. Uses the plural form — appending
|
|
32
|
+
* `"s"` to a string noun and taking the second element of a `[singular,
|
|
33
|
+
* plural]` tuple — to match the count label. Returns `undefined` when no
|
|
34
|
+
* meaningful noun is supplied — including the `true` default, which carries no
|
|
35
|
+
* caller-provided label — so callers can fall back to their own `aria-label`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveResultsNounLabel(resultsNoun: ResultsNoun | undefined): string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Renders the results count displayed above the table, e.g. `"1 result"`,
|
|
40
|
+
* `"1,234 results"`, or `"2 people"`. When the table is `selectable` and rows
|
|
41
|
+
* are selected, the count reflects the selection instead of the total, e.g.
|
|
42
|
+
* `"2 people selected"`. Renders nothing when `resultsNoun` is `false` or
|
|
43
|
+
* omitted.
|
|
44
|
+
*/
|
|
45
|
+
export declare const ResultsCount: {
|
|
46
|
+
({ id, resultsNoun, selectable, selectedCount, totalCount, }: ResultsCountProps): React.JSX.Element | null;
|
|
47
|
+
displayName: string;
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=ResultsCount.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResultsCount.d.ts","sourceRoot":"","sources":["../../../src/components/DataTable/ResultsCount.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;AAE/E,MAAM,WAAW,iBAAiB;IAChC,wFAAwF;IACxF,EAAE,EAAE,MAAM,CAAA;IACV;;;OAGG;IACH,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,uCAAuC;IACvC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,6CAA6C;IAC7C,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAE7E;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,WAAW,GAAG,SAAS,GACnC,MAAM,GAAG,SAAS,CAIpB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,YAAY;kEAMtB,iBAAiB;;CAenB,CAAA"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React__default from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether a {@link ResultsCount} will render anything for the given
|
|
5
|
+
* `resultsNoun`. A count is shown only when a noun is supplied, so callers can
|
|
6
|
+
* use this to decide whether to link the count via `aria-describedby`.
|
|
7
|
+
*/
|
|
8
|
+
function hasResultsCount(resultsNoun) {
|
|
9
|
+
return Boolean(resultsNoun);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Resolves a {@link ResultsNoun} to a plain string usable as the table's
|
|
13
|
+
* `aria-label`, e.g. `"people"` or `"items"`. Uses the plural form — appending
|
|
14
|
+
* `"s"` to a string noun and taking the second element of a `[singular,
|
|
15
|
+
* plural]` tuple — to match the count label. Returns `undefined` when no
|
|
16
|
+
* meaningful noun is supplied — including the `true` default, which carries no
|
|
17
|
+
* caller-provided label — so callers can fall back to their own `aria-label`.
|
|
18
|
+
*/
|
|
19
|
+
function resolveResultsNounLabel(resultsNoun) {
|
|
20
|
+
if (!resultsNoun || resultsNoun === true)
|
|
21
|
+
return undefined;
|
|
22
|
+
// A count other than 1 selects the plural form.
|
|
23
|
+
return resolveNoun(2, resultsNoun);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Renders the results count displayed above the table, e.g. `"1 result"`,
|
|
27
|
+
* `"1,234 results"`, or `"2 people"`. When the table is `selectable` and rows
|
|
28
|
+
* are selected, the count reflects the selection instead of the total, e.g.
|
|
29
|
+
* `"2 people selected"`. Renders nothing when `resultsNoun` is `false` or
|
|
30
|
+
* omitted.
|
|
31
|
+
*/
|
|
32
|
+
const ResultsCount = ({ id, resultsNoun, selectable, selectedCount = 0, totalCount, }) => {
|
|
33
|
+
if (!resultsNoun)
|
|
34
|
+
return null;
|
|
35
|
+
// When the table is selectable and rows are selected, the count reflects the
|
|
36
|
+
// selection (e.g. `"2 people selected"`) rather than the total row count.
|
|
37
|
+
const hasSelection = Boolean(selectable && selectedCount);
|
|
38
|
+
const label = hasSelection
|
|
39
|
+
? getResultsCountLabel(selectedCount, resultsNoun, { selected: true })
|
|
40
|
+
: getResultsCountLabel(totalCount, resultsNoun);
|
|
41
|
+
return (React__default.createElement("p", { id: id, className: "tds-data-table-results-count" }, label));
|
|
42
|
+
};
|
|
43
|
+
ResultsCount.displayName = "ResultsCount";
|
|
44
|
+
const numberFormatter = new Intl.NumberFormat();
|
|
45
|
+
/**
|
|
46
|
+
* Builds the results-count label, e.g. `"1 result"`, `"1,234 results"`, or
|
|
47
|
+
* `"2 people"`. The count is formatted with `Intl.NumberFormat` and the
|
|
48
|
+
* singular noun is used only when the count is 1, every other count uses the
|
|
49
|
+
* plural. `resultsNoun` may be a string (the plural appends `"s"`), a
|
|
50
|
+
* `[singular, plural]` tuple (the plural is supplied explicitly), or `true`
|
|
51
|
+
* (the noun defaults to `"result"`). When `selected` is set the count reflects
|
|
52
|
+
* the current selection and a `"selected"` suffix is appended, e.g.
|
|
53
|
+
* `"2 people selected"`.
|
|
54
|
+
*/
|
|
55
|
+
function getResultsCountLabel(count, resultsNoun, { selected = false } = {}) {
|
|
56
|
+
const noun = resolveNoun(count, resultsNoun);
|
|
57
|
+
const formattedCount = numberFormatter.format(count);
|
|
58
|
+
return `${formattedCount} ${noun}${selected ? " selected" : ""}`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Picks the singular or plural noun for `count` (singular only when `count` is
|
|
62
|
+
* 1, matching English pluralization).
|
|
63
|
+
* `resultsNoun` may be a string (the plural appends `"s"`), a
|
|
64
|
+
* `[singular, plural]` tuple, or `true` (the noun defaults to `"result"`).
|
|
65
|
+
*/
|
|
66
|
+
function resolveNoun(count, resultsNoun) {
|
|
67
|
+
const [singular, plural] = Array.isArray(resultsNoun)
|
|
68
|
+
? resultsNoun
|
|
69
|
+
: resultsNoun === true
|
|
70
|
+
? ["result", "results"]
|
|
71
|
+
: [resultsNoun, `${resultsNoun}s`];
|
|
72
|
+
return count === 1 ? singular : plural;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { ResultsCount, hasResultsCount, resolveResultsNounLabel };
|
|
76
|
+
//# sourceMappingURL=ResultsCount.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResultsCount.js","sources":["../../../src/components/DataTable/ResultsCount.tsx"],"sourcesContent":["import React from \"react\"\n\n/**\n * A noun for the results count. A string uses it as the singular and appends\n * `\"s\"` for the plural; a `[singular, plural]` tuple supplies an irregular\n * plural; `true` defaults the noun to `\"result\"`; `false` renders no count.\n */\nexport type ResultsNoun = boolean | string | [singular: string, plural: string]\n\nexport interface ResultsCountProps {\n /** An `id` for the rendered element so the table can link it via `aria-describedby`. */\n id: string\n /**\n * The noun (and whether to render a count at all). See {@link ResultsNoun}.\n * When `false` or omitted, nothing renders.\n */\n resultsNoun?: ResultsNoun\n /** Whether the table is selectable. */\n selectable?: boolean\n /** The number of currently selected rows. */\n selectedCount?: number\n /** The total number of results (typically `data.length`). */\n totalCount: number\n}\n\n/**\n * Whether a {@link ResultsCount} will render anything for the given\n * `resultsNoun`. A count is shown only when a noun is supplied, so callers can\n * use this to decide whether to link the count via `aria-describedby`.\n */\nexport function hasResultsCount(resultsNoun: ResultsNoun | undefined): boolean {\n return Boolean(resultsNoun)\n}\n\n/**\n * Resolves a {@link ResultsNoun} to a plain string usable as the table's\n * `aria-label`, e.g. `\"people\"` or `\"items\"`. Uses the plural form — appending\n * `\"s\"` to a string noun and taking the second element of a `[singular,\n * plural]` tuple — to match the count label. Returns `undefined` when no\n * meaningful noun is supplied — including the `true` default, which carries no\n * caller-provided label — so callers can fall back to their own `aria-label`.\n */\nexport function resolveResultsNounLabel(\n resultsNoun: ResultsNoun | undefined\n): string | undefined {\n if (!resultsNoun || resultsNoun === true) return undefined\n // A count other than 1 selects the plural form.\n return resolveNoun(2, resultsNoun)\n}\n\n/**\n * Renders the results count displayed above the table, e.g. `\"1 result\"`,\n * `\"1,234 results\"`, or `\"2 people\"`. When the table is `selectable` and rows\n * are selected, the count reflects the selection instead of the total, e.g.\n * `\"2 people selected\"`. Renders nothing when `resultsNoun` is `false` or\n * omitted.\n */\nexport const ResultsCount = ({\n id,\n resultsNoun,\n selectable,\n selectedCount = 0,\n totalCount,\n}: ResultsCountProps) => {\n if (!resultsNoun) return null\n\n // When the table is selectable and rows are selected, the count reflects the\n // selection (e.g. `\"2 people selected\"`) rather than the total row count.\n const hasSelection = Boolean(selectable && selectedCount)\n const label = hasSelection\n ? getResultsCountLabel(selectedCount, resultsNoun, { selected: true })\n : getResultsCountLabel(totalCount, resultsNoun)\n\n return (\n <p id={id} className=\"tds-data-table-results-count\">\n {label}\n </p>\n )\n}\n\nResultsCount.displayName = \"ResultsCount\"\n\nconst numberFormatter = new Intl.NumberFormat()\n\n/**\n * Builds the results-count label, e.g. `\"1 result\"`, `\"1,234 results\"`, or\n * `\"2 people\"`. The count is formatted with `Intl.NumberFormat` and the\n * singular noun is used only when the count is 1, every other count uses the\n * plural. `resultsNoun` may be a string (the plural appends `\"s\"`), a\n * `[singular, plural]` tuple (the plural is supplied explicitly), or `true`\n * (the noun defaults to `\"result\"`). When `selected` is set the count reflects\n * the current selection and a `\"selected\"` suffix is appended, e.g.\n * `\"2 people selected\"`.\n */\nfunction getResultsCountLabel(\n count: number,\n resultsNoun: Exclude<ResultsNoun, false>,\n { selected = false }: { selected?: boolean } = {}\n): string {\n const noun = resolveNoun(count, resultsNoun)\n const formattedCount = numberFormatter.format(count)\n return `${formattedCount} ${noun}${selected ? \" selected\" : \"\"}`\n}\n\n/**\n * Picks the singular or plural noun for `count` (singular only when `count` is\n * 1, matching English pluralization).\n * `resultsNoun` may be a string (the plural appends `\"s\"`), a\n * `[singular, plural]` tuple, or `true` (the noun defaults to `\"result\"`).\n */\nfunction resolveNoun(\n count: number,\n resultsNoun: Exclude<ResultsNoun, false>\n): string {\n const [singular, plural] = Array.isArray(resultsNoun)\n ? resultsNoun\n : resultsNoun === true\n ? [\"result\", \"results\"]\n : [resultsNoun, `${resultsNoun}s`]\n return count === 1 ? singular : plural\n}\n"],"names":["React"],"mappings":";;AAyBA;;;;AAIG;AACG,SAAU,eAAe,CAAC,WAAoC,EAAA;AAClE,IAAA,OAAO,OAAO,CAAC,WAAW,CAAC;AAC7B;AAEA;;;;;;;AAOG;AACG,SAAU,uBAAuB,CACrC,WAAoC,EAAA;AAEpC,IAAA,IAAI,CAAC,WAAW,IAAI,WAAW,KAAK,IAAI;AAAE,QAAA,OAAO,SAAS;;AAE1D,IAAA,OAAO,WAAW,CAAC,CAAC,EAAE,WAAW,CAAC;AACpC;AAEA;;;;;;AAMG;AACI,MAAM,YAAY,GAAG,CAAC,EAC3B,EAAE,EACF,WAAW,EACX,UAAU,EACV,aAAa,GAAG,CAAC,EACjB,UAAU,GACQ,KAAI;AACtB,IAAA,IAAI,CAAC,WAAW;AAAE,QAAA,OAAO,IAAI;;;IAI7B,MAAM,YAAY,GAAG,OAAO,CAAC,UAAU,IAAI,aAAa,CAAC;IACzD,MAAM,KAAK,GAAG;AACZ,UAAE,oBAAoB,CAAC,aAAa,EAAE,WAAW,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;AACrE,UAAE,oBAAoB,CAAC,UAAU,EAAE,WAAW,CAAC;AAEjD,IAAA,QACEA,cAAA,CAAA,aAAA,CAAA,GAAA,EAAA,EAAG,EAAE,EAAE,EAAE,EAAE,SAAS,EAAC,8BAA8B,EAAA,EAChD,KAAK,CACJ;AAER;AAEA,YAAY,CAAC,WAAW,GAAG,cAAc;AAEzC,MAAM,eAAe,GAAG,IAAI,IAAI,CAAC,YAAY,EAAE;AAE/C;;;;;;;;;AASG;AACH,SAAS,oBAAoB,CAC3B,KAAa,EACb,WAAwC,EACxC,EAAE,QAAQ,GAAG,KAAK,EAAA,GAA6B,EAAE,EAAA;IAEjD,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5C,MAAM,cAAc,GAAG,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC;AACpD,IAAA,OAAO,CAAA,EAAG,cAAc,CAAA,CAAA,EAAI,IAAI,GAAG,QAAQ,GAAG,WAAW,GAAG,EAAE,EAAE;AAClE;AAEA;;;;;AAKG;AACH,SAAS,WAAW,CAClB,KAAa,EACb,WAAwC,EAAA;IAExC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW;AAClD,UAAE;UACA,WAAW,KAAK;AAChB,cAAE,CAAC,QAAQ,EAAE,SAAS;cACpB,CAAC,WAAW,EAAE,GAAG,WAAW,CAAA,CAAA,CAAG,CAAC;IACtC,OAAO,KAAK,KAAK,CAAC,GAAG,QAAQ,GAAG,MAAM;AACxC;;;;"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import type { ResultsNoun } from "./ResultsCount";
|
|
3
|
+
/**
|
|
4
|
+
* A table action rendered as an `IconButton` in the button group above the
|
|
5
|
+
* table. Replaces the original RFC's `{ buttonProps; callback }` shape with a
|
|
6
|
+
* narrower, more controllable API so the design can evolve without exposing the
|
|
7
|
+
* full button surface.
|
|
8
|
+
*/
|
|
9
|
+
export type TableAction = {
|
|
10
|
+
/** Called with the keys of the currently selected rows when the action is triggered. */
|
|
11
|
+
callback?: (selectedKeys: (number | string)[]) => void;
|
|
12
|
+
/** Whether the action's button is disabled. */
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
/** The icon rendered inside the action's icon button. */
|
|
15
|
+
icon: ReactNode;
|
|
16
|
+
/** Accessible label for the icon-only action button. */
|
|
17
|
+
label: string;
|
|
18
|
+
/** Standard click handler, invoked with the click event before `callback`. */
|
|
19
|
+
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
|
20
|
+
};
|
|
21
|
+
export interface TableActionsProps {
|
|
22
|
+
/** The table actions to render as an `IconButton` group. */
|
|
23
|
+
actions: TableAction[];
|
|
24
|
+
/**
|
|
25
|
+
* The noun used to describe the selection in each action's `aria-label`.
|
|
26
|
+
* See {@link ResultsNoun}.
|
|
27
|
+
*/
|
|
28
|
+
resultsNoun?: ResultsNoun;
|
|
29
|
+
/** The keys of the currently selected rows, passed to each action's `callback`. */
|
|
30
|
+
selectedKeys: (number | string)[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Renders the table actions as an `IconButton` group displayed directly after
|
|
34
|
+
* the results count, above the table. Each entry becomes an icon button whose
|
|
35
|
+
* `callback` receives the keys of the currently selected rows.
|
|
36
|
+
*/
|
|
37
|
+
export declare const TableActions: {
|
|
38
|
+
({ actions, resultsNoun, selectedKeys, }: TableActionsProps): React.JSX.Element;
|
|
39
|
+
displayName: string;
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=TableActions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TableActions.d.ts","sourceRoot":"","sources":["../../../src/components/DataTable/TableActions.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAE7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAEjD;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,CAAC,YAAY,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,KAAK,IAAI,CAAA;IACtD,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,yDAAyD;IACzD,IAAI,EAAE,SAAS,CAAA;IACf,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAA;IACb,8EAA8E;IAC9E,OAAO,CAAC,EAAE,KAAK,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAA;CACrD,CAAA;AAED,MAAM,WAAW,iBAAiB;IAChC,4DAA4D;IAC5D,OAAO,EAAE,WAAW,EAAE,CAAA;IACtB;;;OAGG;IACH,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,mFAAmF;IACnF,YAAY,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAA;CAClC;AAED;;;;GAIG;AACH,eAAO,MAAM,YAAY;8CAItB,iBAAiB;;CAwBnB,CAAA"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import '../button/Button.js';
|
|
2
|
+
import '../button/DropdownButton.js';
|
|
3
|
+
import '../button/DropdownIconButton.js';
|
|
4
|
+
import { IconButton } from '../button/IconButton.js';
|
|
5
|
+
import '../button/LoadingButton.js';
|
|
6
|
+
import '../button/PageHeaderActionsDropdownButton.js';
|
|
7
|
+
import { ButtonGroup } from '../button-group/ButtonGroup.js';
|
|
8
|
+
import React__default from 'react';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Renders the table actions as an `IconButton` group displayed directly after
|
|
12
|
+
* the results count, above the table. Each entry becomes an icon button whose
|
|
13
|
+
* `callback` receives the keys of the currently selected rows.
|
|
14
|
+
*/
|
|
15
|
+
const TableActions = ({ actions, resultsNoun, selectedKeys, }) => {
|
|
16
|
+
// The count/noun suffix is the same for every table action, so compute it once
|
|
17
|
+
// here rather than per-button inside the map below.
|
|
18
|
+
const countSuffix = getTableActionCountSuffix(selectedKeys.length, resultsNoun);
|
|
19
|
+
return (React__default.createElement(ButtonGroup, { "aria-label": "Table actions" }, actions.map(({ callback, disabled, icon, label, onClick }) => (React__default.createElement(IconButton, { key: label, "aria-label": `${label}${countSuffix}`, disabled: disabled, icon: icon, onClick: (event) => {
|
|
20
|
+
onClick?.(event);
|
|
21
|
+
callback?.(selectedKeys);
|
|
22
|
+
} })))));
|
|
23
|
+
};
|
|
24
|
+
TableActions.displayName = "TableActions";
|
|
25
|
+
const numberFormatter = new Intl.NumberFormat();
|
|
26
|
+
/**
|
|
27
|
+
* The count/noun suffix appended to each table action's `aria-label` when rows
|
|
28
|
+
* are selected, e.g. `" (2 selected people)"` — so `"Email"` reads as
|
|
29
|
+
* `"Email (2 selected people)"`. Empty when nothing is selected. Falls back to
|
|
30
|
+
* the default `"result"` noun when `resultsNoun` isn't provided. Shared across
|
|
31
|
+
* every action, so it's computed once rather than per-button.
|
|
32
|
+
*/
|
|
33
|
+
function getTableActionCountSuffix(count, resultsNoun) {
|
|
34
|
+
if (!count)
|
|
35
|
+
return "";
|
|
36
|
+
const [singular, plural] = Array.isArray(resultsNoun)
|
|
37
|
+
? resultsNoun
|
|
38
|
+
: typeof resultsNoun === "string"
|
|
39
|
+
? [resultsNoun, `${resultsNoun}s`]
|
|
40
|
+
: ["result", "results"];
|
|
41
|
+
const noun = count === 1 ? singular : plural;
|
|
42
|
+
const formattedCount = numberFormatter.format(count);
|
|
43
|
+
return ` (${formattedCount} selected ${noun})`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { TableActions };
|
|
47
|
+
//# sourceMappingURL=TableActions.js.map
|