@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.
Files changed (110) hide show
  1. package/cjs/accordion/Accordion.d.ts +10 -0
  2. package/cjs/accordion/Accordion.js +2 -2
  3. package/cjs/accordion/Accordion.js.map +1 -1
  4. package/cjs/data/table/helpers/table-cell.d.ts +2 -2
  5. package/cjs/data/table/helpers/table-cell.js +2 -5
  6. package/cjs/data/table/helpers/table-cell.js.map +1 -1
  7. package/cjs/data/table/helpers/table-focus.d.ts +26 -2
  8. package/cjs/data/table/helpers/table-focus.js +60 -9
  9. package/cjs/data/table/helpers/table-focus.js.map +1 -1
  10. package/cjs/data/table/helpers/table-grid-nav.d.ts +40 -10
  11. package/cjs/data/table/helpers/table-grid-nav.js +102 -25
  12. package/cjs/data/table/helpers/table-grid-nav.js.map +1 -1
  13. package/cjs/data/table/helpers/table-keyboard.d.ts +24 -3
  14. package/cjs/data/table/helpers/table-keyboard.js +25 -5
  15. package/cjs/data/table/helpers/table-keyboard.js.map +1 -1
  16. package/cjs/data/table/hooks/useGridCache.d.ts +17 -0
  17. package/cjs/data/table/hooks/useGridCache.js +65 -0
  18. package/cjs/data/table/hooks/useGridCache.js.map +1 -0
  19. package/cjs/data/table/root/DataTableRoot.d.ts +14 -4
  20. package/cjs/data/table/root/DataTableRoot.js +4 -6
  21. package/cjs/data/table/root/DataTableRoot.js.map +1 -1
  22. package/cjs/data/table/root/useTableKeyboardNav.d.ts +10 -4
  23. package/cjs/data/table/root/useTableKeyboardNav.js +70 -99
  24. package/cjs/data/table/root/useTableKeyboardNav.js.map +1 -1
  25. package/cjs/data/token-filter/AutoSuggest.d.ts +21 -0
  26. package/cjs/data/token-filter/AutoSuggest.js +129 -0
  27. package/cjs/data/token-filter/AutoSuggest.js.map +1 -0
  28. package/cjs/data/token-filter/TokenFilter.d.ts +11 -0
  29. package/cjs/data/token-filter/TokenFilter.js +91 -0
  30. package/cjs/data/token-filter/TokenFilter.js.map +1 -0
  31. package/cjs/data/token-filter/TokenFilter.types.d.ts +46 -0
  32. package/cjs/data/token-filter/TokenFilter.types.js +3 -0
  33. package/cjs/data/token-filter/TokenFilter.types.js.map +1 -0
  34. package/cjs/data/token-filter/helpers/generate-autocomplete-options.d.ts +70 -0
  35. package/cjs/data/token-filter/helpers/generate-autocomplete-options.js +171 -0
  36. package/cjs/data/token-filter/helpers/generate-autocomplete-options.js.map +1 -0
  37. package/cjs/data/token-filter/helpers/parse-query-text.d.ts +31 -0
  38. package/cjs/data/token-filter/helpers/parse-query-text.js +91 -0
  39. package/cjs/data/token-filter/helpers/parse-query-text.js.map +1 -0
  40. package/cjs/link-card/LinkCard.d.ts +13 -0
  41. package/cjs/link-card/LinkCard.js +2 -2
  42. package/cjs/link-card/LinkCard.js.map +1 -1
  43. package/cjs/process/Process.d.ts +1 -1
  44. package/cjs/tooltip/Tooltip.js +1 -1
  45. package/cjs/tooltip/Tooltip.js.map +1 -1
  46. package/esm/accordion/Accordion.d.ts +10 -0
  47. package/esm/accordion/Accordion.js +2 -2
  48. package/esm/accordion/Accordion.js.map +1 -1
  49. package/esm/data/table/helpers/table-cell.d.ts +2 -2
  50. package/esm/data/table/helpers/table-cell.js +2 -5
  51. package/esm/data/table/helpers/table-cell.js.map +1 -1
  52. package/esm/data/table/helpers/table-focus.d.ts +26 -2
  53. package/esm/data/table/helpers/table-focus.js +55 -9
  54. package/esm/data/table/helpers/table-focus.js.map +1 -1
  55. package/esm/data/table/helpers/table-grid-nav.d.ts +40 -10
  56. package/esm/data/table/helpers/table-grid-nav.js +96 -24
  57. package/esm/data/table/helpers/table-grid-nav.js.map +1 -1
  58. package/esm/data/table/helpers/table-keyboard.d.ts +24 -3
  59. package/esm/data/table/helpers/table-keyboard.js +24 -4
  60. package/esm/data/table/helpers/table-keyboard.js.map +1 -1
  61. package/esm/data/table/hooks/useGridCache.d.ts +17 -0
  62. package/esm/data/table/hooks/useGridCache.js +63 -0
  63. package/esm/data/table/hooks/useGridCache.js.map +1 -0
  64. package/esm/data/table/root/DataTableRoot.d.ts +14 -4
  65. package/esm/data/table/root/DataTableRoot.js +4 -6
  66. package/esm/data/table/root/DataTableRoot.js.map +1 -1
  67. package/esm/data/table/root/useTableKeyboardNav.d.ts +10 -4
  68. package/esm/data/table/root/useTableKeyboardNav.js +75 -104
  69. package/esm/data/table/root/useTableKeyboardNav.js.map +1 -1
  70. package/esm/data/token-filter/AutoSuggest.d.ts +21 -0
  71. package/esm/data/token-filter/AutoSuggest.js +93 -0
  72. package/esm/data/token-filter/AutoSuggest.js.map +1 -0
  73. package/esm/data/token-filter/TokenFilter.d.ts +11 -0
  74. package/esm/data/token-filter/TokenFilter.js +55 -0
  75. package/esm/data/token-filter/TokenFilter.js.map +1 -0
  76. package/esm/data/token-filter/TokenFilter.types.d.ts +46 -0
  77. package/esm/data/token-filter/TokenFilter.types.js +2 -0
  78. package/esm/data/token-filter/TokenFilter.types.js.map +1 -0
  79. package/esm/data/token-filter/helpers/generate-autocomplete-options.d.ts +70 -0
  80. package/esm/data/token-filter/helpers/generate-autocomplete-options.js +169 -0
  81. package/esm/data/token-filter/helpers/generate-autocomplete-options.js.map +1 -0
  82. package/esm/data/token-filter/helpers/parse-query-text.d.ts +31 -0
  83. package/esm/data/token-filter/helpers/parse-query-text.js +87 -0
  84. package/esm/data/token-filter/helpers/parse-query-text.js.map +1 -0
  85. package/esm/link-card/LinkCard.d.ts +13 -0
  86. package/esm/link-card/LinkCard.js +2 -2
  87. package/esm/link-card/LinkCard.js.map +1 -1
  88. package/esm/process/Process.d.ts +1 -1
  89. package/esm/tooltip/Tooltip.js +2 -2
  90. package/esm/tooltip/Tooltip.js.map +1 -1
  91. package/package.json +3 -3
  92. package/src/accordion/Accordion.tsx +19 -2
  93. package/src/data/table/helpers/table-cell.ts +2 -7
  94. package/src/data/table/helpers/table-focus.ts +70 -9
  95. package/src/data/table/helpers/table-grid-nav.test.ts +659 -0
  96. package/src/data/table/helpers/table-grid-nav.ts +128 -32
  97. package/src/data/table/helpers/table-keyboard.test.ts +27 -27
  98. package/src/data/table/helpers/table-keyboard.ts +34 -4
  99. package/src/data/table/hooks/useGridCache.ts +73 -0
  100. package/src/data/table/root/DataTableRoot.tsx +21 -11
  101. package/src/data/table/root/useTableKeyboardNav.ts +110 -128
  102. package/src/data/token-filter/AutoSuggest.tsx +179 -0
  103. package/src/data/token-filter/TokenFilter.tsx +124 -0
  104. package/src/data/token-filter/TokenFilter.types.ts +79 -0
  105. package/src/data/token-filter/helpers/generate-autocomplete-options.ts +244 -0
  106. package/src/data/token-filter/helpers/parse-query-text.test.ts +410 -0
  107. package/src/data/token-filter/helpers/parse-query-text.ts +148 -0
  108. package/src/link-card/LinkCard.tsx +15 -1
  109. package/src/process/Process.tsx +1 -1
  110. package/src/tooltip/Tooltip.tsx +3 -3
