@navikt/ds-react 8.4.1 → 8.5.1
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/accordion/Accordion.d.ts +10 -0
- package/cjs/accordion/Accordion.js +2 -2
- package/cjs/accordion/Accordion.js.map +1 -1
- package/cjs/data/table/helpers/table-cell.d.ts +2 -2
- package/cjs/data/table/helpers/table-cell.js +2 -5
- package/cjs/data/table/helpers/table-cell.js.map +1 -1
- package/cjs/data/table/helpers/table-focus.d.ts +26 -2
- package/cjs/data/table/helpers/table-focus.js +60 -9
- package/cjs/data/table/helpers/table-focus.js.map +1 -1
- package/cjs/data/table/helpers/table-grid-nav.d.ts +40 -10
- package/cjs/data/table/helpers/table-grid-nav.js +102 -25
- package/cjs/data/table/helpers/table-grid-nav.js.map +1 -1
- package/cjs/data/table/helpers/table-keyboard.d.ts +24 -3
- package/cjs/data/table/helpers/table-keyboard.js +25 -5
- package/cjs/data/table/helpers/table-keyboard.js.map +1 -1
- package/cjs/data/table/hooks/useGridCache.d.ts +17 -0
- package/cjs/data/table/hooks/useGridCache.js +65 -0
- package/cjs/data/table/hooks/useGridCache.js.map +1 -0
- package/cjs/data/table/root/DataTableRoot.d.ts +14 -4
- package/cjs/data/table/root/DataTableRoot.js +4 -6
- package/cjs/data/table/root/DataTableRoot.js.map +1 -1
- package/cjs/data/table/root/useTableKeyboardNav.d.ts +10 -4
- package/cjs/data/table/root/useTableKeyboardNav.js +70 -99
- package/cjs/data/table/root/useTableKeyboardNav.js.map +1 -1
- package/cjs/data/token-filter/AutoSuggest.d.ts +21 -0
- package/cjs/data/token-filter/AutoSuggest.js +129 -0
- package/cjs/data/token-filter/AutoSuggest.js.map +1 -0
- package/cjs/data/token-filter/TokenFilter.d.ts +11 -0
- package/cjs/data/token-filter/TokenFilter.js +91 -0
- package/cjs/data/token-filter/TokenFilter.js.map +1 -0
- package/cjs/data/token-filter/TokenFilter.types.d.ts +46 -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 +70 -0
- package/cjs/data/token-filter/helpers/generate-autocomplete-options.js +171 -0
- package/cjs/data/token-filter/helpers/generate-autocomplete-options.js.map +1 -0
- package/cjs/data/token-filter/helpers/parse-query-text.d.ts +31 -0
- package/cjs/data/token-filter/helpers/parse-query-text.js +91 -0
- package/cjs/data/token-filter/helpers/parse-query-text.js.map +1 -0
- package/cjs/link-card/LinkCard.d.ts +13 -0
- package/cjs/link-card/LinkCard.js +2 -2
- package/cjs/link-card/LinkCard.js.map +1 -1
- package/cjs/process/Process.d.ts +1 -1
- package/cjs/tooltip/Tooltip.js +1 -1
- package/cjs/tooltip/Tooltip.js.map +1 -1
- package/esm/accordion/Accordion.d.ts +10 -0
- package/esm/accordion/Accordion.js +2 -2
- package/esm/accordion/Accordion.js.map +1 -1
- package/esm/data/table/helpers/table-cell.d.ts +2 -2
- package/esm/data/table/helpers/table-cell.js +2 -5
- package/esm/data/table/helpers/table-cell.js.map +1 -1
- package/esm/data/table/helpers/table-focus.d.ts +26 -2
- package/esm/data/table/helpers/table-focus.js +55 -9
- package/esm/data/table/helpers/table-focus.js.map +1 -1
- package/esm/data/table/helpers/table-grid-nav.d.ts +40 -10
- package/esm/data/table/helpers/table-grid-nav.js +96 -24
- package/esm/data/table/helpers/table-grid-nav.js.map +1 -1
- package/esm/data/table/helpers/table-keyboard.d.ts +24 -3
- package/esm/data/table/helpers/table-keyboard.js +24 -4
- package/esm/data/table/helpers/table-keyboard.js.map +1 -1
- package/esm/data/table/hooks/useGridCache.d.ts +17 -0
- package/esm/data/table/hooks/useGridCache.js +63 -0
- package/esm/data/table/hooks/useGridCache.js.map +1 -0
- package/esm/data/table/root/DataTableRoot.d.ts +14 -4
- package/esm/data/table/root/DataTableRoot.js +4 -6
- package/esm/data/table/root/DataTableRoot.js.map +1 -1
- package/esm/data/table/root/useTableKeyboardNav.d.ts +10 -4
- package/esm/data/table/root/useTableKeyboardNav.js +75 -104
- package/esm/data/table/root/useTableKeyboardNav.js.map +1 -1
- package/esm/data/token-filter/AutoSuggest.d.ts +21 -0
- package/esm/data/token-filter/AutoSuggest.js +93 -0
- package/esm/data/token-filter/AutoSuggest.js.map +1 -0
- package/esm/data/token-filter/TokenFilter.d.ts +11 -0
- package/esm/data/token-filter/TokenFilter.js +55 -0
- package/esm/data/token-filter/TokenFilter.js.map +1 -0
- package/esm/data/token-filter/TokenFilter.types.d.ts +46 -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 +70 -0
- package/esm/data/token-filter/helpers/generate-autocomplete-options.js +169 -0
- package/esm/data/token-filter/helpers/generate-autocomplete-options.js.map +1 -0
- package/esm/data/token-filter/helpers/parse-query-text.d.ts +31 -0
- package/esm/data/token-filter/helpers/parse-query-text.js +87 -0
- package/esm/data/token-filter/helpers/parse-query-text.js.map +1 -0
- package/esm/link-card/LinkCard.d.ts +13 -0
- package/esm/link-card/LinkCard.js +2 -2
- package/esm/link-card/LinkCard.js.map +1 -1
- package/esm/process/Process.d.ts +1 -1
- package/esm/tooltip/Tooltip.js +2 -2
- package/esm/tooltip/Tooltip.js.map +1 -1
- package/package.json +3 -3
- package/src/accordion/Accordion.tsx +19 -2
- package/src/data/table/helpers/table-cell.ts +2 -7
- package/src/data/table/helpers/table-focus.ts +70 -9
- package/src/data/table/helpers/table-grid-nav.test.ts +659 -0
- package/src/data/table/helpers/table-grid-nav.ts +128 -32
- package/src/data/table/helpers/table-keyboard.test.ts +27 -27
- package/src/data/table/helpers/table-keyboard.ts +34 -4
- package/src/data/table/hooks/useGridCache.ts +73 -0
- package/src/data/table/root/DataTableRoot.tsx +21 -11
- package/src/data/table/root/useTableKeyboardNav.ts +110 -128
- package/src/data/token-filter/AutoSuggest.tsx +179 -0
- package/src/data/token-filter/TokenFilter.tsx +124 -0
- package/src/data/token-filter/TokenFilter.types.ts +79 -0
- package/src/data/token-filter/helpers/generate-autocomplete-options.ts +244 -0
- package/src/data/token-filter/helpers/parse-query-text.test.ts +410 -0
- package/src/data/token-filter/helpers/parse-query-text.ts +148 -0
- package/src/link-card/LinkCard.tsx +15 -1
- package/src/process/Process.tsx +1 -1
- package/src/tooltip/Tooltip.tsx +3 -3
|
@@ -1,90 +1,129 @@
|
|
|
1
|
-
import { useEffect
|
|
1
|
+
import { useEffect } from "react";
|
|
2
2
|
import { useEventCallback } from "../../../utils/hooks";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { focusInitialTableTarget } from "../helpers/table-cell";
|
|
4
|
+
import { focusCellAndUpdateTabIndex } from "../helpers/table-focus";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
findFirstCell,
|
|
7
|
+
findFirstCellInRow,
|
|
8
|
+
findLastCell,
|
|
9
|
+
findLastCellInRow,
|
|
10
|
+
findNextFocusableCell,
|
|
9
11
|
} from "../helpers/table-grid-nav";
|
|
10
12
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
type NavigationAction,
|
|
14
|
+
getNavigationAction,
|
|
15
|
+
shouldBlockNavigation,
|
|
13
16
|
} from "../helpers/table-keyboard";
|
|
17
|
+
import { useGridCache } from "../hooks/useGridCache";
|
|
18
|
+
|
|
19
|
+
type UseTableKeyboardNavOptions = {
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Custom callback to determine if navigation should be blocked.
|
|
23
|
+
* Called before default blocking logic.
|
|
24
|
+
*/
|
|
25
|
+
shouldBlockNavigation?: (event: KeyboardEvent) => boolean;
|
|
26
|
+
};
|
|
14
27
|
|
|
15
28
|
function useTableKeyboardNav(
|
|
16
29
|
tableRef: HTMLTableElement | null,
|
|
17
|
-
{ enabled
|
|
30
|
+
{ enabled, shouldBlockNavigation: customBlockFn }: UseTableKeyboardNavOptions,
|
|
18
31
|
) {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const observerRef = useRef<MutationObserver | null>(null);
|
|
24
|
-
|
|
25
|
-
const gridCacheRef = useRef<GridCache>({
|
|
26
|
-
grid: null,
|
|
27
|
-
dirty: true,
|
|
28
|
-
});
|
|
32
|
+
const { getTableGrid, activeCell, setActiveCell } = useGridCache(
|
|
33
|
+
tableRef,
|
|
34
|
+
enabled,
|
|
35
|
+
);
|
|
29
36
|
|
|
30
37
|
/**
|
|
31
|
-
*
|
|
32
|
-
* - Save original tabIndex of cells and restore when navigating away?
|
|
38
|
+
* Executes a navigation action and returns the target cell.
|
|
33
39
|
*/
|
|
34
|
-
const
|
|
35
|
-
(
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
const executeNavigationAction = useEventCallback(
|
|
41
|
+
(action: NavigationAction) => {
|
|
42
|
+
if (!tableRef) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let currentCell = activeCell;
|
|
47
|
+
currentCell ??= focusInitialTableTarget(tableRef);
|
|
48
|
+
|
|
49
|
+
if (!currentCell) {
|
|
38
50
|
return null;
|
|
39
51
|
}
|
|
40
52
|
|
|
41
|
-
const { grid, positions
|
|
42
|
-
tableRef,
|
|
43
|
-
gridCacheRef.current,
|
|
44
|
-
);
|
|
53
|
+
const { grid, positions } = getTableGrid(tableRef);
|
|
45
54
|
const currentPos = positions.get(currentCell);
|
|
46
55
|
|
|
47
|
-
|
|
56
|
+
let nextCell: Element | null = null;
|
|
57
|
+
|
|
58
|
+
switch (action.type) {
|
|
59
|
+
case "delta": {
|
|
60
|
+
if (!currentPos) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
nextCell = findNextFocusableCell(
|
|
64
|
+
grid,
|
|
65
|
+
currentPos,
|
|
66
|
+
action.delta,
|
|
67
|
+
currentCell,
|
|
68
|
+
);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case "home": {
|
|
73
|
+
if (!currentPos) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
nextCell = findFirstCellInRow(grid, currentPos.y);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
case "end": {
|
|
81
|
+
if (!currentPos) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
nextCell = findLastCellInRow(grid, currentPos.y);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case "tableStart": {
|
|
89
|
+
nextCell = findFirstCell(grid);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case "tableEnd": {
|
|
94
|
+
nextCell = findLastCell(grid);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!nextCell || nextCell === currentCell) {
|
|
48
100
|
return null;
|
|
49
101
|
}
|
|
50
102
|
|
|
51
|
-
|
|
52
|
-
grid,
|
|
53
|
-
currentPos,
|
|
54
|
-
delta,
|
|
55
|
-
currentCell,
|
|
56
|
-
maxCols,
|
|
57
|
-
);
|
|
58
|
-
return nextCell
|
|
59
|
-
? focusCellAndUpdateTabIndex(nextCell, currentCell)
|
|
60
|
-
: null;
|
|
103
|
+
return focusCellAndUpdateTabIndex(nextCell, currentCell);
|
|
61
104
|
},
|
|
62
105
|
);
|
|
63
106
|
|
|
64
107
|
/**
|
|
65
|
-
* Handles keyboard navigation with arrow keys.
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* TODO:
|
|
69
|
-
* - Check for other "blocking" scenarios, like actionmenus, dropdown etc
|
|
70
|
-
* - Consider having acallback user can hook into to determine if navigation should be blocked
|
|
71
|
-
* - Consider adding Home, End, PageUp, PageDown navigation
|
|
72
|
-
*
|
|
108
|
+
* Handles keyboard navigation with arrow keys, Home/End, and PageUp/PageDown.
|
|
109
|
+
* Checks if navigation should be blocked based on current focus context.
|
|
73
110
|
*/
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
if (shouldBlockArrowKeyNavigation(event)) {
|
|
111
|
+
const handleTableKeyDown = useEventCallback((event: KeyboardEvent): void => {
|
|
112
|
+
if (customBlockFn?.(event)) {
|
|
77
113
|
return;
|
|
78
114
|
}
|
|
79
115
|
|
|
80
|
-
|
|
116
|
+
if (shouldBlockNavigation(event)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
81
119
|
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
newCell = navigateByArrowKey(delta);
|
|
120
|
+
const action = getNavigationAction(event);
|
|
121
|
+
if (!action) {
|
|
122
|
+
return;
|
|
86
123
|
}
|
|
87
124
|
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
const newCell = executeNavigationAction(action);
|
|
88
127
|
newCell && setActiveCell(newCell);
|
|
89
128
|
});
|
|
90
129
|
|
|
@@ -92,8 +131,14 @@ function useTableKeyboardNav(
|
|
|
92
131
|
* When focus is moved to elements inside a cell like inputs, checkbox etc
|
|
93
132
|
* we want to update the active cell to the parent td/th, so that keyboard navigation continues to work as expected from there.
|
|
94
133
|
*/
|
|
95
|
-
const
|
|
134
|
+
const handleTableFocusIn = useEventCallback((event: FocusEvent): void => {
|
|
96
135
|
const target = event.target as Element | null;
|
|
136
|
+
|
|
137
|
+
if (tableRef && target === tableRef) {
|
|
138
|
+
focusInitialTableTarget(tableRef);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
97
142
|
const newCell = target?.closest("td, th") ?? null;
|
|
98
143
|
if (!newCell || newCell === activeCell) {
|
|
99
144
|
return;
|
|
@@ -108,90 +153,27 @@ function useTableKeyboardNav(
|
|
|
108
153
|
});
|
|
109
154
|
|
|
110
155
|
/**
|
|
111
|
-
*
|
|
112
|
-
* - We want to check if elements are removed/added, like when filtering table, pagination etc
|
|
113
|
-
* - Changes in colspan/rowspan that can affect the grid structure
|
|
114
|
-
* - Hidden attribute or styles that can affect focusability of cells
|
|
115
|
-
*
|
|
116
|
-
* We also check if the active cell is removed from the DOM, and clear it if so.
|
|
156
|
+
* Attach event listeners for keyboard navigation and focus management.
|
|
117
157
|
*/
|
|
118
158
|
useEffect(() => {
|
|
119
159
|
if (!tableRef || !enabled) {
|
|
120
160
|
return;
|
|
121
161
|
}
|
|
122
162
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (activeCellRef.current && !activeCellRef.current.isConnected) {
|
|
126
|
-
setActiveCell(null);
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
observerRef.current.observe(tableRef, {
|
|
131
|
-
subtree: true,
|
|
132
|
-
childList: true,
|
|
133
|
-
attributes: true,
|
|
134
|
-
attributeFilter: ["colspan", "rowspan", "hidden", "style"],
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
return () => {
|
|
138
|
-
if (observerRef.current) {
|
|
139
|
-
observerRef.current.disconnect();
|
|
140
|
-
observerRef.current = null;
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
}, [tableRef, enabled]);
|
|
144
|
-
|
|
145
|
-
useEffect(() => {
|
|
146
|
-
if (!tableRef || !enabled) {
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
tableRef.addEventListener("keydown", onKeyDown);
|
|
151
|
-
tableRef.addEventListener("focusin", onFocusIn);
|
|
163
|
+
tableRef.addEventListener("keydown", handleTableKeyDown);
|
|
164
|
+
tableRef.addEventListener("focusin", handleTableFocusIn);
|
|
152
165
|
|
|
153
166
|
return () => {
|
|
154
|
-
tableRef.removeEventListener("keydown",
|
|
155
|
-
tableRef.removeEventListener("focusin",
|
|
167
|
+
tableRef.removeEventListener("keydown", handleTableKeyDown);
|
|
168
|
+
tableRef.removeEventListener("focusin", handleTableFocusIn);
|
|
156
169
|
};
|
|
157
|
-
}, [tableRef,
|
|
158
|
-
|
|
159
|
-
/*
|
|
160
|
-
* If keyboard-nav is re-enabled, we need to make sure to update the grid cache,
|
|
161
|
-
* since the table might have changed while it was disabled.
|
|
162
|
-
*/
|
|
163
|
-
useEffect(() => {
|
|
164
|
-
if (!enabled) {
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
gridCacheRef.current.dirty = true;
|
|
169
|
-
}, [enabled]);
|
|
170
|
+
}, [tableRef, handleTableKeyDown, handleTableFocusIn, enabled]);
|
|
170
171
|
|
|
171
172
|
return {
|
|
172
173
|
/* Table should only have tabIndex until the focus is moved inside and is enabled */
|
|
173
|
-
|
|
174
|
-
/*
|
|
175
|
-
* Allows us to capture focus on the table when navigating with Tab from outside, and move it to the first cell.
|
|
176
|
-
* We only want to do this if no cell is already focused.
|
|
177
|
-
*/
|
|
178
|
-
onFocus: () => {
|
|
179
|
-
if (!tableRef) {
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const focusedElement = document.activeElement;
|
|
184
|
-
const cellInTable = focusedElement?.closest("td, th");
|
|
185
|
-
|
|
186
|
-
/* Assume onFocusIn handler has updates cell */
|
|
187
|
-
if (cellInTable && tableRef.contains(cellInTable)) {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const firstCell = getFirstCell(tableRef);
|
|
192
|
-
return firstCell ? focusCell(firstCell) : null;
|
|
193
|
-
},
|
|
174
|
+
tabIndex: enabled ? (activeCell ? undefined : 0) : undefined,
|
|
194
175
|
};
|
|
195
176
|
}
|
|
196
177
|
|
|
197
178
|
export { useTableKeyboardNav };
|
|
179
|
+
export type { UseTableKeyboardNavOptions };
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import React, { forwardRef, useMemo } from "react";
|
|
2
|
+
import { cl } from "../../utils/helpers";
|
|
3
|
+
import type {
|
|
4
|
+
QueryFilteringOption,
|
|
5
|
+
QueryFilteringOptionGroup,
|
|
6
|
+
} from "./TokenFilter.types";
|
|
7
|
+
|
|
8
|
+
interface AutoSuggestOption {
|
|
9
|
+
value: string;
|
|
10
|
+
label: string;
|
|
11
|
+
tags?: string[];
|
|
12
|
+
filteringTags?: string[];
|
|
13
|
+
description?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface AutoSuggestGroup {
|
|
17
|
+
label: string;
|
|
18
|
+
options: AutoSuggestOption[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface AutoSuggestProps {
|
|
22
|
+
options: AutoSuggestGroup[];
|
|
23
|
+
value: string;
|
|
24
|
+
filterText: string;
|
|
25
|
+
onSelect: (value: string) => void;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const AutoSuggest = forwardRef<HTMLDivElement, AutoSuggestProps>(
|
|
30
|
+
({ options, value, filterText, onSelect, className }, ref) => {
|
|
31
|
+
console.info({ options, value, filterText });
|
|
32
|
+
/* const highlightedText = filterText === undefined ? value : filterText; */
|
|
33
|
+
/* const filterValue = (value || "").toLowerCase();
|
|
34
|
+
|
|
35
|
+
const filteredGroups = options
|
|
36
|
+
.map((group) => ({
|
|
37
|
+
...group,
|
|
38
|
+
options: group.options.filter((option) => {
|
|
39
|
+
const searchableText = [
|
|
40
|
+
option.label,
|
|
41
|
+
option.description,
|
|
42
|
+
...(option.filteringTags ?? []),
|
|
43
|
+
...(option.tags ?? []),
|
|
44
|
+
]
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(" ")
|
|
47
|
+
.toLowerCase();
|
|
48
|
+
return searchableText.includes(filterValue);
|
|
49
|
+
}),
|
|
50
|
+
}))
|
|
51
|
+
.filter((group) => group.options.length > 0); */
|
|
52
|
+
|
|
53
|
+
const { items } = useAutosuggestItems({ options, filterValue: filterText });
|
|
54
|
+
|
|
55
|
+
console.info({ items });
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div ref={ref} className={cl("aksel-auto-suggest", className)}>
|
|
59
|
+
{options.map((group) => (
|
|
60
|
+
<div key={group.label} className="aksel-auto-suggest__group">
|
|
61
|
+
<div className="aksel-auto-suggest__group-label">{group.label}</div>
|
|
62
|
+
<ul className="aksel-auto-suggest__list">
|
|
63
|
+
{group.options.map((option) => (
|
|
64
|
+
<li key={option.value} className="aksel-auto-suggest__item">
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
className="aksel-auto-suggest__button"
|
|
68
|
+
onClick={() => onSelect(option.value)}
|
|
69
|
+
>
|
|
70
|
+
<span className="aksel-auto-suggest__label">
|
|
71
|
+
{option.label}
|
|
72
|
+
</span>
|
|
73
|
+
{option.description && (
|
|
74
|
+
<span className="aksel-auto-suggest__description">
|
|
75
|
+
{option.description}
|
|
76
|
+
</span>
|
|
77
|
+
)}
|
|
78
|
+
{option.tags && option.tags.length > 0 && (
|
|
79
|
+
<div className="aksel-auto-suggest__tags">
|
|
80
|
+
{option.tags.map((tag) => (
|
|
81
|
+
<span key={tag} className="aksel-auto-suggest__tag">
|
|
82
|
+
{tag}
|
|
83
|
+
</span>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</button>
|
|
88
|
+
</li>
|
|
89
|
+
))}
|
|
90
|
+
</ul>
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
function useAutosuggestItems({ options, filterValue }) {
|
|
99
|
+
const { items } = useMemo(() => createItems(options), [options]);
|
|
100
|
+
|
|
101
|
+
const filteredItems = useMemo(() => {
|
|
102
|
+
const localFilteredItems = items;
|
|
103
|
+
if (filterValue) {
|
|
104
|
+
localFilteredItems.unshift({
|
|
105
|
+
value: filterValue,
|
|
106
|
+
type: "use-entered",
|
|
107
|
+
label: `Use "${filterValue}"`,
|
|
108
|
+
option: { value: filterValue },
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return localFilteredItems;
|
|
112
|
+
}, [items, filterValue]);
|
|
113
|
+
|
|
114
|
+
return { items: filteredItems };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* TODO: Need to split autosuggest types and filter types */
|
|
118
|
+
type AutoSuggestItem = {
|
|
119
|
+
type?: "parent" | "child" | "use-entered";
|
|
120
|
+
option: QueryFilteringOption | QueryFilteringOptionGroup | { value: string };
|
|
121
|
+
parent?: QueryFilteringOptionGroup;
|
|
122
|
+
disabled?: boolean;
|
|
123
|
+
value?: string;
|
|
124
|
+
label?: string;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
function createItems(
|
|
128
|
+
options: (QueryFilteringOption | QueryFilteringOptionGroup)[],
|
|
129
|
+
) {
|
|
130
|
+
const items: AutoSuggestItem[] = [];
|
|
131
|
+
const itemToGroup = new WeakMap<AutoSuggestItem, AutoSuggestItem>();
|
|
132
|
+
|
|
133
|
+
for (const option of options) {
|
|
134
|
+
if (isGroup(option)) {
|
|
135
|
+
for (const item of flattenGroup(option, itemToGroup)) {
|
|
136
|
+
items.push(item);
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
items.push({ ...option, option });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { items };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function flattenGroup(
|
|
147
|
+
group: QueryFilteringOptionGroup,
|
|
148
|
+
map: WeakMap<AutoSuggestItem, AutoSuggestItem>,
|
|
149
|
+
) {
|
|
150
|
+
const { options, ...rest } = group;
|
|
151
|
+
|
|
152
|
+
const groupItem: AutoSuggestItem = { ...rest, type: "parent", option: group };
|
|
153
|
+
|
|
154
|
+
const items: AutoSuggestItem[] = [groupItem];
|
|
155
|
+
|
|
156
|
+
for (const option of options) {
|
|
157
|
+
const childOption: AutoSuggestItem = {
|
|
158
|
+
...option,
|
|
159
|
+
type: "child",
|
|
160
|
+
disabled: option.disabled ?? false,
|
|
161
|
+
option,
|
|
162
|
+
parent: group,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
items.push(childOption);
|
|
166
|
+
|
|
167
|
+
map.set(childOption, groupItem);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return items;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isGroup(
|
|
174
|
+
optionOrGroup: QueryFilteringOption | QueryFilteringOptionGroup,
|
|
175
|
+
): optionOrGroup is QueryFilteringOptionGroup {
|
|
176
|
+
return "options" in optionOrGroup;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export { AutoSuggest };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React, { forwardRef, useState } from "react";
|
|
2
|
+
import { Popover } from "../../popover";
|
|
3
|
+
import { cl } from "../../utils/helpers";
|
|
4
|
+
import type {
|
|
5
|
+
ParsedOption,
|
|
6
|
+
ParsedProperty,
|
|
7
|
+
QueryFilterQuery,
|
|
8
|
+
QueryFilteringOptions,
|
|
9
|
+
QueryFilteringProperties,
|
|
10
|
+
} from "./TokenFilter.types";
|
|
11
|
+
import { generateAutoCompleteOptions } from "./helpers/generate-autocomplete-options";
|
|
12
|
+
import { parseQueryText } from "./helpers/parse-query-text";
|
|
13
|
+
|
|
14
|
+
type TokenFilterProps = {
|
|
15
|
+
query: QueryFilterQuery;
|
|
16
|
+
onChange: (newQuery: QueryFilterQuery) => void;
|
|
17
|
+
className?: string;
|
|
18
|
+
filteringOptions: QueryFilteringOptions;
|
|
19
|
+
filteringProperties: QueryFilteringProperties;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const TokenFilter = forwardRef<HTMLDivElement, TokenFilterProps>(
|
|
23
|
+
({ query, className, filteringProperties, filteringOptions }, ref) => {
|
|
24
|
+
const [inputAnchor, setInputAnchor] = useState<HTMLInputElement | null>(
|
|
25
|
+
null,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const [filterText, setFilterText] = useState<string>("");
|
|
29
|
+
const { properties, options } = derrivedFilterState(
|
|
30
|
+
filteringProperties,
|
|
31
|
+
filteringOptions,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const queryState = parseQueryText(filterText, properties);
|
|
35
|
+
|
|
36
|
+
const autoCompleteOptions = generateAutoCompleteOptions(
|
|
37
|
+
queryState,
|
|
38
|
+
properties,
|
|
39
|
+
options,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
/* const handleSelectOption = (value: string) => {
|
|
43
|
+
setFilterText(value);
|
|
44
|
+
}; */
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
ref={ref}
|
|
49
|
+
className={cl("aksel-property-filter", className)}
|
|
50
|
+
role="search"
|
|
51
|
+
>
|
|
52
|
+
<input
|
|
53
|
+
type="text"
|
|
54
|
+
className="aksel-property-filter__input"
|
|
55
|
+
placeholder="Type to filter..."
|
|
56
|
+
ref={setInputAnchor}
|
|
57
|
+
value={filterText}
|
|
58
|
+
onChange={(e) => setFilterText(e.target.value)}
|
|
59
|
+
/>
|
|
60
|
+
<Popover
|
|
61
|
+
anchorEl={inputAnchor}
|
|
62
|
+
open={filterText.length > 0}
|
|
63
|
+
onClose={() => setFilterText("")}
|
|
64
|
+
>
|
|
65
|
+
a
|
|
66
|
+
{/* <AutoSuggest
|
|
67
|
+
|
|
68
|
+
options={autoCompleteOptions.options}
|
|
69
|
+
value={filterText}
|
|
70
|
+
filterText={autoCompleteOptions.value}
|
|
71
|
+
onSelect={handleSelectOption}
|
|
72
|
+
/> */}
|
|
73
|
+
</Popover>
|
|
74
|
+
{query.tokens.map((token, index) => {
|
|
75
|
+
return (
|
|
76
|
+
<div key={index} className="aksel-property-filter__token">
|
|
77
|
+
<strong>{token.propertyKey}</strong> {token.operator}{" "}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
})}
|
|
81
|
+
<ul>
|
|
82
|
+
{filteringProperties.map((prop) => (
|
|
83
|
+
<li key={prop.key}>{prop.propertyLabel}</li>
|
|
84
|
+
))}
|
|
85
|
+
</ul>
|
|
86
|
+
<pre>{JSON.stringify(queryState, null, 2)}</pre>
|
|
87
|
+
<pre>{JSON.stringify(autoCompleteOptions, null, 2)}</pre>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
function derrivedFilterState(
|
|
94
|
+
filteringProperties: QueryFilteringProperties,
|
|
95
|
+
filteringOptions: QueryFilteringOptions,
|
|
96
|
+
/* query: QueryFilterQuery */
|
|
97
|
+
): {
|
|
98
|
+
properties: ParsedProperty[];
|
|
99
|
+
options: ParsedOption[];
|
|
100
|
+
} {
|
|
101
|
+
const propertyMap = new Map<string, any>();
|
|
102
|
+
|
|
103
|
+
for (const property of filteringProperties) {
|
|
104
|
+
propertyMap.set(property.key, {
|
|
105
|
+
propertyKey: property.key,
|
|
106
|
+
propertyLabel: property?.propertyLabel ?? "",
|
|
107
|
+
groupValuesLabel: property?.groupValuesLabel ?? "",
|
|
108
|
+
propertyGroup: property?.group,
|
|
109
|
+
/* operators: (property?.operators ?? []).map(op => (typeof op === 'string' ? op : op.operator)), */
|
|
110
|
+
/* defaultOperator: property?.defaultOperator ?? '=', */
|
|
111
|
+
externalProperty: property,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const internalOptions = filteringOptions.map((option) => ({
|
|
116
|
+
property: propertyMap.get(option.propertyKey) ?? null,
|
|
117
|
+
value: option.value,
|
|
118
|
+
label: option.label ?? option.value ?? "",
|
|
119
|
+
tags: option.tags ?? [],
|
|
120
|
+
filteringTags: option.filteringTags ?? [],
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
return { properties: [...propertyMap.values()], options: internalOptions };
|
|
124
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
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 QueryFilteringProperty = {
|
|
44
|
+
key: string;
|
|
45
|
+
propertyLabel: string;
|
|
46
|
+
groupValuesLabel: string;
|
|
47
|
+
group: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type QueryFilteringProperties = QueryFilteringProperty[];
|
|
51
|
+
|
|
52
|
+
type ParsedProperty = {
|
|
53
|
+
propertyKey: string;
|
|
54
|
+
propertyLabel: string;
|
|
55
|
+
groupValuesLabel: string;
|
|
56
|
+
propertyGroup: string;
|
|
57
|
+
externalProperty: QueryFilteringProperty;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type ParsedOption = {
|
|
61
|
+
property: ParsedProperty | null;
|
|
62
|
+
value: any;
|
|
63
|
+
label: string;
|
|
64
|
+
tags: string[];
|
|
65
|
+
filteringTags: string[];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type {
|
|
69
|
+
QueryFilterOperator,
|
|
70
|
+
QueryFilterQuery,
|
|
71
|
+
QueryFilteringOptions,
|
|
72
|
+
QueryFilteringProperty,
|
|
73
|
+
QueryFilterOperation,
|
|
74
|
+
QueryFilteringProperties,
|
|
75
|
+
ParsedProperty,
|
|
76
|
+
ParsedOption,
|
|
77
|
+
QueryFilteringOption,
|
|
78
|
+
QueryFilteringOptionGroup,
|
|
79
|
+
};
|