@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.
Files changed (78) hide show
  1. package/cjs/data/table/helpers/table-grid-nav.d.ts +9 -15
  2. package/cjs/data/table/helpers/table-grid-nav.js +18 -25
  3. package/cjs/data/table/helpers/table-grid-nav.js.map +1 -1
  4. package/cjs/data/table/helpers/table-keyboard.d.ts +1 -1
  5. package/cjs/data/table/helpers/table-keyboard.js +1 -6
  6. package/cjs/data/table/helpers/table-keyboard.js.map +1 -1
  7. package/cjs/data/table/root/DataTableRoot.d.ts +14 -4
  8. package/cjs/data/table/root/DataTableRoot.js +4 -6
  9. package/cjs/data/table/root/DataTableRoot.js.map +1 -1
  10. package/cjs/data/table/root/useTableKeyboardNav.d.ts +1 -1
  11. package/cjs/data/table/root/useTableKeyboardNav.js +32 -19
  12. package/cjs/data/table/root/useTableKeyboardNav.js.map +1 -1
  13. package/cjs/data/token-filter/AutoSuggest.d.ts +21 -0
  14. package/cjs/data/token-filter/AutoSuggest.js +129 -0
  15. package/cjs/data/token-filter/AutoSuggest.js.map +1 -0
  16. package/cjs/data/token-filter/TokenFilter.d.ts +11 -0
  17. package/cjs/data/token-filter/TokenFilter.js +91 -0
  18. package/cjs/data/token-filter/TokenFilter.js.map +1 -0
  19. package/cjs/data/token-filter/TokenFilter.types.d.ts +46 -0
  20. package/cjs/data/token-filter/TokenFilter.types.js +3 -0
  21. package/cjs/data/token-filter/TokenFilter.types.js.map +1 -0
  22. package/cjs/data/token-filter/helpers/generate-autocomplete-options.d.ts +70 -0
  23. package/cjs/data/token-filter/helpers/generate-autocomplete-options.js +171 -0
  24. package/cjs/data/token-filter/helpers/generate-autocomplete-options.js.map +1 -0
  25. package/cjs/data/token-filter/helpers/parse-query-text.d.ts +31 -0
  26. package/cjs/data/token-filter/helpers/parse-query-text.js +91 -0
  27. package/cjs/data/token-filter/helpers/parse-query-text.js.map +1 -0
  28. package/cjs/tooltip/Tooltip.js +1 -1
  29. package/cjs/tooltip/Tooltip.js.map +1 -1
  30. package/cjs/utils/i18n/locales/nb.d.ts +75 -154
  31. package/cjs/utils/i18n/locales/nb.js +75 -154
  32. package/cjs/utils/i18n/locales/nb.js.map +1 -1
  33. package/esm/data/table/helpers/table-grid-nav.d.ts +9 -15
  34. package/esm/data/table/helpers/table-grid-nav.js +18 -25
  35. package/esm/data/table/helpers/table-grid-nav.js.map +1 -1
  36. package/esm/data/table/helpers/table-keyboard.d.ts +1 -1
  37. package/esm/data/table/helpers/table-keyboard.js +1 -6
  38. package/esm/data/table/helpers/table-keyboard.js.map +1 -1
  39. package/esm/data/table/root/DataTableRoot.d.ts +14 -4
  40. package/esm/data/table/root/DataTableRoot.js +4 -6
  41. package/esm/data/table/root/DataTableRoot.js.map +1 -1
  42. package/esm/data/table/root/useTableKeyboardNav.d.ts +1 -1
  43. package/esm/data/table/root/useTableKeyboardNav.js +32 -19
  44. package/esm/data/table/root/useTableKeyboardNav.js.map +1 -1
  45. package/esm/data/token-filter/AutoSuggest.d.ts +21 -0
  46. package/esm/data/token-filter/AutoSuggest.js +93 -0
  47. package/esm/data/token-filter/AutoSuggest.js.map +1 -0
  48. package/esm/data/token-filter/TokenFilter.d.ts +11 -0
  49. package/esm/data/token-filter/TokenFilter.js +55 -0
  50. package/esm/data/token-filter/TokenFilter.js.map +1 -0
  51. package/esm/data/token-filter/TokenFilter.types.d.ts +46 -0
  52. package/esm/data/token-filter/TokenFilter.types.js +2 -0
  53. package/esm/data/token-filter/TokenFilter.types.js.map +1 -0
  54. package/esm/data/token-filter/helpers/generate-autocomplete-options.d.ts +70 -0
  55. package/esm/data/token-filter/helpers/generate-autocomplete-options.js +169 -0
  56. package/esm/data/token-filter/helpers/generate-autocomplete-options.js.map +1 -0
  57. package/esm/data/token-filter/helpers/parse-query-text.d.ts +31 -0
  58. package/esm/data/token-filter/helpers/parse-query-text.js +87 -0
  59. package/esm/data/token-filter/helpers/parse-query-text.js.map +1 -0
  60. package/esm/tooltip/Tooltip.js +2 -2
  61. package/esm/tooltip/Tooltip.js.map +1 -1
  62. package/esm/utils/i18n/locales/nb.d.ts +75 -154
  63. package/esm/utils/i18n/locales/nb.js +75 -154
  64. package/esm/utils/i18n/locales/nb.js.map +1 -1
  65. package/package.json +3 -3
  66. package/src/data/table/helpers/table-grid-nav.test.ts +659 -0
  67. package/src/data/table/helpers/table-grid-nav.ts +19 -38
  68. package/src/data/table/helpers/table-keyboard.ts +1 -10
  69. package/src/data/table/root/DataTableRoot.tsx +21 -10
  70. package/src/data/table/root/useTableKeyboardNav.ts +35 -23
  71. package/src/data/token-filter/AutoSuggest.tsx +179 -0
  72. package/src/data/token-filter/TokenFilter.tsx +124 -0
  73. package/src/data/token-filter/TokenFilter.types.ts +79 -0
  74. package/src/data/token-filter/helpers/generate-autocomplete-options.ts +244 -0
  75. package/src/data/token-filter/helpers/parse-query-text.test.ts +410 -0
  76. package/src/data/token-filter/helpers/parse-query-text.ts +148 -0
  77. package/src/tooltip/Tooltip.tsx +3 -3
  78. 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 (not the same as current cell and contains focusable elements).
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: Element | undefined,
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 or are the same as the current cell.
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
- if (isCellFocusable(cell, currentCell)) {
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 same row as the current position.
129
+ * Finds the first focusable cell in the given row.
131
130
  */
132
131
  function findFirstCellInRow(
133
132
  grid: (Element | undefined)[][],
134
- positions: Map<Element, { x: number; y: number }>,
135
- currentCell: Element,
133
+ rowIndex: number,
136
134
  ): Element | null {
137
- const currentPos = positions.get(currentCell);
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, currentCell)) {
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 same row as the current position.
147
+ * Finds the last focusable cell in the given row.
155
148
  */
156
149
  function findLastCellInRow(
157
150
  grid: (Element | undefined)[][],
158
- positions: Map<Element, { x: number; y: number }>,
159
- currentCell: Element,
151
+ rowIndex: number,
160
152
  ): Element | null {
161
- const currentPos = positions.get(currentCell);
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, currentCell)) {
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, currentCell)) {
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, currentCell)) {
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
- * Zebra striped table
35
- * @default false
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
- zebraStripes?: boolean;
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 { tableTabIndex } = useTableKeyboardNav(tableRef, {
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
- "aksel-data-table--zebra-stripes": zebraStripes,
163
- "aksel-data-table--truncate-content": truncateContent,
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={tableTabIndex}
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
- nextCell = findFirstCellInRow(grid, positions, currentCell);
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
- nextCell = findLastCellInRow(grid, positions, currentCell);
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, currentCell);
88
+ case "tableStart": {
89
+ nextCell = findFirstCell(grid);
86
90
  break;
91
+ }
87
92
 
88
- case "tableEnd":
89
- nextCell = findLastCell(grid, currentCell);
93
+ case "tableEnd": {
94
+ nextCell = findLastCell(grid);
90
95
  break;
96
+ }
91
97
  }
92
98
 
93
- return nextCell
94
- ? focusCellAndUpdateTabIndex(nextCell, currentCell)
95
- : null;
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 (shouldBlockNavigation(event, customBlockFn)) {
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
- tableTabIndex: enabled ? (activeCell ? undefined : 0) : undefined,
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
+ };