laif-ds 1.0.5-app-data-grid.102 → 1.0.5-app-data-grid.104
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/CHANGELOG.md +2 -5
- package/dist/_virtual/index6.js +2 -2
- package/dist/_virtual/index7.js +2 -2
- package/dist/agent-docs/adoption-report.json +36 -5
- package/dist/agent-docs/components/AppDataGrid/columns.md +113 -0
- package/dist/agent-docs/components/AppDataGrid/dom-hooks.md +75 -0
- package/dist/agent-docs/components/AppDataGrid/examples.md +129 -0
- package/dist/agent-docs/components/AppDataGrid/filtering.md +126 -0
- package/dist/agent-docs/components/AppDataGrid/grouping.md +20 -0
- package/dist/agent-docs/components/AppDataGrid/i18n-and-accessibility.md +40 -0
- package/dist/agent-docs/components/AppDataGrid/layout.md +45 -0
- package/dist/agent-docs/components/AppDataGrid/migration-from-datatable.md +32 -0
- package/dist/agent-docs/components/AppDataGrid/saved-views.md +76 -0
- package/dist/agent-docs/components/AppDataGrid/selection.md +44 -0
- package/dist/agent-docs/components/AppDataGrid/server-and-pagination.md +73 -0
- package/dist/agent-docs/components/AppDataGrid/toolbar.md +28 -0
- package/dist/agent-docs/components/AppDataGrid.md +77 -819
- package/dist/agent-docs/components-list.md +11 -61
- package/dist/agent-docs/manifest.json +29 -82
- package/dist/index.d.ts +1 -1
- package/dist/node_modules/eventemitter3/index2.js +1 -1
- package/dist/node_modules/style-to-object/cjs/index.js +1 -1
- package/package.json +1 -1
package/dist/CHANGELOG.md
CHANGED
|
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
### 🚀 Added
|
|
11
11
|
|
|
12
|
-
- **AppDataGrid**: New data grid, the successor to `DataTable`. Columns are declared with the `createGrid*Column` builders (string, number, boolean, date, date-time, single/multi select, computed, display, action, row menu, column groups).
|
|
12
|
+
- **AppDataGrid**: New data grid, the successor to `DataTable`. Columns are declared with the `createGrid*Column` builders (string, number, boolean, date, date-time, single/multi select, computed, display, action, row menu, column groups). Filters live in a filter row inside the columns by default (`filterMode="row"`) or in a panel (`filterMode="panel"`, work in progress), can be lifted outside the grid (`filters`, `GridFilterChips`, `useGridFilters`), and application-declared filter controls are supported (`filterControls`); a `toolbar.resetFilters` slot restores filters and search to the page's own defaults. Server mode via `onQueryChange`, fired only on search/sort/filter/page changes and debounced by `debounceTime`; `paginated` mode — with an AppSelect page-size picker, a "Page i/N" indicator and a default `pageSize` of 25 — or an `infinite` source; `groupBy` grouping with aggregated cells; row selection with a page-selection bar for bulk actions; column pinning, resizing and reordering, notified through `onColumnWidthsChange` / `onColumnOrderChange`; `virtualized` rendering; `density` and full screen, with ghost toolbar buttons for the filter row, column visibility and full screen shown by default and individually disableable; saved views capturing filters, search, sorting, density, column visibility, widths and order (`preset`, `readGridViewPreset`, `createInMemoryGridViewStore`); `i18n` (`defaultGridI18n`); loading, empty and error/retry states; a polite live region announcing result changes. See `AppDataGrid.md`
|
|
13
13
|
- **DataTable**: The filter-option getters (`getStringFilterOptions`, `getNumberFilterOptions`, …), `defaultDataTableI18n` / `italianDataTableI18n` and the service helpers (`applyClientSideFilter`, `buildSearchFilter`, …) are now exported from the package root
|
|
14
14
|
- **designTokens**: The shared design-token object is now exported from the package root, including the new `table` (row/header heights, cell padding) and `tableZ` (z-index layers) entries
|
|
15
15
|
- **Button**: New `icon-xs` size
|
|
@@ -21,11 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
21
21
|
|
|
22
22
|
- **AppSelect**: The trigger is now an ARIA `combobox` (`aria-haspopup="listbox"`, `aria-controls`, `aria-activedescendant`): it opens with Enter, Space or the arrow keys, Arrow/Home/End move through the enabled options, and without `label`/`aria-label` its accessible name falls back to `placeholder`. The popover is no longer modal, so the rest of the page is not hidden from assistive technology while it is open. In multiple mode each chosen option exposes `aria-checked`, and search matches both the option label and its value
|
|
23
23
|
- **Theme**: In the default light theme `--colors--surface--secondary` (behind `d-secondary` and `d-muted`) now uses a new `neutral--150` step instead of `neutral--125`, so it is distinguishable from the page background
|
|
24
|
-
|
|
25
|
-
### 🗑️ Deprecated
|
|
26
|
-
|
|
27
24
|
- **DataTable**: Deprecated in favour of **AppDataGrid**. It is still exported and existing code keeps compiling (no `@deprecated` tag, so consumer lint rules are not tripped); its docs and stories are marked deprecated. Migration guide: `AppDataGrid.md`
|
|
28
|
-
- **GridFilterPanel**: Exported for the lifted-filters layout but deprecated in favour of the AppDataGrid filter row
|
|
25
|
+
- **GridFilterPanel**: Exported for the lifted-filters panel layout but deprecated in favour of the AppDataGrid filter row (`filterMode="row"`)
|
|
29
26
|
|
|
30
27
|
### 🐛 Fixed
|
|
31
28
|
|
package/dist/_virtual/index6.js
CHANGED
package/dist/_virtual/index7.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
|
-
"generatedAt": "2026-09-30T12:
|
|
3
|
+
"generatedAt": "2026-09-30T12:51:53.069Z",
|
|
4
4
|
"package": {
|
|
5
5
|
"name": "laif-ds",
|
|
6
|
-
"version": "1.0.5-app-data-grid.
|
|
6
|
+
"version": "1.0.5-app-data-grid.104"
|
|
7
7
|
},
|
|
8
8
|
"summary": {
|
|
9
|
-
"scannedFiles":
|
|
10
|
-
"deprecatedComponentsCount":
|
|
11
|
-
"deprecatedComponentsUsedCount":
|
|
9
|
+
"scannedFiles": 502,
|
|
10
|
+
"deprecatedComponentsCount": 5,
|
|
11
|
+
"deprecatedComponentsUsedCount": 3,
|
|
12
12
|
"componentsWithoutExamplesCount": 5,
|
|
13
13
|
"componentsWithoutPropsTableCount": 30,
|
|
14
14
|
"repeatedClassPatternsCount": 25,
|
|
@@ -89,6 +89,37 @@
|
|
|
89
89
|
}
|
|
90
90
|
]
|
|
91
91
|
},
|
|
92
|
+
{
|
|
93
|
+
"component": "GridFilterPanel",
|
|
94
|
+
"replacement": "AppDataGrid",
|
|
95
|
+
"totalOccurrences": 7,
|
|
96
|
+
"usages": [
|
|
97
|
+
{
|
|
98
|
+
"file": "src/components/ui/app-data-grid/app-data-grid.types.ts",
|
|
99
|
+
"occurrences": 2
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"file": "src/components/ui/app-data-grid/app-data-grid.tsx",
|
|
103
|
+
"occurrences": 1
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"file": "src/components/ui/app-data-grid/index.ts",
|
|
107
|
+
"occurrences": 1
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"file": "src/components/ui/app-data-grid/parts/grid-filter-panel.tsx",
|
|
111
|
+
"occurrences": 1
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"file": "src/components/ui/app-data-grid/parts/grid-filter-row.tsx",
|
|
115
|
+
"occurrences": 1
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"file": "src/components/ui/app-data-grid/use-grid-filters.ts",
|
|
119
|
+
"occurrences": 1
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
},
|
|
92
123
|
{
|
|
93
124
|
"component": "SidebarMenuBadge",
|
|
94
125
|
"replacement": "NavItem.badge",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# AppDataGrid — Columns
|
|
2
|
+
|
|
3
|
+
> Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
|
|
4
|
+
|
|
5
|
+
## Column definition
|
|
6
|
+
|
|
7
|
+
### Builders
|
|
8
|
+
|
|
9
|
+
| Builder | Column type | `cell` signature | `cell` required | `id` required | Sortable / filterable / searchable by default |
|
|
10
|
+
| ------------------------------ | -------------------- | ------------------------ | --------------- | ------------- | --------------------------------------------- |
|
|
11
|
+
| `createGridStringColumn` | `string` | `(value, row, tableRow)` | no | no | yes / yes / no |
|
|
12
|
+
| `createGridNumberColumn` | `number` | `(value, row, tableRow)` | no | no | yes / yes / no |
|
|
13
|
+
| `createGridBooleanColumn` | `boolean` | `(value, row, tableRow)` | no | no | yes / yes / no |
|
|
14
|
+
| `createGridDateColumn` | `date` | `(value, row, tableRow)` | no | no | yes / yes / no |
|
|
15
|
+
| `createGridDateTimeColumn` | `datetime` | `(value, row, tableRow)` | no | no | yes / yes / no |
|
|
16
|
+
| `createGridSingleSelectColumn` | `list_single_select` | `(value, row, tableRow)` | no | no | yes / yes / no |
|
|
17
|
+
| `createGridMultiSelectColumn` | `list_multi_select` | `(value, row, tableRow)` | no | no | yes / yes / no |
|
|
18
|
+
| `createGridComputedColumn` | `other` | `(value, row, tableRow)` | no | no | no / no / no |
|
|
19
|
+
| `createGridActionColumn` | — | `(row)` | **yes** | **yes** | never |
|
|
20
|
+
| `createGridDisplayColumn` | — | `(row)` | **yes** | no | no / no / opt-in |
|
|
21
|
+
| `createGridRowMenuColumn` | — | none (`rowMenu`) | n/a | **yes** | never |
|
|
22
|
+
| `createGridColumnGroup` | group | — | — | **yes** | — |
|
|
23
|
+
|
|
24
|
+
The two `cell` arities are copied from `DataTable` on purpose, so existing `cell:` closures keep compiling
|
|
25
|
+
after a rename. All builders are generic: `createGridNumberColumn<Row, number>({ ... })`.
|
|
26
|
+
|
|
27
|
+
Column ids: explicit `id`, else `accessorKey`, else derived from `headerLabel` (`<type>-<slug>`); a
|
|
28
|
+
column with none of them throws. Ids must be stable across reloads — they key widths, visibility, order,
|
|
29
|
+
sorting and saved views.
|
|
30
|
+
|
|
31
|
+
### Accessor builder options
|
|
32
|
+
|
|
33
|
+
Applies to the eight accessor builders (`accessorKey`-based).
|
|
34
|
+
|
|
35
|
+
| Option | Type | Default | Description |
|
|
36
|
+
| ----------------- | -------------------------------------------------------------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
37
|
+
| `accessorKey` | `string` | — | **Required.** Field to read; dotted paths (`user.name`) read nested values. Also the key used in `computedFilter`. |
|
|
38
|
+
| `header` | `string \| ReactNode` | — | **Required.** When not a string, `headerLabel` is required by the type. |
|
|
39
|
+
| `headerLabel` | `string` | `header` if a non-empty string, else `id` | Text form of the header: accessible name, menus, chips, columns panel. |
|
|
40
|
+
| `id` | `string` | `accessorKey` | Stable column id. |
|
|
41
|
+
| `sortable` | `boolean` | `true` (`false` for computed) | Renders the sort button. Note: `DataTable` defaulted to `false`. |
|
|
42
|
+
| `filterable` | `boolean` | `true` (`false` for computed) | Offers the column in the filter row / panel. Note: `DataTable` defaulted to `false`. |
|
|
43
|
+
| `searchable` | `boolean` | `false` | **Opt-in.** Includes the column in the global search (client matching and `computedFilter`). Enable it only on text-like columns the backend can search. |
|
|
44
|
+
| `filterType` | `GridFilterType` (column types except `"other"`) | the column type | How the column is filtered, when different from its type (e.g. `"list_single_select"` + `options` on a string column). |
|
|
45
|
+
| `options` | `readonly string[] \| readonly GridSelectOption[] \| readonly AppSelectOption[]` | `undefined` | Choices of a list column (or of a list `filterType`). Bare strings are normalised to `{ value, label }`. Numeric `value`s are kept as numbers in the payload. |
|
|
46
|
+
| `pinned` | `"left" \| "right"` | `undefined` | Sticks the column to an edge during horizontal scroll. Pin a **prefix** (left) or a **suffix** (right): offsets accumulate in declaration order. |
|
|
47
|
+
| `align` | `"left" \| "right"` | `"left"` | Content alignment of header, cells, group cells and footer. `"right"` adds `tabular-nums`. |
|
|
48
|
+
| `width` | `number` | `160` (elastic) | Initial width in px (64–1200). Columns without a declared width share leftover space; a drag replaces it. |
|
|
49
|
+
| `rowHeader` | `boolean` | `false` | Renders this column's cells as `<th scope="row">` — the row's name for assistive tech and for row-menu labels. At most one column per grid (a second one throws). |
|
|
50
|
+
| `cell` | `(value, row, tableRow) => ReactNode` | stringified value | Cell renderer. Text returned as a string gets automatic truncation handling (tooltip + focusable target only when it overflows). |
|
|
51
|
+
| `cellClassName` | `string \| ((value, row) => string)` | `undefined` | Cell classes. Note the two-parameter callback. |
|
|
52
|
+
| `headerClassName` | `string` | `undefined` | Header cell classes. |
|
|
53
|
+
| `aggregatedCell` | `(rows: TData[]) => ReactNode` | `undefined` | Content of this column's cell in **group rows**. Receives every original row under the group, at any depth. Absent = empty group cell. |
|
|
54
|
+
| `aggregate` | `(rows: TData[]) => ReactNode` | `undefined` | Column total in the sticky `<tfoot>`, over the rows after search/filters/sort (the page when `paginated`). No footer unless a visible column declares one. |
|
|
55
|
+
|
|
56
|
+
Action / display / row-menu columns accept `id`, `header`/`headerLabel`, `pinned`, `width`, `align`,
|
|
57
|
+
`rowHeader` (display: also `searchable`). `createGridRowMenuColumn` takes `rowMenu` instead of `cell`.
|
|
58
|
+
|
|
59
|
+
### Column groups (multi-level headers)
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
createGridColumnGroup<Row>({
|
|
63
|
+
id: "customer", // required, unique among ALL nodes (groups and leaves)
|
|
64
|
+
header: "Customer",
|
|
65
|
+
headerLabel: "Customer", // required when header is not a string
|
|
66
|
+
columns: [nameColumn, cityColumn /* or nested groups */],
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Groups add header rows only; rows, sorting, filtering, widths and pinning see the flat list of leaves.
|
|
71
|
+
Nesting depth is unlimited. Each body cell is linked to its headers through `id`/`headers`. A group whose
|
|
72
|
+
leaves are all hidden disappears.
|
|
73
|
+
|
|
74
|
+
### Row menu column
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
createGridRowMenuColumn<Job>({
|
|
78
|
+
id: "actions",
|
|
79
|
+
header: "Actions",
|
|
80
|
+
pinned: "right",
|
|
81
|
+
width: 100,
|
|
82
|
+
rowMenu: (row) => [
|
|
83
|
+
{ id: "open", label: "Open", onSelect: () => open(row.id) },
|
|
84
|
+
{
|
|
85
|
+
id: "duplicate",
|
|
86
|
+
label: "Duplicate",
|
|
87
|
+
onSelect: () => duplicate(row.id),
|
|
88
|
+
disabled: row.closed,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "delete",
|
|
92
|
+
label: "Delete",
|
|
93
|
+
destructive: true,
|
|
94
|
+
onSelect: () => remove(row.id),
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Items are data (`GridRowMenuItem`: `id`, `label`, `onSelect`, `destructive?`, `disabled?`); the grid
|
|
101
|
+
renders the `…` trigger and a non-modal menu aligned to the end. The trigger's accessible name is
|
|
102
|
+
`i18n.table.rowMenu` plus the row's identity (from the `rowHeader` column, else the row id). Returning an
|
|
103
|
+
empty list (or `undefined`) renders no trigger.
|
|
104
|
+
|
|
105
|
+
### Column helpers
|
|
106
|
+
|
|
107
|
+
- `createGridSorting([{ column: "name", order: "asc" }])` → `GridSorting` (`{ sort_by, sort_order }`).
|
|
108
|
+
- `createGridInitialState({ filters?, searchbarFilter?, sorting?, pagination?, columnVisibility? })` →
|
|
109
|
+
`GridInitialState` (for `initialState`).
|
|
110
|
+
- `toGridFilterColumns(columns)` → `GridFilterColumn[]` for `useGridFilters` (flat leaf columns; columns
|
|
111
|
+
without `accessorKey` are skipped).
|
|
112
|
+
|
|
113
|
+
---
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# AppDataGrid — DOM hooks
|
|
2
|
+
|
|
3
|
+
> Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
|
|
4
|
+
|
|
5
|
+
## DOM hooks (for tests)
|
|
6
|
+
|
|
7
|
+
Stable attributes, independent of the i18n language.
|
|
8
|
+
|
|
9
|
+
| Selector / attribute | Meaning |
|
|
10
|
+
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| `[data-app-data-grid]` | Grid root. |
|
|
12
|
+
| `data-emissions` (root) | Query emissions: `0` at mount, +1 per emission, unchanged by view actions. Debounced: read in `waitFor`. |
|
|
13
|
+
| `data-render-count` (root) | React commits of the root since mount. |
|
|
14
|
+
| `data-mount-count` (root) | Mounts of a grid with this `id` / `data-testid` (stays `1` across full-screen toggles). |
|
|
15
|
+
| `data-loadmore-call-count` (root) | Load-more requests issued (infinite mode). |
|
|
16
|
+
| `data-grid-fullscreen` (root) | `"true"` / `"false"`. |
|
|
17
|
+
| `[data-grid-toolbar]` | Toolbar. |
|
|
18
|
+
| `[data-grid-search]` | Search input. |
|
|
19
|
+
| `[data-grid-viewport]` | Scroll container; its `clientHeight` equals `viewportHeight`. |
|
|
20
|
+
| `[data-grid-table]` | The `<table>`. |
|
|
21
|
+
| `[data-grid-col="<id>"]` | `<col>` of a column — count columns with this (the extra layout col is `[data-grid-col-filler]`). |
|
|
22
|
+
| `[data-grid-header="<id>"]` | Leaf `<th>` (`aria-sort`). |
|
|
23
|
+
| `[data-grid-header-group="<id>"]` | Group `<th>` (`scope="colgroup"`). |
|
|
24
|
+
| `[data-grid-header-row="<n>"]` | Header `<tr>`, 0-based. |
|
|
25
|
+
| `[data-grid-sort="<id>"]` | Sort button. |
|
|
26
|
+
| `[data-grid-sort-index]` | Multi-sort priority badge. |
|
|
27
|
+
| `[data-grid-resize-handle="<id>"]` | Resize handle (`aria-valuenow` = width). |
|
|
28
|
+
| `[data-grid-row]` | Data row, never a spacer. Use `querySelector` for the first one (the first `<tr>` may be a spacer). |
|
|
29
|
+
| `[data-grid-cell="<id>"]` | Body cell. |
|
|
30
|
+
| `[data-grid-row-header]` | Row-header cell (`<th scope="row">`). |
|
|
31
|
+
| `[data-grid-spacer]` | Virtualization spacer rows. |
|
|
32
|
+
| `[data-grid-skeleton-row]` | Skeleton row (`aria-hidden`). |
|
|
33
|
+
| `[data-grid-pinned="true"]` | Pinned cell (either side). |
|
|
34
|
+
| `[data-grid-cell-text]` | Text cell that fits. |
|
|
35
|
+
| `[data-grid-cell-expand]` | Focusable target of a truncated text cell. |
|
|
36
|
+
| `[data-grid-row-menu]` | Row-menu trigger. |
|
|
37
|
+
| `[data-grid-row-menu-item="<id>"]` | Row-menu item (`data-variant="destructive"` when destructive). |
|
|
38
|
+
| `[data-grid-live-region]` | The single status region. |
|
|
39
|
+
| `[data-grid-empty]` | Empty-outcome cell. |
|
|
40
|
+
| `[data-grid-error]` | Error-outcome cell. |
|
|
41
|
+
| `[data-grid-status-cell]` | The outcome cell, empty or error. |
|
|
42
|
+
| `[data-grid-retry]` | Retry button (absent without `onRetry`). |
|
|
43
|
+
| `[data-grid-footer]` | Totals `<tfoot>`. |
|
|
44
|
+
| `[data-grid-footer-cell="<id>"]` | Footer cell. |
|
|
45
|
+
| `[data-grid-pagination]` | Paginator. |
|
|
46
|
+
| `[data-grid-page-range]` | «Rows x-y of N». |
|
|
47
|
+
| `[data-grid-page-number="<n>"]` | Page button (1-based; current one has `aria-current="page"`). |
|
|
48
|
+
| `[data-grid-page-size]` | Wrapper of the page-size selector. |
|
|
49
|
+
| `tr[data-grid-filter-row]` | Filter row (absent while hidden). |
|
|
50
|
+
| `[data-grid-filter-cell="<id>"]` | Filter cell. |
|
|
51
|
+
| `[data-grid-filter-cell-trigger="<id>"]` | Filter cell's button. |
|
|
52
|
+
| `[data-grid-header-filtered="<id>"]` | Funnel on a filtered column's header. |
|
|
53
|
+
| `[data-grid-toolbar-filter-row-toggle]` | Filter-row toggle. |
|
|
54
|
+
| `[data-grid-toolbar-columns]` | Columns-panel trigger. |
|
|
55
|
+
| `[data-grid-fullscreen-toggle]` | Full-screen toggle. |
|
|
56
|
+
| `[data-grid-actions-row]` | Container of the `actions` prop buttons. |
|
|
57
|
+
| `[data-grid-action="<label>"]` | One `actions` command. |
|
|
58
|
+
| `[data-grid-actions-trigger]` | Collapsed trigger (two or more actions). |
|
|
59
|
+
| `[data-grid-group-row="<path>"]` | Group row (`aria-expanded`). |
|
|
60
|
+
| `[data-grid-group-toggle="<path>"]` | Group expand/collapse control. |
|
|
61
|
+
| `[data-grid-group-count]` | Group row count — read the attribute (the text has parentheses). |
|
|
62
|
+
| `[data-grid-group-select="<path>"]` | Group checkbox. |
|
|
63
|
+
| `[data-grid-select-all]` | Header checkbox. |
|
|
64
|
+
| `[data-grid-selection-bar]` | Selection bar. |
|
|
65
|
+
| `[data-grid-selection-count]` | Selected count. |
|
|
66
|
+
| `[data-grid-selection-action="<label>"]` | Bulk action. |
|
|
67
|
+
| `[data-grid-toolbar-custom-filters]` | Toolbar group of custom filters. |
|
|
68
|
+
| `[data-grid-toolbar-custom-filter="<id>"]` | One custom filter (by badge id). |
|
|
69
|
+
| `[data-grid-filter-chip-trigger="<id>"]` | A chip in `GridFilterChips`. |
|
|
70
|
+
| `[data-grid-filter-add-chip]` | «+ Filter» chip. |
|
|
71
|
+
| `[data-grid-filter-scope]` | Chips' scope clause. |
|
|
72
|
+
| `[data-grid-column-registry-item="<id>"]` | Columns-panel row. |
|
|
73
|
+
| `[data-grid-column-registry-toggle="<id>"]` | Columns-panel visibility checkbox. |
|
|
74
|
+
|
|
75
|
+
---
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# AppDataGrid — Examples
|
|
2
|
+
|
|
3
|
+
> Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
|
|
4
|
+
|
|
5
|
+
## Examples
|
|
6
|
+
|
|
7
|
+
### Server grid with `onQueryChange`
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { useState } from "react";
|
|
11
|
+
import {
|
|
12
|
+
AppDataGrid,
|
|
13
|
+
type GridColumnNode,
|
|
14
|
+
type GridServerState,
|
|
15
|
+
} from "laif-ds";
|
|
16
|
+
|
|
17
|
+
const initialQuery: GridServerState = {
|
|
18
|
+
pagination: { pageIndex: 0, pageSize: 25 },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const UsersGrid = ({ columns }: { columns: GridColumnNode<User>[] }) => {
|
|
22
|
+
// The grid does not emit at mount: the initial query is fetched by the consumer.
|
|
23
|
+
const [query, setQuery] = useState<GridServerState>(initialQuery);
|
|
24
|
+
const { data, isLoading, isError, refetch } = useUsersSearch(query); // your data hook
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<AppDataGrid
|
|
28
|
+
columns={columns}
|
|
29
|
+
data={data?.items ?? []}
|
|
30
|
+
getRowId={(row) => String(row.id)}
|
|
31
|
+
serverMode
|
|
32
|
+
paginated
|
|
33
|
+
serverConfig={{ totalItems: data?.total ?? 0, onStateChange: setQuery }}
|
|
34
|
+
loading={isLoading}
|
|
35
|
+
error={isError}
|
|
36
|
+
onRetry={() => void refetch()}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
// In useUsersSearch, send query.computedFilter, query.computedSorting and query.pagination.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Grouping with aggregates
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
const columns: GridColumnNode<Sale>[] = [
|
|
47
|
+
createGridStringColumn<Sale>({ accessorKey: "country", header: "Country" }),
|
|
48
|
+
createGridStringColumn<Sale>({ accessorKey: "city", header: "City" }),
|
|
49
|
+
createGridNumberColumn<Sale, number>({
|
|
50
|
+
accessorKey: "amount",
|
|
51
|
+
header: "Amount",
|
|
52
|
+
align: "right",
|
|
53
|
+
aggregatedCell: (rows) => rows.reduce((sum, r) => sum + r.amount, 0),
|
|
54
|
+
aggregate: (rows) => rows.reduce((sum, r) => sum + r.amount, 0),
|
|
55
|
+
}),
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
<AppDataGrid
|
|
59
|
+
columns={columns}
|
|
60
|
+
data={sales}
|
|
61
|
+
groupBy={["country", "city"]}
|
|
62
|
+
initialOpenGroups="first"
|
|
63
|
+
groupLabelPlacement="first-column"
|
|
64
|
+
viewportHeight={520}
|
|
65
|
+
/>;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Lifted filters with chips and the filter row
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { useMemo, useState } from "react";
|
|
72
|
+
import {
|
|
73
|
+
AppDataGrid,
|
|
74
|
+
GridFilterChips,
|
|
75
|
+
gridResultScope,
|
|
76
|
+
gridViewportHeight,
|
|
77
|
+
toGridFilterColumns,
|
|
78
|
+
useGridFilters,
|
|
79
|
+
} from "laif-ds";
|
|
80
|
+
|
|
81
|
+
export const OrdersPage = ({ orders }: { orders: Order[] }) => {
|
|
82
|
+
const filters = useGridFilters({ columns: toGridFilterColumns(columns) });
|
|
83
|
+
const [filterRowOpen, setFilterRowOpen] = useState(true); // the row starts open
|
|
84
|
+
|
|
85
|
+
// With lifted filters the consumer filters the rows (client) or sends
|
|
86
|
+
// filters.computedFilter to the server.
|
|
87
|
+
const visibleOrders = useMemo(
|
|
88
|
+
() => applyMyFilters(orders, filters.committedBadges),
|
|
89
|
+
[orders, filters.committedBadges],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<>
|
|
94
|
+
<GridFilterChips
|
|
95
|
+
badges={filters.badges}
|
|
96
|
+
onRemove={filters.removeBadge}
|
|
97
|
+
onClearAll={filters.clearBadges}
|
|
98
|
+
scope={gridResultScope({
|
|
99
|
+
badges: filters.badges,
|
|
100
|
+
resultTotal: visibleOrders.length,
|
|
101
|
+
})}
|
|
102
|
+
/>
|
|
103
|
+
<AppDataGrid
|
|
104
|
+
columns={columns}
|
|
105
|
+
data={visibleOrders}
|
|
106
|
+
resultTotal={visibleOrders.length}
|
|
107
|
+
filters={filters.badges}
|
|
108
|
+
columnVisibility={filters.columnVisibility}
|
|
109
|
+
onColumnVisibilityChange={(next) => {
|
|
110
|
+
for (const [id, visible] of Object.entries(next))
|
|
111
|
+
if (filters.columnVisibility[id] !== visible)
|
|
112
|
+
filters.setColumnVisible(id, visible);
|
|
113
|
+
}}
|
|
114
|
+
onAddFilter={filters.addBadge}
|
|
115
|
+
onUpdateFilter={filters.updateBadge}
|
|
116
|
+
onRemoveFilter={filters.removeBadge}
|
|
117
|
+
onFilterRowOpenChange={setFilterRowOpen}
|
|
118
|
+
viewportHeight={gridViewportHeight({
|
|
119
|
+
rowCount: 10,
|
|
120
|
+
density: "default",
|
|
121
|
+
hasFilterRow: filterRowOpen,
|
|
122
|
+
})}
|
|
123
|
+
/>
|
|
124
|
+
</>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# AppDataGrid — Filtering
|
|
2
|
+
|
|
3
|
+
> Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
|
|
4
|
+
|
|
5
|
+
## Filtering
|
|
6
|
+
|
|
7
|
+
### Global search
|
|
8
|
+
|
|
9
|
+
The search field filters rows (client) or feeds `filters.searchbarFilter` and `computedFilter` (server).
|
|
10
|
+
Only columns with `searchable: true` are matched. Keystrokes are debounced by `debounceTime`.
|
|
11
|
+
|
|
12
|
+
### Filter row (default, `filterMode="row"`)
|
|
13
|
+
|
|
14
|
+
A row inside `<thead>` with one filter cell per column (plus the checkbox column). Each cell is a button
|
|
15
|
+
that opens an editor popover; the editor and its operators are derived from the column's `filterType`:
|
|
16
|
+
text field, number field, boolean, date / date-time picker, single or multi list (from `options`). A
|
|
17
|
+
list column without options is shown as an incomplete declaration, never as a free text field. Cells of
|
|
18
|
+
filtered columns get an accent border; filtered headers show a funnel icon. The toolbar toggle unmounts
|
|
19
|
+
and remounts the row.
|
|
20
|
+
|
|
21
|
+
Without lifted `filters`, the grid owns the filter state and (on the client branch) filters `data`
|
|
22
|
+
itself. Client-side matching reproduces the backend operators (`like`, `starts_with`, `eq`,
|
|
23
|
+
`array_overlap`, `date_after`, …), with edge-case differences a client cannot replicate (database
|
|
24
|
+
collation/case folding of non-ASCII text, `citext` columns, decimal precision). `%` and `_` in text
|
|
25
|
+
filters behave as `LIKE` wildcards, as on the server.
|
|
26
|
+
|
|
27
|
+
Operators (`GridFilterOperator`): `like`, `n_like`, `starts_with`, `ends_with`, `eq`, `ne`, `eq_null`,
|
|
28
|
+
`n_eq_null`, `gt`, `ge`, `lt`, `le`, `checked`, `unchecked`, `date_after`, `date_before`,
|
|
29
|
+
`date_time_after`, `date_time_before`, `array_overlap`, `n_array_overlap`. A range is expressed as two
|
|
30
|
+
badges (e.g. `ge` + `le`).
|
|
31
|
+
|
|
32
|
+
### Lifted filters (`useGridFilters`)
|
|
33
|
+
|
|
34
|
+
Lift the filter state when chips, counts or a server fetch live outside the grid. The hook owns the
|
|
35
|
+
badges; the grid, `GridFilterChips` and `GridFilterPanel` read/write the same store, and the chips work
|
|
36
|
+
without a grid in the tree.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const filters = useGridFilters({
|
|
40
|
+
columns: toGridFilterColumns(leafColumns), // required, for pruning
|
|
41
|
+
initialBadges, // optional
|
|
42
|
+
initialColumnVisibility, // optional
|
|
43
|
+
debounceTime, // optional, default 200
|
|
44
|
+
onFiltersChange: (computedFilter) => refetch(computedFilter), // optional
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`GridFiltersApi`: `badges` (live, feed the grid and editors), `committedBadges` (debounced, use them to
|
|
49
|
+
filter rows), `computedFilter`, `columnVisibility`, `panelOpen`, `emissions`, `addBadge`, `addBadges`
|
|
50
|
+
(several in one mutation — use for ranges), `updateBadge`, `removeBadge`, `removeBadges`, `clearBadges`,
|
|
51
|
+
`restore(badges, columnVisibility)`, `setColumnVisible(id, visible)`, `setPanelOpen(open)`.
|
|
52
|
+
|
|
53
|
+
Rules when `filters` is passed to the grid:
|
|
54
|
+
|
|
55
|
+
- **You filter the rows.** The grid does not apply lifted badges to `data`. On the client, filter with
|
|
56
|
+
`committedBadges`; on the server, send `filters.computedFilter` (or the value received by
|
|
57
|
+
`onFiltersChange`).
|
|
58
|
+
- **The grid does not emit for them** (except in infinite mode, below). `onFiltersChange` is called
|
|
59
|
+
synchronously inside the hook's actions (keystrokes coalesced by the hook's `debounceTime`; adding,
|
|
60
|
+
removing and restoring are immediate), and only when the payload actually changes.
|
|
61
|
+
- **Pagination resets to page 0** when the badges' content changes (compared by content, so an inline
|
|
62
|
+
`filters={badges.filter(...)}` is fine).
|
|
63
|
+
- Wire `onAddFilter={filters.addBadge}`, `onUpdateFilter={filters.updateBadge}` and
|
|
64
|
+
`onRemoveFilter={filters.removeBadge}`; pass `columnVisibility={filters.columnVisibility}` with
|
|
65
|
+
`onColumnVisibilityChange` so hidden columns also prune their badges.
|
|
66
|
+
|
|
67
|
+
#### Infinite mode restarts on its own
|
|
68
|
+
|
|
69
|
+
With `infinite` on (and `paginated` off), a change in the content of lifted `filters` makes the grid
|
|
70
|
+
emit **one** `onQueryChange` with `pageIndex: 0`; later load-mores ask for 1, 2, …. Keystrokes into an
|
|
71
|
+
existing badge coalesce into one restart. It never fires at mount, and a change that matches the last
|
|
72
|
+
emission (e.g. the `restore()` after a preset) does not restart. Recommended wiring in infinite mode:
|
|
73
|
+
do **not** also refetch in `onFiltersChange`; if you do, treat `pageIndex: 0` as a replace and drop
|
|
74
|
+
superseded responses.
|
|
75
|
+
|
|
76
|
+
### Filter chips, panel and columns panel
|
|
77
|
+
|
|
78
|
+
- `GridFilterChips` — `{ badges, onRemove, onClearAll?, scope?, onAddFilter?, onEditValue?, i18n?,
|
|
79
|
+
className? }`. A row of chips (one button per badge with an edit/remove menu), an optional dashed «+
|
|
80
|
+
Filter» chip, and a scope clause («128 rows · 2 filters») when `scope` is passed. Build `scope` with
|
|
81
|
+
`gridResultScope({ badges, resultTotal, rowsInPage?, pageIndex?, pageSize?, locale? })`.
|
|
82
|
+
- `GridFilterPanel` — the filter inventory overlay used by `filterMode="panel"`; see
|
|
83
|
+
[GridFilterPanel](./GridFilterPanel.md). Mount it inside the grid through `toolbar.filters.render`,
|
|
84
|
+
otherwise it is covered in full screen.
|
|
85
|
+
- `GridColumnRegistryPanel` — the standalone columns panel (`columns`, `columnVisibility`,
|
|
86
|
+
`onColumnVisibleChange`, `columnOrder?`, `onColumnOrderChange?`, `i18n?`, `className?`). The grid
|
|
87
|
+
already renders one behind `toolbar.columns`.
|
|
88
|
+
|
|
89
|
+
### Custom filters (without a column)
|
|
90
|
+
|
|
91
|
+
A filter that cites no column («only favourites», «older than N days») is declared once as a
|
|
92
|
+
`GridFilterControl` and enters the state as a badge with `kind: "custom"`. It is counted, cleared by
|
|
93
|
+
reset, appended to `computedFilter`, and never pruned by column visibility.
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import {
|
|
97
|
+
createGridCustomFilterBadge,
|
|
98
|
+
defaultGridI18n,
|
|
99
|
+
type GridFilterControl,
|
|
100
|
+
type GridFilterControlRegistry,
|
|
101
|
+
} from "laif-ds";
|
|
102
|
+
|
|
103
|
+
const olderThan: GridFilterControl = {
|
|
104
|
+
type: "olderThan", // registry key
|
|
105
|
+
Control: ({ value, onChange, label }) => (
|
|
106
|
+
<DaysInput value={value} onChange={onChange} label={label} />
|
|
107
|
+
), // rendered by YOUR app, not by the grid
|
|
108
|
+
toFragment: (value, ctx) => ({ [ctx.field]: { operator: "ge", value } }), // null = not applied
|
|
109
|
+
describe: (value) => `Older than ${String(value)} days`, // the only text source
|
|
110
|
+
};
|
|
111
|
+
const registry: GridFilterControlRegistry = { olderThan };
|
|
112
|
+
|
|
113
|
+
filters.addBadge(
|
|
114
|
+
createGridCustomFilterBadge(olderThan, 30, defaultGridI18n, "older"),
|
|
115
|
+
);
|
|
116
|
+
// To change the value: rebuild with the SAME id and call filters.updateBadge("older", badge).
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`toFragment` and `describe` are both mandatory. `createGridCustomFilterBadge(control, value, i18n, id)`
|
|
120
|
+
is the only constructor; it fills `kind`, `customKey`, `customLabel` and `customFragment`. With lifted
|
|
121
|
+
`filters` and `onRemoveFilter`, the toolbar shows a dashed group with each custom filter's full name and
|
|
122
|
+
a remove button; chips show `customLabel`. Pass `filterControls={registry}` to flag badges whose key is
|
|
123
|
+
not registered. The grid never renders `Control`: custom filters are named and removable in the grid,
|
|
124
|
+
edited where your app creates them.
|
|
125
|
+
|
|
126
|
+
---
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# AppDataGrid — Grouping
|
|
2
|
+
|
|
3
|
+
> Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
|
|
4
|
+
|
|
5
|
+
## Grouping
|
|
6
|
+
|
|
7
|
+
Client branch only. Each `groupBy` entry is one level; group rows show label, chevron and count
|
|
8
|
+
(`data-grid-group-count`), and one cell per visible column filled by that column's `aggregatedCell`.
|
|
9
|
+
|
|
10
|
+
- Groups are ordered by first appearance (i.e. by the current sort), not alphabetically.
|
|
11
|
+
- Expanding is view state and never emits. Opening or closing a group above the viewport keeps what you
|
|
12
|
+
are reading in place; closing a group you are reading inside lands on the group row.
|
|
13
|
+
- Hiding the grouping column does not ungroup: the label moves to the first visible column.
|
|
14
|
+
- A grouped grid always has a bounded viewport (`viewportHeight`, default 480).
|
|
15
|
+
- `aria-rowcount` counts the traversable rows — data rows plus the group rows on screen — so it changes
|
|
16
|
+
as groups open and close.
|
|
17
|
+
- The `<tfoot>` total (`aggregate`) sums the data rows, not the group aggregates.
|
|
18
|
+
- Group-row checkboxes select every leaf of the group, even when it is collapsed.
|
|
19
|
+
|
|
20
|
+
---
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# AppDataGrid — i18n and accessibility
|
|
2
|
+
|
|
3
|
+
> Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
|
|
4
|
+
|
|
5
|
+
## i18n
|
|
6
|
+
|
|
7
|
+
`GridI18n` is a superset of `DataTableI18n`. Required top-level keys: `search`, `searchPlaceholder`,
|
|
8
|
+
`clearSearch`, `reset`, `notFoundMessage`, `searchTooltipLabel`, `selectAll`, `selectRow`, and the
|
|
9
|
+
sections `filters`, `stringOperators`, `numberOperators`, `booleanOperators`, `dateOperators`,
|
|
10
|
+
`dateTimeOperators`, `listOperators`, `sorting`, `columnVisibility`, `pagination`, `tableActions`.
|
|
11
|
+
Optional sections (fall back to `defaultGridI18n`): `selectColumnHeader`, `columnResize`, `columnOrder`,
|
|
12
|
+
`loading`, `toolbar`, `table`, `grouping`, `selection`, `status`.
|
|
13
|
+
|
|
14
|
+
`table` keys include `label` («Data grid», the `<table>` name), `skipToRows` / `skipToHeader` (bypass
|
|
15
|
+
links), `expandCell`, `rowMenu`, `errorMessage`, `retry`, `totalsLabel`.
|
|
16
|
+
|
|
17
|
+
Always start from the default bag: `i18n={{ ...defaultGridI18n, ...overrides }}`. There is no bundled
|
|
18
|
+
Italian bag; `GRID_IT_LOCALE` (`"it-IT"`) is the locale constant for number formatting in
|
|
19
|
+
`gridResultScope`.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Accessibility
|
|
24
|
+
|
|
25
|
+
- Native table semantics: `aria-sort` on sortable headers; `id`/`headers` chain for multi-level headers;
|
|
26
|
+
`aria-rowcount` / `aria-rowindex` over the traversable body rows (header rows excluded; `-1` while an
|
|
27
|
+
infinite source has more); `aria-colcount`.
|
|
28
|
+
- One row header per row via `rowHeader: true`; `aria-label` on the table from `i18n.table.label`; two
|
|
29
|
+
bypass links («skip to rows», «skip to headers»).
|
|
30
|
+
- **Exactly one live region** (`role="status"`, polite, atomic) announces loading, results, errors, sort,
|
|
31
|
+
filters and selection. Skeleton rows are `aria-hidden`.
|
|
32
|
+
- Truncated text cells get a focusable target with the full value; cells that fit add no tab stop.
|
|
33
|
+
Copying a data row pastes as one spreadsheet row.
|
|
34
|
+
- Full screen: `role="dialog"`, focus contained, Escape exits.
|
|
35
|
+
- Menus and popovers are non-modal and portal inside the grid root when needed (so they stay reachable
|
|
36
|
+
in full screen).
|
|
37
|
+
- Known gaps: column resize has no single-pointer alternative to dragging (WCAG 2.5.7) and the handle's
|
|
38
|
+
pointer target is 8 px wide (2.5.8); infinite loading has no keyboard trigger other than scrolling.
|
|
39
|
+
|
|
40
|
+
---
|