@navikt/ds-react 8.5.0 → 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/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 +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 +1 -1
- package/cjs/data/table/root/useTableKeyboardNav.js +32 -19
- 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/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 +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 +1 -1
- package/esm/data/table/root/useTableKeyboardNav.js +32 -19
- 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/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 +21 -10
- package/src/data/table/root/useTableKeyboardNav.ts +35 -23
- 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/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;
|
|
@@ -25,17 +25,27 @@ import { useTableKeyboardNav } from "./useTableKeyboardNav";
|
|
|
25
25
|
interface DataTableProps extends React.HTMLAttributes<HTMLTableElement> {
|
|
26
26
|
children: React.ReactNode;
|
|
27
27
|
rowDensity?: "condensed" | "normal" | "spacious";
|
|
28
|
+
/**
|
|
29
|
+
* Zebra striped table
|
|
30
|
+
* @default false
|
|
31
|
+
*/
|
|
32
|
+
zebraStripes?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Truncate content in cells and show ellipsis for overflowed text.
|
|
35
|
+
* @default true
|
|
36
|
+
*/
|
|
37
|
+
truncateContent?: boolean;
|
|
28
38
|
/**
|
|
29
39
|
* Enables keyboard navigation for table rows and cells.
|
|
30
40
|
* @default false
|
|
31
41
|
*/
|
|
32
42
|
withKeyboardNav?: boolean;
|
|
33
43
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
44
|
+
* Custom callback to determine if navigation should be blocked.
|
|
45
|
+
* Called before default blocking logic.
|
|
46
|
+
* Requires `withKeyboardNav` to be `true`.
|
|
36
47
|
*/
|
|
37
|
-
|
|
38
|
-
truncateContent?: boolean;
|
|
48
|
+
shouldBlockNavigation?: (event: KeyboardEvent) => boolean;
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
interface DataTableRootComponent extends React.ForwardRefExoticComponent<
|
|
@@ -141,6 +151,7 @@ const DataTable = forwardRef<HTMLTableElement, DataTableProps>(
|
|
|
141
151
|
withKeyboardNav = false,
|
|
142
152
|
zebraStripes = false,
|
|
143
153
|
truncateContent = true,
|
|
154
|
+
shouldBlockNavigation,
|
|
144
155
|
...rest
|
|
145
156
|
},
|
|
146
157
|
forwardedRef,
|
|
@@ -148,8 +159,9 @@ const DataTable = forwardRef<HTMLTableElement, DataTableProps>(
|
|
|
148
159
|
const [tableRef, setTableRef] = useState<HTMLTableElement | null>(null);
|
|
149
160
|
const mergedRef = useMergeRefs(forwardedRef, setTableRef);
|
|
150
161
|
|
|
151
|
-
const {
|
|
162
|
+
const { tabIndex } = useTableKeyboardNav(tableRef, {
|
|
152
163
|
enabled: withKeyboardNav,
|
|
164
|
+
shouldBlockNavigation,
|
|
153
165
|
});
|
|
154
166
|
|
|
155
167
|
return (
|
|
@@ -158,12 +170,11 @@ const DataTable = forwardRef<HTMLTableElement, DataTableProps>(
|
|
|
158
170
|
<table
|
|
159
171
|
{...rest}
|
|
160
172
|
ref={mergedRef}
|
|
161
|
-
className={cl("aksel-data-table", className
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
})}
|
|
173
|
+
className={cl("aksel-data-table", className)}
|
|
174
|
+
data-zebra-stripes={zebraStripes}
|
|
175
|
+
data-truncate-content={truncateContent}
|
|
165
176
|
data-density={rowDensity}
|
|
166
|
-
tabIndex={
|
|
177
|
+
tabIndex={tabIndex}
|
|
167
178
|
/>
|
|
168
179
|
</div>
|
|
169
180
|
</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
|
|
|
@@ -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
|
+
};
|