@navikt/ds-react 8.5.0 → 8.5.2
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/cjs/data/table/helpers/table-grid-nav.d.ts +9 -15
- package/cjs/data/table/helpers/table-grid-nav.js +18 -25
- package/cjs/data/table/helpers/table-grid-nav.js.map +1 -1
- package/cjs/data/table/helpers/table-keyboard.d.ts +1 -1
- package/cjs/data/table/helpers/table-keyboard.js +1 -6
- package/cjs/data/table/helpers/table-keyboard.js.map +1 -1
- package/cjs/data/table/root/DataTableRoot.d.ts +41 -4
- package/cjs/data/table/root/DataTableRoot.js +10 -6
- package/cjs/data/table/root/DataTableRoot.js.map +1 -1
- package/cjs/data/table/root/useTableKeyboardNav.d.ts +1 -1
- package/cjs/data/table/root/useTableKeyboardNav.js +32 -19
- package/cjs/data/table/root/useTableKeyboardNav.js.map +1 -1
- package/cjs/data/table/td/DataTableTd.d.ts +5 -4
- package/cjs/data/table/td/DataTableTd.js +2 -2
- package/cjs/data/table/td/DataTableTd.js.map +1 -1
- package/cjs/data/token-filter/AutoSuggest.d.ts +9 -0
- package/cjs/data/token-filter/AutoSuggest.js +56 -0
- package/cjs/data/token-filter/AutoSuggest.js.map +1 -0
- package/cjs/data/token-filter/AutoSuggest.types.d.ts +12 -0
- package/cjs/data/token-filter/AutoSuggest.types.js +3 -0
- package/cjs/data/token-filter/AutoSuggest.types.js.map +1 -0
- package/cjs/data/token-filter/TokenFilter.d.ts +11 -0
- package/cjs/data/token-filter/TokenFilter.js +102 -0
- package/cjs/data/token-filter/TokenFilter.js.map +1 -0
- package/cjs/data/token-filter/TokenFilter.types.d.ts +52 -0
- package/cjs/data/token-filter/TokenFilter.types.js +3 -0
- package/cjs/data/token-filter/TokenFilter.types.js.map +1 -0
- package/cjs/data/token-filter/helpers/generate-autocomplete-options.d.ts +24 -0
- package/cjs/data/token-filter/helpers/generate-autocomplete-options.js +197 -0
- package/cjs/data/token-filter/helpers/generate-autocomplete-options.js.map +1 -0
- package/cjs/data/token-filter/helpers/grouping.d.ts +28 -0
- package/cjs/data/token-filter/helpers/grouping.js +61 -0
- package/cjs/data/token-filter/helpers/grouping.js.map +1 -0
- package/cjs/data/token-filter/helpers/operators.d.ts +22 -0
- package/cjs/data/token-filter/helpers/operators.js +66 -0
- package/cjs/data/token-filter/helpers/operators.js.map +1 -0
- package/cjs/data/token-filter/helpers/parse-query-text.d.ts +25 -0
- package/cjs/data/token-filter/helpers/parse-query-text.js +46 -0
- package/cjs/data/token-filter/helpers/parse-query-text.js.map +1 -0
- package/cjs/data/token-filter/helpers/query-builder.d.ts +20 -0
- package/cjs/data/token-filter/helpers/query-builder.js +38 -0
- package/cjs/data/token-filter/helpers/query-builder.js.map +1 -0
- package/cjs/data/token-filter/helpers/text-matching.d.ts +16 -0
- package/cjs/data/token-filter/helpers/text-matching.js +47 -0
- package/cjs/data/token-filter/helpers/text-matching.js.map +1 -0
- package/cjs/form/combobox/Input/InputController.js +1 -1
- package/cjs/form/combobox/Input/InputController.js.map +1 -1
- package/cjs/form/file-upload/dropzone/FileUploadDropzone.js +1 -1
- package/cjs/form/file-upload/dropzone/FileUploadDropzone.js.map +1 -1
- package/cjs/tooltip/Tooltip.js +1 -1
- package/cjs/tooltip/Tooltip.js.map +1 -1
- package/cjs/utils/i18n/locales/nb.d.ts +75 -154
- package/cjs/utils/i18n/locales/nb.js +75 -154
- package/cjs/utils/i18n/locales/nb.js.map +1 -1
- package/esm/data/table/helpers/table-grid-nav.d.ts +9 -15
- package/esm/data/table/helpers/table-grid-nav.js +18 -25
- package/esm/data/table/helpers/table-grid-nav.js.map +1 -1
- package/esm/data/table/helpers/table-keyboard.d.ts +1 -1
- package/esm/data/table/helpers/table-keyboard.js +1 -6
- package/esm/data/table/helpers/table-keyboard.js.map +1 -1
- package/esm/data/table/root/DataTableRoot.d.ts +41 -4
- package/esm/data/table/root/DataTableRoot.js +10 -6
- package/esm/data/table/root/DataTableRoot.js.map +1 -1
- package/esm/data/table/root/useTableKeyboardNav.d.ts +1 -1
- package/esm/data/table/root/useTableKeyboardNav.js +32 -19
- package/esm/data/table/root/useTableKeyboardNav.js.map +1 -1
- package/esm/data/table/td/DataTableTd.d.ts +5 -4
- package/esm/data/table/td/DataTableTd.js +2 -2
- package/esm/data/table/td/DataTableTd.js.map +1 -1
- package/esm/data/token-filter/AutoSuggest.d.ts +9 -0
- package/esm/data/token-filter/AutoSuggest.js +20 -0
- package/esm/data/token-filter/AutoSuggest.js.map +1 -0
- package/esm/data/token-filter/AutoSuggest.types.d.ts +12 -0
- package/esm/data/token-filter/AutoSuggest.types.js +2 -0
- package/esm/data/token-filter/AutoSuggest.types.js.map +1 -0
- package/esm/data/token-filter/TokenFilter.d.ts +11 -0
- package/esm/data/token-filter/TokenFilter.js +66 -0
- package/esm/data/token-filter/TokenFilter.js.map +1 -0
- package/esm/data/token-filter/TokenFilter.types.d.ts +52 -0
- package/esm/data/token-filter/TokenFilter.types.js +2 -0
- package/esm/data/token-filter/TokenFilter.types.js.map +1 -0
- package/esm/data/token-filter/helpers/generate-autocomplete-options.d.ts +24 -0
- package/esm/data/token-filter/helpers/generate-autocomplete-options.js +195 -0
- package/esm/data/token-filter/helpers/generate-autocomplete-options.js.map +1 -0
- package/esm/data/token-filter/helpers/grouping.d.ts +28 -0
- package/esm/data/token-filter/helpers/grouping.js +59 -0
- package/esm/data/token-filter/helpers/grouping.js.map +1 -0
- package/esm/data/token-filter/helpers/operators.d.ts +22 -0
- package/esm/data/token-filter/helpers/operators.js +60 -0
- package/esm/data/token-filter/helpers/operators.js.map +1 -0
- package/esm/data/token-filter/helpers/parse-query-text.d.ts +25 -0
- package/esm/data/token-filter/helpers/parse-query-text.js +44 -0
- package/esm/data/token-filter/helpers/parse-query-text.js.map +1 -0
- package/esm/data/token-filter/helpers/query-builder.d.ts +20 -0
- package/esm/data/token-filter/helpers/query-builder.js +34 -0
- package/esm/data/token-filter/helpers/query-builder.js.map +1 -0
- package/esm/data/token-filter/helpers/text-matching.d.ts +16 -0
- package/esm/data/token-filter/helpers/text-matching.js +45 -0
- package/esm/data/token-filter/helpers/text-matching.js.map +1 -0
- package/esm/form/combobox/Input/InputController.js +1 -1
- package/esm/form/combobox/Input/InputController.js.map +1 -1
- package/esm/form/file-upload/dropzone/FileUploadDropzone.js +1 -1
- package/esm/form/file-upload/dropzone/FileUploadDropzone.js.map +1 -1
- package/esm/tooltip/Tooltip.js +2 -2
- package/esm/tooltip/Tooltip.js.map +1 -1
- package/esm/utils/i18n/locales/nb.d.ts +75 -154
- package/esm/utils/i18n/locales/nb.js +75 -154
- package/esm/utils/i18n/locales/nb.js.map +1 -1
- package/package.json +3 -3
- package/src/data/table/helpers/table-grid-nav.test.ts +659 -0
- package/src/data/table/helpers/table-grid-nav.ts +19 -38
- package/src/data/table/helpers/table-keyboard.ts +1 -10
- package/src/data/table/root/DataTableRoot.tsx +50 -10
- package/src/data/table/root/useTableKeyboardNav.ts +35 -23
- package/src/data/table/td/DataTableTd.tsx +13 -6
- package/src/data/token-filter/AutoSuggest.tsx +55 -0
- package/src/data/token-filter/AutoSuggest.types.ts +14 -0
- package/src/data/token-filter/TokenFilter.tsx +129 -0
- package/src/data/token-filter/TokenFilter.types.ts +85 -0
- package/src/data/token-filter/helpers/generate-autocomplete-options.test.ts +896 -0
- package/src/data/token-filter/helpers/generate-autocomplete-options.ts +289 -0
- package/src/data/token-filter/helpers/grouping.test.ts +206 -0
- package/src/data/token-filter/helpers/grouping.ts +73 -0
- package/src/data/token-filter/helpers/operators.test.ts +281 -0
- package/src/data/token-filter/helpers/operators.ts +91 -0
- package/src/data/token-filter/helpers/parse-query-text.test.ts +201 -0
- package/src/data/token-filter/helpers/parse-query-text.ts +86 -0
- package/src/data/token-filter/helpers/query-builder.test.ts +126 -0
- package/src/data/token-filter/helpers/query-builder.ts +41 -0
- package/src/data/token-filter/helpers/text-matching.test.ts +125 -0
- package/src/data/token-filter/helpers/text-matching.ts +58 -0
- package/src/form/combobox/Input/InputController.tsx +0 -1
- package/src/form/file-upload/dropzone/FileUploadDropzone.tsx +0 -1
- package/src/tooltip/Tooltip.tsx +3 -3
- package/src/utils/i18n/locales/nb.ts +4 -83
|
@@ -85,14 +85,11 @@ function getNextGridPosition(
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
|
-
* Checks if a cell is focusable (
|
|
88
|
+
* Checks if a cell is focusable (contains focusable elements).
|
|
89
89
|
* Type guard that narrows Element | undefined to Element.
|
|
90
90
|
*/
|
|
91
|
-
function isCellFocusable(
|
|
92
|
-
cell
|
|
93
|
-
currentCell: Element,
|
|
94
|
-
): cell is Element {
|
|
95
|
-
if (!cell || cell === currentCell) {
|
|
91
|
+
function isCellFocusable(cell: Element | undefined): cell is Element {
|
|
92
|
+
if (!cell) {
|
|
96
93
|
return false;
|
|
97
94
|
}
|
|
98
95
|
return !!findFocusableElementInCell(cell);
|
|
@@ -100,7 +97,7 @@ function isCellFocusable(
|
|
|
100
97
|
|
|
101
98
|
/**
|
|
102
99
|
* Finds the next cell in the given direction, starting from the current position.
|
|
103
|
-
* Skips over cells that are not focusable
|
|
100
|
+
* Skips over cells that are not focusable.
|
|
104
101
|
* Returns null if no next cell is found in the given direction.
|
|
105
102
|
*/
|
|
106
103
|
function findNextFocusableCell(
|
|
@@ -118,7 +115,9 @@ function findNextFocusableCell(
|
|
|
118
115
|
}
|
|
119
116
|
|
|
120
117
|
const cell = grid[nextPos.y][nextPos.x];
|
|
121
|
-
|
|
118
|
+
|
|
119
|
+
/* We check against current cell to avoid returning the same cell in cases of rowspan/colspan. */
|
|
120
|
+
if (cell !== currentCell && isCellFocusable(cell)) {
|
|
122
121
|
return cell;
|
|
123
122
|
}
|
|
124
123
|
|
|
@@ -127,22 +126,16 @@ function findNextFocusableCell(
|
|
|
127
126
|
}
|
|
128
127
|
|
|
129
128
|
/**
|
|
130
|
-
* Finds the first focusable cell in the
|
|
129
|
+
* Finds the first focusable cell in the given row.
|
|
131
130
|
*/
|
|
132
131
|
function findFirstCellInRow(
|
|
133
132
|
grid: (Element | undefined)[][],
|
|
134
|
-
|
|
135
|
-
currentCell: Element,
|
|
133
|
+
rowIndex: number,
|
|
136
134
|
): Element | null {
|
|
137
|
-
const
|
|
138
|
-
if (!currentPos) {
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const row = grid[currentPos.y] ?? [];
|
|
135
|
+
const row = grid[rowIndex] ?? [];
|
|
143
136
|
for (let x = 0; x < row.length; x += 1) {
|
|
144
137
|
const cell = row[x];
|
|
145
|
-
if (isCellFocusable(cell
|
|
138
|
+
if (isCellFocusable(cell)) {
|
|
146
139
|
return cell;
|
|
147
140
|
}
|
|
148
141
|
}
|
|
@@ -151,22 +144,16 @@ function findFirstCellInRow(
|
|
|
151
144
|
}
|
|
152
145
|
|
|
153
146
|
/**
|
|
154
|
-
* Finds the last focusable cell in the
|
|
147
|
+
* Finds the last focusable cell in the given row.
|
|
155
148
|
*/
|
|
156
149
|
function findLastCellInRow(
|
|
157
150
|
grid: (Element | undefined)[][],
|
|
158
|
-
|
|
159
|
-
currentCell: Element,
|
|
151
|
+
rowIndex: number,
|
|
160
152
|
): Element | null {
|
|
161
|
-
const
|
|
162
|
-
if (!currentPos) {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const row = grid[currentPos.y] ?? [];
|
|
153
|
+
const row = grid[rowIndex] ?? [];
|
|
167
154
|
for (let x = row.length - 1; x >= 0; x -= 1) {
|
|
168
155
|
const cell = row[x];
|
|
169
|
-
if (isCellFocusable(cell
|
|
156
|
+
if (isCellFocusable(cell)) {
|
|
170
157
|
return cell;
|
|
171
158
|
}
|
|
172
159
|
}
|
|
@@ -177,15 +164,12 @@ function findLastCellInRow(
|
|
|
177
164
|
/**
|
|
178
165
|
* Finds the first focusable cell in the entire table.
|
|
179
166
|
*/
|
|
180
|
-
function findFirstCell(
|
|
181
|
-
grid: (Element | undefined)[][],
|
|
182
|
-
currentCell: Element,
|
|
183
|
-
): Element | null {
|
|
167
|
+
function findFirstCell(grid: (Element | undefined)[][]): Element | null {
|
|
184
168
|
for (let y = 0; y < grid.length; y += 1) {
|
|
185
169
|
const row = grid[y] ?? [];
|
|
186
170
|
for (let x = 0; x < row.length; x += 1) {
|
|
187
171
|
const cell = row[x];
|
|
188
|
-
if (isCellFocusable(cell
|
|
172
|
+
if (isCellFocusable(cell)) {
|
|
189
173
|
return cell;
|
|
190
174
|
}
|
|
191
175
|
}
|
|
@@ -197,15 +181,12 @@ function findFirstCell(
|
|
|
197
181
|
/**
|
|
198
182
|
* Finds the last focusable cell in the entire table.
|
|
199
183
|
*/
|
|
200
|
-
function findLastCell(
|
|
201
|
-
grid: (Element | undefined)[][],
|
|
202
|
-
currentCell: Element,
|
|
203
|
-
): Element | null {
|
|
184
|
+
function findLastCell(grid: (Element | undefined)[][]): Element | null {
|
|
204
185
|
for (let y = grid.length - 1; y >= 0; y -= 1) {
|
|
205
186
|
const row = grid[y] ?? [];
|
|
206
187
|
for (let x = row.length - 1; x >= 0; x -= 1) {
|
|
207
188
|
const cell = row[x];
|
|
208
|
-
if (isCellFocusable(cell
|
|
189
|
+
if (isCellFocusable(cell)) {
|
|
209
190
|
return cell;
|
|
210
191
|
}
|
|
211
192
|
}
|
|
@@ -27,7 +27,6 @@ function getNavigationAction(event: KeyboardEvent): NavigationAction | null {
|
|
|
27
27
|
return { type: "delta", delta: keyToCoord[key as DirectionsT] };
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
// Home/End keys
|
|
31
30
|
if (key === "Home") {
|
|
32
31
|
return event.ctrlKey || event.metaKey
|
|
33
32
|
? { type: "tableStart" }
|
|
@@ -54,15 +53,7 @@ function getNavigationAction(event: KeyboardEvent): NavigationAction | null {
|
|
|
54
53
|
* - User is navigating inside multiline textarea
|
|
55
54
|
* - contenteditable attrb is in use
|
|
56
55
|
*/
|
|
57
|
-
function shouldBlockNavigation(
|
|
58
|
-
event: KeyboardEvent,
|
|
59
|
-
customBlockFn?: (event: KeyboardEvent) => boolean,
|
|
60
|
-
): boolean {
|
|
61
|
-
/* Check custom block function first */
|
|
62
|
-
if (customBlockFn?.(event)) {
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
|
|
56
|
+
function shouldBlockNavigation(event: KeyboardEvent): boolean {
|
|
66
57
|
const key = event.key;
|
|
67
58
|
if (!(key in keyToCoord)) {
|
|
68
59
|
return false;
|
|
@@ -24,18 +24,49 @@ import { useTableKeyboardNav } from "./useTableKeyboardNav";
|
|
|
24
24
|
|
|
25
25
|
interface DataTableProps extends React.HTMLAttributes<HTMLTableElement> {
|
|
26
26
|
children: React.ReactNode;
|
|
27
|
+
/**
|
|
28
|
+
* Controls vertical cell padding.
|
|
29
|
+
* @default "normal"
|
|
30
|
+
*/
|
|
27
31
|
rowDensity?: "condensed" | "normal" | "spacious";
|
|
32
|
+
/**
|
|
33
|
+
* Zebra striped table
|
|
34
|
+
* @default false
|
|
35
|
+
*/
|
|
36
|
+
zebraStripes?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Truncate content in cells and show ellipsis for overflowed text.
|
|
39
|
+
*
|
|
40
|
+
* **NB:** When using `layout="auto"`, you have to manually set a `maxWidth` on columns that should be truncated.
|
|
41
|
+
* @default true
|
|
42
|
+
*/
|
|
43
|
+
truncateContent?: boolean; // TODO: Consider making this default false when layout=auto, and maybe disallow it but add a wrap prop on the td-comp.
|
|
28
44
|
/**
|
|
29
45
|
* Enables keyboard navigation for table rows and cells.
|
|
30
46
|
* @default false
|
|
31
47
|
*/
|
|
32
48
|
withKeyboardNav?: boolean;
|
|
33
49
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
50
|
+
* Custom callback to determine if navigation should be blocked.
|
|
51
|
+
* Called before default blocking logic.
|
|
52
|
+
* Requires `withKeyboardNav` to be `true`.
|
|
36
53
|
*/
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
shouldBlockNavigation?: (event: KeyboardEvent) => boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Controls table layout.
|
|
57
|
+
*
|
|
58
|
+
* ### fixed
|
|
59
|
+
* Gives you full control of column widths. This is required for resizable columns.
|
|
60
|
+
*
|
|
61
|
+
* ### auto
|
|
62
|
+
* Makes the columns resize automatically based on the content.
|
|
63
|
+
* The table will take up at least 100% of available width.
|
|
64
|
+
*
|
|
65
|
+
* **NB:** When using this with `truncateContent`, you have to manually
|
|
66
|
+
* set a `contentMaxWidth` on cells that should be truncated.
|
|
67
|
+
* @default "fixed"
|
|
68
|
+
*/
|
|
69
|
+
layout?: "fixed" | "auto";
|
|
39
70
|
}
|
|
40
71
|
|
|
41
72
|
interface DataTableRootComponent extends React.ForwardRefExoticComponent<
|
|
@@ -133,6 +164,12 @@ interface DataTableRootComponent extends React.ForwardRefExoticComponent<
|
|
|
133
164
|
Tfoot: typeof DataTableTfoot;
|
|
134
165
|
}
|
|
135
166
|
|
|
167
|
+
/**
|
|
168
|
+
* TODO Component description etc.
|
|
169
|
+
*
|
|
170
|
+
* **NB:** To get sticky headers, you have to set a height restriction on the table container. You can use VStack for this:
|
|
171
|
+
* TODO example
|
|
172
|
+
*/
|
|
136
173
|
const DataTable = forwardRef<HTMLTableElement, DataTableProps>(
|
|
137
174
|
(
|
|
138
175
|
{
|
|
@@ -141,6 +178,8 @@ const DataTable = forwardRef<HTMLTableElement, DataTableProps>(
|
|
|
141
178
|
withKeyboardNav = false,
|
|
142
179
|
zebraStripes = false,
|
|
143
180
|
truncateContent = true,
|
|
181
|
+
shouldBlockNavigation,
|
|
182
|
+
layout = "fixed",
|
|
144
183
|
...rest
|
|
145
184
|
},
|
|
146
185
|
forwardedRef,
|
|
@@ -148,8 +187,9 @@ const DataTable = forwardRef<HTMLTableElement, DataTableProps>(
|
|
|
148
187
|
const [tableRef, setTableRef] = useState<HTMLTableElement | null>(null);
|
|
149
188
|
const mergedRef = useMergeRefs(forwardedRef, setTableRef);
|
|
150
189
|
|
|
151
|
-
const {
|
|
190
|
+
const { tabIndex } = useTableKeyboardNav(tableRef, {
|
|
152
191
|
enabled: withKeyboardNav,
|
|
192
|
+
shouldBlockNavigation,
|
|
153
193
|
});
|
|
154
194
|
|
|
155
195
|
return (
|
|
@@ -158,12 +198,12 @@ const DataTable = forwardRef<HTMLTableElement, DataTableProps>(
|
|
|
158
198
|
<table
|
|
159
199
|
{...rest}
|
|
160
200
|
ref={mergedRef}
|
|
161
|
-
className={cl("aksel-data-table", className
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
})}
|
|
201
|
+
className={cl("aksel-data-table", className)}
|
|
202
|
+
data-zebra-stripes={zebraStripes}
|
|
203
|
+
data-truncate-content={truncateContent}
|
|
165
204
|
data-density={rowDensity}
|
|
166
|
-
|
|
205
|
+
data-layout={layout}
|
|
206
|
+
tabIndex={tabIndex}
|
|
167
207
|
/>
|
|
168
208
|
</div>
|
|
169
209
|
</div>
|
|
@@ -53,46 +53,54 @@ function useTableKeyboardNav(
|
|
|
53
53
|
const { grid, positions } = getTableGrid(tableRef);
|
|
54
54
|
const currentPos = positions.get(currentCell);
|
|
55
55
|
|
|
56
|
-
if (
|
|
57
|
-
!currentPos &&
|
|
58
|
-
action.type !== "tableStart" &&
|
|
59
|
-
action.type !== "tableEnd"
|
|
60
|
-
) {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
56
|
let nextCell: Element | null = null;
|
|
65
57
|
|
|
66
58
|
switch (action.type) {
|
|
67
|
-
case "delta":
|
|
59
|
+
case "delta": {
|
|
60
|
+
if (!currentPos) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
68
63
|
nextCell = findNextFocusableCell(
|
|
69
64
|
grid,
|
|
70
|
-
currentPos
|
|
65
|
+
currentPos,
|
|
71
66
|
action.delta,
|
|
72
67
|
currentCell,
|
|
73
68
|
);
|
|
74
69
|
break;
|
|
70
|
+
}
|
|
75
71
|
|
|
76
|
-
case "home":
|
|
77
|
-
|
|
72
|
+
case "home": {
|
|
73
|
+
if (!currentPos) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
nextCell = findFirstCellInRow(grid, currentPos.y);
|
|
78
77
|
break;
|
|
78
|
+
}
|
|
79
79
|
|
|
80
|
-
case "end":
|
|
81
|
-
|
|
80
|
+
case "end": {
|
|
81
|
+
if (!currentPos) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
nextCell = findLastCellInRow(grid, currentPos.y);
|
|
82
85
|
break;
|
|
86
|
+
}
|
|
83
87
|
|
|
84
|
-
case "tableStart":
|
|
85
|
-
nextCell = findFirstCell(grid
|
|
88
|
+
case "tableStart": {
|
|
89
|
+
nextCell = findFirstCell(grid);
|
|
86
90
|
break;
|
|
91
|
+
}
|
|
87
92
|
|
|
88
|
-
case "tableEnd":
|
|
89
|
-
nextCell = findLastCell(grid
|
|
93
|
+
case "tableEnd": {
|
|
94
|
+
nextCell = findLastCell(grid);
|
|
90
95
|
break;
|
|
96
|
+
}
|
|
91
97
|
}
|
|
92
98
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
if (!nextCell || nextCell === currentCell) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return focusCellAndUpdateTabIndex(nextCell, currentCell);
|
|
96
104
|
},
|
|
97
105
|
);
|
|
98
106
|
|
|
@@ -101,7 +109,11 @@ function useTableKeyboardNav(
|
|
|
101
109
|
* Checks if navigation should be blocked based on current focus context.
|
|
102
110
|
*/
|
|
103
111
|
const handleTableKeyDown = useEventCallback((event: KeyboardEvent): void => {
|
|
104
|
-
if (
|
|
112
|
+
if (customBlockFn?.(event)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (shouldBlockNavigation(event)) {
|
|
105
117
|
return;
|
|
106
118
|
}
|
|
107
119
|
|
|
@@ -159,7 +171,7 @@ function useTableKeyboardNav(
|
|
|
159
171
|
|
|
160
172
|
return {
|
|
161
173
|
/* Table should only have tabIndex until the focus is moved inside and is enabled */
|
|
162
|
-
|
|
174
|
+
tabIndex: enabled ? (activeCell ? undefined : 0) : undefined,
|
|
163
175
|
};
|
|
164
176
|
}
|
|
165
177
|
|
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
import React, { forwardRef } from "react";
|
|
2
2
|
import { cl } from "../../../utils/helpers";
|
|
3
3
|
|
|
4
|
-
interface DataTableTdProps extends React.
|
|
4
|
+
interface DataTableTdProps extends React.TdHTMLAttributes<HTMLTableCellElement> {
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Sets a max-width on the content wrapper div inside the cell.
|
|
7
|
+
* This is only needed when using `layout="auto"` together with
|
|
8
|
+
* `truncateContent` on `<DataTable>` and you want the cell to be truncated.
|
|
7
9
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
contentMaxWidth?: number | `${number}${string}`;
|
|
11
|
+
/**
|
|
12
|
+
* TODO: Consider a prop like this instead of contentMaxWidth to use together with layout=auto.
|
|
13
|
+
* Maybe even with layout=auto as a way to override truncateContent on single cells?
|
|
14
|
+
* Need to work on the name though. Or maybe have two separate props?
|
|
15
|
+
*/
|
|
16
|
+
//textWrap?: boolean | number | `${number}${string}`;
|
|
10
17
|
}
|
|
11
18
|
|
|
12
19
|
const DataTableTd = forwardRef<HTMLTableCellElement, DataTableTdProps>(
|
|
13
|
-
({ className, children, ...rest }, forwardedRef) => {
|
|
20
|
+
({ className, children, contentMaxWidth, ...rest }, forwardedRef) => {
|
|
14
21
|
return (
|
|
15
22
|
<td
|
|
16
23
|
{...rest}
|
|
17
24
|
ref={forwardedRef}
|
|
18
25
|
className={cl("aksel-data-table__td", className)}
|
|
19
26
|
>
|
|
20
|
-
<div>{children}</div>
|
|
27
|
+
<div style={{ maxWidth: contentMaxWidth }}>{children}</div>
|
|
21
28
|
</td>
|
|
22
29
|
);
|
|
23
30
|
},
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React, { forwardRef } from "react";
|
|
2
|
+
import { Box } from "../../primitives/box";
|
|
3
|
+
import { VStack } from "../../primitives/stack";
|
|
4
|
+
import { Label } from "../../typography";
|
|
5
|
+
import type { AutoCompleteOption, OptionGroup } from "./AutoSuggest.types";
|
|
6
|
+
|
|
7
|
+
interface AutoSuggestProps {
|
|
8
|
+
options: OptionGroup<AutoCompleteOption>[];
|
|
9
|
+
onSelect: (value: string) => void;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const AutoSuggest = forwardRef<HTMLDivElement, AutoSuggestProps>(
|
|
14
|
+
({ options, onSelect }, ref) => {
|
|
15
|
+
return (
|
|
16
|
+
<Box ref={ref} padding="space-6">
|
|
17
|
+
{options.map((group) => (
|
|
18
|
+
<div key={group.label}>
|
|
19
|
+
<Label as="div">{group.label}</Label>
|
|
20
|
+
<VStack gap="space-4">
|
|
21
|
+
{group.options.map((option) => {
|
|
22
|
+
return (
|
|
23
|
+
<div key={option.value}>
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
onClick={() =>
|
|
27
|
+
/* @ts-expect-error TODO: We need to convert the data properly */
|
|
28
|
+
onSelect(option.value ?? option.propertyKey)
|
|
29
|
+
}
|
|
30
|
+
>
|
|
31
|
+
<span>
|
|
32
|
+
{/* @ts-expect-error TODO: We need to convert the data properly */}
|
|
33
|
+
{option.value ?? option.label ?? option.propertyLabel}
|
|
34
|
+
</span>
|
|
35
|
+
{option.description && <span>{option.description}</span>}
|
|
36
|
+
{option.tags && option.tags.length > 0 && (
|
|
37
|
+
<div>
|
|
38
|
+
{option.tags.map((tag) => (
|
|
39
|
+
<span key={tag}>{tag}</span>
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
})}
|
|
47
|
+
</VStack>
|
|
48
|
+
</div>
|
|
49
|
+
))}
|
|
50
|
+
</Box>
|
|
51
|
+
);
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
export { AutoSuggest };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface OptionGroup<T> {
|
|
2
|
+
label: string;
|
|
3
|
+
options: T[];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface AutoCompleteOption {
|
|
7
|
+
value: string;
|
|
8
|
+
label: string;
|
|
9
|
+
tags?: string[];
|
|
10
|
+
filteringTags?: string[];
|
|
11
|
+
description?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type { AutoCompleteOption, OptionGroup };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import React, { forwardRef, useState } from "react";
|
|
2
|
+
import { Popover } from "../../popover";
|
|
3
|
+
import { cl } from "../../utils/helpers";
|
|
4
|
+
import { AutoSuggest } from "./AutoSuggest";
|
|
5
|
+
import type {
|
|
6
|
+
ParsedOption,
|
|
7
|
+
ParsedProperty,
|
|
8
|
+
QueryFilterQuery,
|
|
9
|
+
QueryFilteringOptions,
|
|
10
|
+
QueryFilteringProperties,
|
|
11
|
+
} from "./TokenFilter.types";
|
|
12
|
+
import { generateAutoCompleteOptions } from "./helpers/generate-autocomplete-options";
|
|
13
|
+
import { parseQueryText } from "./helpers/parse-query-text";
|
|
14
|
+
|
|
15
|
+
type TokenFilterProps = {
|
|
16
|
+
query: QueryFilterQuery;
|
|
17
|
+
onChange: (newQuery: QueryFilterQuery) => void;
|
|
18
|
+
className?: string;
|
|
19
|
+
filteringOptions: QueryFilteringOptions;
|
|
20
|
+
filteringProperties: QueryFilteringProperties;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const TokenFilter = forwardRef<HTMLDivElement, TokenFilterProps>(
|
|
24
|
+
({ query, className, filteringProperties, filteringOptions }, ref) => {
|
|
25
|
+
const [inputAnchor, setInputAnchor] = useState<HTMLInputElement | null>(
|
|
26
|
+
null,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const [filterText, setFilterText] = useState<string>("");
|
|
30
|
+
const { properties, options } = derrivedFilterState(
|
|
31
|
+
filteringProperties,
|
|
32
|
+
filteringOptions,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const queryState = parseQueryText(filterText, properties);
|
|
36
|
+
|
|
37
|
+
const autoCompleteOptions = generateAutoCompleteOptions(
|
|
38
|
+
queryState,
|
|
39
|
+
properties,
|
|
40
|
+
options,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const handleSelectOption = (value: string) => {
|
|
44
|
+
setFilterText(value);
|
|
45
|
+
setCustomOpen(false);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const [customOpen, setCustomOpen] = useState(false);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
ref={ref}
|
|
53
|
+
className={cl("aksel-property-filter", className)}
|
|
54
|
+
role="search"
|
|
55
|
+
>
|
|
56
|
+
<input
|
|
57
|
+
type="text"
|
|
58
|
+
className="aksel-property-filter__input"
|
|
59
|
+
placeholder="Type to filter..."
|
|
60
|
+
ref={setInputAnchor}
|
|
61
|
+
value={filterText}
|
|
62
|
+
onChange={(e) => setFilterText(e.target.value)}
|
|
63
|
+
onFocus={() => setCustomOpen(true)}
|
|
64
|
+
/>
|
|
65
|
+
<Popover
|
|
66
|
+
anchorEl={inputAnchor}
|
|
67
|
+
open={customOpen}
|
|
68
|
+
onClose={() => {
|
|
69
|
+
setFilterText("");
|
|
70
|
+
setCustomOpen(false);
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
<AutoSuggest
|
|
74
|
+
/* @ts-expect-error TODO: handle conversion better */
|
|
75
|
+
options={autoCompleteOptions.options}
|
|
76
|
+
onSelect={handleSelectOption}
|
|
77
|
+
/>
|
|
78
|
+
</Popover>
|
|
79
|
+
{query.tokens.map((token, index) => {
|
|
80
|
+
return (
|
|
81
|
+
<div key={index} className="aksel-property-filter__token">
|
|
82
|
+
<strong>{token.propertyKey}</strong> {token.operator}{" "}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
})}
|
|
86
|
+
<ul>
|
|
87
|
+
{filteringProperties.map((prop) => (
|
|
88
|
+
<li key={prop.key}>{prop.propertyLabel}</li>
|
|
89
|
+
))}
|
|
90
|
+
</ul>
|
|
91
|
+
<pre>{JSON.stringify(queryState, null, 2)}</pre>
|
|
92
|
+
<pre>{JSON.stringify(autoCompleteOptions, null, 2)}</pre>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
function derrivedFilterState(
|
|
99
|
+
filteringProperties: QueryFilteringProperties,
|
|
100
|
+
filteringOptions: QueryFilteringOptions,
|
|
101
|
+
/* query: QueryFilterQuery */
|
|
102
|
+
): {
|
|
103
|
+
properties: ParsedProperty[];
|
|
104
|
+
options: ParsedOption[];
|
|
105
|
+
} {
|
|
106
|
+
const propertyMap = new Map<string, any>();
|
|
107
|
+
|
|
108
|
+
for (const property of filteringProperties) {
|
|
109
|
+
propertyMap.set(property.key, {
|
|
110
|
+
propertyKey: property.key,
|
|
111
|
+
propertyLabel: property?.propertyLabel ?? "",
|
|
112
|
+
groupValuesLabel: property?.groupValuesLabel ?? "",
|
|
113
|
+
propertyGroup: property?.group,
|
|
114
|
+
operators: property?.operators ?? [],
|
|
115
|
+
/* defaultOperator: property?.defaultOperator ?? '=', */
|
|
116
|
+
externalProperty: property,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const internalOptions = filteringOptions.map((option) => ({
|
|
121
|
+
property: propertyMap.get(option.propertyKey) ?? null,
|
|
122
|
+
value: option.value,
|
|
123
|
+
label: option.label ?? option.value ?? "",
|
|
124
|
+
tags: option.tags ?? [],
|
|
125
|
+
filteringTags: option.filteringTags ?? [],
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
return { properties: [...propertyMap.values()], options: internalOptions };
|
|
129
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
type QueryFilterOperator =
|
|
2
|
+
| "<"
|
|
3
|
+
| "<="
|
|
4
|
+
| ">"
|
|
5
|
+
| ">="
|
|
6
|
+
| ":"
|
|
7
|
+
| "!:"
|
|
8
|
+
| "="
|
|
9
|
+
| "!="
|
|
10
|
+
| "^"
|
|
11
|
+
| "!^"
|
|
12
|
+
| (string & {});
|
|
13
|
+
|
|
14
|
+
type QueryFilterOperation = "and" | "or";
|
|
15
|
+
|
|
16
|
+
type QueryFilterToken = {
|
|
17
|
+
propertyKey: string;
|
|
18
|
+
operator: QueryFilterOperator;
|
|
19
|
+
value: any;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type QueryFilterQuery = {
|
|
23
|
+
tokens: QueryFilterToken[];
|
|
24
|
+
operation: QueryFilterOperation;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type QueryFilteringOption = {
|
|
28
|
+
propertyKey: string;
|
|
29
|
+
value: any;
|
|
30
|
+
label?: string;
|
|
31
|
+
tags?: string[];
|
|
32
|
+
filteringTags?: string[];
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type QueryFilteringOptions = QueryFilteringOption[];
|
|
37
|
+
|
|
38
|
+
type QueryFilteringOptionGroup = {
|
|
39
|
+
label: string;
|
|
40
|
+
options: QueryFilteringOptions;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type QueryFilteringScopedOperator =
|
|
44
|
+
| string
|
|
45
|
+
| { operator: string; tokenType: "single" | "multiple" };
|
|
46
|
+
|
|
47
|
+
type QueryFilteringProperty = {
|
|
48
|
+
key: string;
|
|
49
|
+
propertyLabel: string;
|
|
50
|
+
groupValuesLabel?: string;
|
|
51
|
+
group?: string;
|
|
52
|
+
operators?: QueryFilteringScopedOperator[];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type QueryFilteringProperties = QueryFilteringProperty[];
|
|
56
|
+
|
|
57
|
+
type ParsedProperty = {
|
|
58
|
+
propertyKey: string;
|
|
59
|
+
propertyLabel: string;
|
|
60
|
+
groupValuesLabel: string;
|
|
61
|
+
propertyGroup: string;
|
|
62
|
+
operators: QueryFilteringScopedOperator[];
|
|
63
|
+
externalProperty: QueryFilteringProperty;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type ParsedOption = {
|
|
67
|
+
property: ParsedProperty | null;
|
|
68
|
+
value: any;
|
|
69
|
+
label: string;
|
|
70
|
+
tags: string[];
|
|
71
|
+
filteringTags: string[];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type {
|
|
75
|
+
QueryFilterOperator,
|
|
76
|
+
QueryFilterQuery,
|
|
77
|
+
QueryFilteringOptions,
|
|
78
|
+
QueryFilteringProperty,
|
|
79
|
+
QueryFilterOperation,
|
|
80
|
+
QueryFilteringProperties,
|
|
81
|
+
ParsedProperty,
|
|
82
|
+
ParsedOption,
|
|
83
|
+
QueryFilteringOption,
|
|
84
|
+
QueryFilteringOptionGroup,
|
|
85
|
+
};
|