@navikt/ds-react 8.5.0 → 8.5.2

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