@@ -1,90 +1,129 @@
1
- import { useEffect, useRef, useState } from "react";
1
+ import { useEffect } from "react";
2
2
  import { useEventCallback } from "../../../utils/hooks";
3
- import { getActiveCell, getFirstCell } from "../helpers/table-cell";
4
- import { focusCell, focusCellAndUpdateTabIndex } from "../helpers/table-focus";
3
+ import { focusInitialTableTarget } from "../helpers/table-cell";
4
+ import { focusCellAndUpdateTabIndex } from "../helpers/table-focus";
5
5
  import {
6
- type GridCache,
7
- ensureTableGrid,
8
- findNextCell,
6
+ findFirstCell,
7
+ findFirstCellInRow,
8
+ findLastCell,
9
+ findLastCellInRow,
10
+ findNextFocusableCell,
9
11
  } from "../helpers/table-grid-nav";
10
12
  import {
11
- getDeltaFromKey,
12
- shouldBlockArrowKeyNavigation,
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 }: { enabled: boolean },
30
+ { enabled, shouldBlockNavigation: customBlockFn }: UseTableKeyboardNavOptions,
18
31
  ) {
19
- const [activeCell, setActiveCell] = useState<Element | null>(null);
20
- const activeCellRef = useRef<Element | null>(null);
21
- activeCellRef.current = activeCell;
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
- * TODO:
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 navigateByArrowKey = useEventCallback(
35
- (delta: { x: number; y: number }) => {
36
- const currentCell = getActiveCell(tableRef, activeCell);
37
- if (!currentCell || !tableRef) {
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, maxCols } = ensureTableGrid(
42
- tableRef,
43
- gridCacheRef.current,
44
- );
53
+ const { grid, positions } = getTableGrid(tableRef);
45
54
  const currentPos = positions.get(currentCell);
46
55
 
47
- if (!currentPos) {
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
- const nextCell = findNextCell(
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
- * We check if the key is an arrow key, and if so, we calculate the next cell to focus based on the current active cell and the grid structure.
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 onKeyDown = useEventCallback((event: KeyboardEvent): void => {
75
- /* Stops keydown from moving if we can assume that you are currently editing input, select etc */
76
- if (shouldBlockArrowKeyNavigation(event)) {
111
+ const handleTableKeyDown = useEventCallback((event: KeyboardEvent): void => {
112
+ if (customBlockFn?.(event)) {
77
113
  return;
78
114
  }
79
115
 
80
- let newCell: Element | null = null;
116
+ if (shouldBlockNavigation(event)) {
117
+ return;
118
+ }
81
119
 
82
- const delta = getDeltaFromKey(event.key);
83
- if (delta) {
84
- event.preventDefault();
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 onFocusIn = useEventCallback((event: FocusEvent): void => {
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
- * Observes changes is table structure and updates the grid cache accordingly.
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
- observerRef.current = new MutationObserver(() => {
124
- gridCacheRef.current.dirty = true;
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", onKeyDown);
155
- tableRef.removeEventListener("focusin", onFocusIn);
167
+ tableRef.removeEventListener("keydown", handleTableKeyDown);
168
+ tableRef.removeEventListener("focusin", handleTableFocusIn);
156
169
  };
157
- }, [tableRef, onKeyDown, onFocusIn, enabled]);
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
- tableTabIndex: enabled ? (activeCell ? undefined : 0) : undefined,
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
+ };