@olenbetong/appframe-ds 1.3.6 → 1.3.8

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 (179) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +6 -6
  3. package/scripts/copyAssets.mjs +27 -27
  4. package/src/AfLookup/Lookup.css +130 -129
  5. package/src/AfLookup/Lookup.tsx +186 -186
  6. package/src/AfLookup/LookupCombobox.css +72 -78
  7. package/src/AfLookup/LookupCombobox.tsx +237 -237
  8. package/src/AfLookup/LookupEdit.tsx +130 -130
  9. package/src/AfLookup/LookupGrid.css +7 -12
  10. package/src/AfLookup/LookupGrid.tsx +124 -124
  11. package/src/AfLookup/index.ts +5 -5
  12. package/src/autocomplete/Autocomplete.css +256 -256
  13. package/src/autocomplete/Autocomplete.tsx +320 -320
  14. package/src/autocomplete/AutocompleteFrame.tsx +168 -168
  15. package/src/autocomplete/Combobox.tsx +286 -286
  16. package/src/autocomplete/index.ts +4 -4
  17. package/src/autocomplete/types.ts +204 -204
  18. package/src/autocomplete/utils.ts +183 -183
  19. package/src/binding/BoundNumericTextField.tsx +102 -102
  20. package/src/binding/BoundTextField.tsx +49 -49
  21. package/src/binding/CancelButton.tsx +30 -30
  22. package/src/binding/CancelIconButton.tsx +30 -30
  23. package/src/binding/DataEditToolbar.css +23 -23
  24. package/src/binding/DataEditToolbar.tsx +84 -84
  25. package/src/binding/DeleteButton.tsx +35 -35
  26. package/src/binding/DeleteIconButton.tsx +35 -35
  27. package/src/binding/RefreshButton.tsx +31 -31
  28. package/src/binding/RefreshIconButton.tsx +33 -33
  29. package/src/binding/RefreshRowIconButton.tsx +33 -33
  30. package/src/binding/SaveButton.tsx +37 -37
  31. package/src/binding/SaveIconButton.tsx +38 -38
  32. package/src/container/Container.tsx +50 -49
  33. package/src/container/index.ts +1 -1
  34. package/src/cutoff/CutOffPicker.css +12 -12
  35. package/src/cutoff/CutOffPicker.tsx +165 -165
  36. package/src/cutoff/context.tsx +194 -194
  37. package/src/cutoff/dsCutOffDates.ts +21 -21
  38. package/src/cutoff/index.ts +4 -4
  39. package/src/cutoff/utils.test.ts +59 -59
  40. package/src/cutoff/utils.ts +276 -276
  41. package/src/filter/ChipListInput.tsx +79 -79
  42. package/src/filter/FieldFilterPanel.css +150 -150
  43. package/src/filter/FieldFilterPanel.tsx +309 -309
  44. package/src/filter/FilterBuilder.css +152 -152
  45. package/src/filter/FilterBuilder.test.tsx +153 -153
  46. package/src/filter/FilterBuilder.tsx +435 -435
  47. package/src/filter/FilterEditor.css +231 -231
  48. package/src/filter/FilterEditor.test.tsx +285 -285
  49. package/src/filter/FilterEditor.tsx +75 -75
  50. package/src/filter/FilterGroupEditor.tsx +160 -160
  51. package/src/filter/FilterNameDialog.css +22 -22
  52. package/src/filter/FilterNameDialog.tsx +81 -81
  53. package/src/filter/FilterRow.tsx +117 -117
  54. package/src/filter/FilterShareDialog.css +28 -28
  55. package/src/filter/FilterShareDialog.tsx +201 -201
  56. package/src/filter/FilterStringField.tsx +78 -78
  57. package/src/filter/FilterValueEditor.tsx +239 -239
  58. package/src/filter/SavedFilterTree.css +107 -107
  59. package/src/filter/SavedFilterTree.test.tsx +94 -94
  60. package/src/filter/SavedFilterTree.tsx +218 -218
  61. package/src/filter/fieldFilter.test.ts +158 -158
  62. package/src/filter/fieldFilter.ts +139 -139
  63. package/src/filter/index.ts +27 -27
  64. package/src/filter/types.ts +38 -38
  65. package/src/filter/useDistinctValues.test.ts +102 -102
  66. package/src/filter/useDistinctValues.ts +230 -230
  67. package/src/global.d.ts +11 -11
  68. package/src/grid/AfGridColumnsPanel.tsx +118 -118
  69. package/src/grid/AfGridContext.tsx +51 -51
  70. package/src/grid/AfGridError.tsx +46 -46
  71. package/src/grid/AfHeaderFilterCell.tsx +147 -147
  72. package/src/grid/AfHeaderFilterPanel.tsx +111 -111
  73. package/src/grid/GridEditLookup.css +12 -12
  74. package/src/grid/GridEditLookup.tsx +121 -121
  75. package/src/grid/Toolbar.tsx +233 -233
  76. package/src/grid/editing.ts +100 -100
  77. package/src/grid/filter.ts +78 -78
  78. package/src/grid/formatters.ts +48 -48
  79. package/src/grid/index.css +245 -245
  80. package/src/grid/index.tsx +503 -503
  81. package/src/grid/license.ts +48 -48
  82. package/src/grid/localization.ts +369 -369
  83. package/src/grid/slots/index.tsx +334 -334
  84. package/src/grid/slots/slots.css +64 -64
  85. package/src/grid/slots/slots.test.tsx +39 -39
  86. package/src/grid/theme.tsx +138 -138
  87. package/src/grid/useAfColumns.tsx +371 -371
  88. package/src/grid/useAfCurrentIndex.ts +63 -63
  89. package/src/grid/useAfData.ts +20 -20
  90. package/src/grid/useAfFilter.ts +160 -160
  91. package/src/grid/useAfFilterFields.ts +55 -55
  92. package/src/grid/useAfGridApi.ts +71 -71
  93. package/src/grid/useAfKeyBindings.ts +36 -36
  94. package/src/grid/useAfNewItemRow.ts +258 -258
  95. package/src/grid/useAfPagination.ts +53 -53
  96. package/src/grid/useAfPersistedState.ts +230 -230
  97. package/src/grid/useAfRowEditModel.ts +145 -145
  98. package/src/grid/useAfRowGrouping.ts +47 -47
  99. package/src/grid/useAfServerAggregation.test.ts +68 -68
  100. package/src/grid/useAfServerAggregation.ts +158 -158
  101. package/src/grid/useAfSortModel.ts +84 -84
  102. package/src/i18n.ts +13 -13
  103. package/src/index.ts +14 -14
  104. package/src/input/InputAdornments.css +84 -84
  105. package/src/input/InputAdornments.tsx +102 -102
  106. package/src/input/index.ts +1 -1
  107. package/src/layout/AppMenu.tsx +74 -74
  108. package/src/layout/AppMenuDrawer.css +70 -70
  109. package/src/layout/AppMenuDrawer.tsx +122 -122
  110. package/src/layout/ErrorBoundary.tsx +162 -162
  111. package/src/layout/PageContainer.css +48 -48
  112. package/src/layout/PageContainer.tsx +41 -41
  113. package/src/layout/ResponsiveDialog.css +86 -86
  114. package/src/layout/ResponsiveDialog.tsx +55 -55
  115. package/src/layout/index.tsx +69 -69
  116. package/src/link/Link.tsx +23 -23
  117. package/src/link/index.ts +1 -1
  118. package/src/mdi/Mdi.css +138 -138
  119. package/src/mdi/Mdi.test.tsx +91 -91
  120. package/src/mdi/Mdi.tsx +141 -141
  121. package/src/mdi/MdiConfirmCloseDialog.tsx +47 -47
  122. package/src/mdi/MdiContext.tsx +59 -59
  123. package/src/mdi/MdiDocumentContext.tsx +60 -60
  124. package/src/mdi/MdiPanel.tsx +61 -61
  125. package/src/mdi/MdiRestore.test.tsx +86 -86
  126. package/src/mdi/MdiTab.tsx +30 -30
  127. package/src/mdi/controller.test.ts +382 -382
  128. package/src/mdi/controller.ts +656 -656
  129. package/src/mdi/dockviewHost.ts +132 -132
  130. package/src/mdi/fakeHost.ts +200 -200
  131. package/src/mdi/host.ts +44 -44
  132. package/src/mdi/ids.test.ts +20 -20
  133. package/src/mdi/ids.ts +18 -18
  134. package/src/mdi/index.ts +31 -31
  135. package/src/mdi/layoutRestore.test.ts +156 -156
  136. package/src/mdi/layoutRestore.ts +311 -311
  137. package/src/mdi/layoutSpec.test.ts +143 -143
  138. package/src/mdi/layoutSpec.ts +214 -214
  139. package/src/mdi/layoutStore.test.ts +93 -93
  140. package/src/mdi/layoutStore.ts +99 -99
  141. package/src/mdi/memorySettingsStore.ts +21 -21
  142. package/src/mdi/registry.ts +34 -34
  143. package/src/mdi/testTypes.ts +37 -37
  144. package/src/mdi/theme.ts +22 -22
  145. package/src/mdi/types.ts +214 -214
  146. package/src/page/Page.css +53 -53
  147. package/src/page/PageBlock.css +64 -64
  148. package/src/page/PageBlock.tsx +93 -92
  149. package/src/page/PageHeader.css +88 -88
  150. package/src/page/PageHeader.tsx +115 -115
  151. package/src/page/PageTitle.css +5 -5
  152. package/src/page/PageTitle.tsx +21 -21
  153. package/src/page/index.ts +4 -4
  154. package/src/paper/Paper.tsx +52 -52
  155. package/src/progress/LinearProgress.css +93 -93
  156. package/src/progress/LinearProgress.tsx +104 -104
  157. package/src/progress/index.ts +1 -1
  158. package/src/report-downloader/ReportDownloader.css +67 -67
  159. package/src/report-downloader/components/StatusItem.tsx +87 -87
  160. package/src/report-downloader/components/StatusList.tsx +28 -28
  161. package/src/report-downloader/index.ts +41 -41
  162. package/src/report-downloader/status.tsx +92 -92
  163. package/src/sortable/DropIndicator.css +51 -51
  164. package/src/sortable/DropIndicator.tsx +27 -27
  165. package/src/sortable/SortableItemList.css +21 -21
  166. package/src/sortable/SortableItemList.tsx +264 -264
  167. package/src/sortable/index.ts +8 -8
  168. package/src/sortable/reorderSortOrder.ts +126 -126
  169. package/src/sortable/sortOrder.ts +101 -101
  170. package/src/sortable/types.ts +9 -9
  171. package/src/sortable/useSortableContainer.ts +53 -53
  172. package/src/sortable/useSortableItem.ts +95 -95
  173. package/src/sortable/useSortableList.ts +117 -117
  174. package/src/test/happy-dom-document-class.ts +24 -24
  175. package/src/test/setup.ts +12 -12
  176. package/src/theme/useDsColorScheme.ts +53 -53
  177. package/tsconfig.build.json +9 -9
  178. package/tsconfig.json +8 -8
  179. package/vitest.config.ts +10 -10
@@ -1,334 +1,334 @@
1
- import "./slots.css";
2
-
3
- import {
4
- Badge,
5
- Button,
6
- Checkbox,
7
- Divider,
8
- Select,
9
- Spinner,
10
- Switch,
11
- Tag,
12
- Textfield,
13
- Tooltip,
14
- } from "@digdir/designsystemet-react";
15
- import type { GridSlotProps, GridSlotsComponent } from "@mui/x-data-grid-premium";
16
-
17
- import clsx from "clsx";
18
- import { type Ref, useCallback } from "react";
19
-
20
- type DsSize = "2xs" | "xs" | "sm" | "md" | "lg";
21
-
22
- /**
23
- * Maps the MUI slot sizes onto the Designsystemet size tokens. The grid only
24
- * ever asks for `small` or `medium`, but the toolbar buttons also allow `large`.
25
- */
26
- function dsSize(size?: "small" | "medium" | "large"): DsSize {
27
- switch (size) {
28
- case "small":
29
- return "sm";
30
- case "large":
31
- return "lg";
32
- default:
33
- return "sm";
34
- }
35
- }
36
-
37
- /**
38
- * The grid passes `title` as a `ReactNode`, but Designsystemet's tooltip only
39
- * accepts a string. Anything else is rendered without a tooltip.
40
- */
41
- function tooltipContent(title: React.ReactNode) {
42
- return typeof title === "string" ? title : typeof title === "number" ? String(title) : undefined;
43
- }
44
-
45
- function BaseCheckbox(props: GridSlotProps["baseCheckbox"]) {
46
- let {
47
- indeterminate,
48
- inputRef,
49
- label,
50
- size,
51
- density: _density,
52
- fullWidth: _fullWidth,
53
- touchRippleRef: _touchRippleRef,
54
- slotProps,
55
- className,
56
- ...other
57
- } = props;
58
-
59
- // `indeterminate` is not a DOM attribute, so it has to be set on the input
60
- // element itself.
61
- let ref = useCallback(
62
- (element: HTMLInputElement | null) => {
63
- if (element) {
64
- element.indeterminate = !!indeterminate;
65
- }
66
-
67
- if (typeof inputRef === "function") {
68
- inputRef(element);
69
- } else if (inputRef) {
70
- (inputRef as React.RefObject<HTMLInputElement | null>).current = element;
71
- }
72
- },
73
- [indeterminate, inputRef],
74
- );
75
-
76
- return (
77
- <Checkbox
78
- {...(other as any)}
79
- {...slotProps?.htmlInput}
80
- ref={ref}
81
- className={clsx("AfGrid-checkbox", className)}
82
- data-size={dsSize(size)}
83
- label={label}
84
- aria-label={label ? undefined : ((other as any)["aria-label"] ?? "")}
85
- />
86
- );
87
- }
88
-
89
- function BaseButton(props: GridSlotProps["baseButton"]) {
90
- let { children, size, startIcon, touchRippleRef: _touchRippleRef, ...other } = props;
91
-
92
- return (
93
- <Button {...(other as any)} variant="tertiary" data-size={dsSize(size)}>
94
- {startIcon}
95
- {children}
96
- </Button>
97
- );
98
- }
99
-
100
- function BaseIconButton(props: GridSlotProps["baseIconButton"]) {
101
- let { children, size, label, color: _color, edge: _edge, touchRippleRef: _touchRippleRef, ...other } = props;
102
-
103
- return (
104
- <Button {...(other as any)} icon variant="tertiary" data-size={dsSize(size)} aria-label={label}>
105
- {children}
106
- </Button>
107
- );
108
- }
109
-
110
- function BaseTooltip(props: GridSlotProps["baseTooltip"]) {
111
- let { children, title, enterDelay: _enterDelay, disableInteractive: _disableInteractive, ...other } = props;
112
- let content = tooltipContent(title);
113
-
114
- // Designsystemet labels the tooltip host itself, and logs `Missing
115
- // tabindex="0" attribute on:` whenever that host is not tabbable. In the grid
116
- // it never is — the column header is the tab stop and moves focus with the
117
- // arrow keys, so MUI gives the menu button `tabindex="-1"` and hangs the sort
118
- // tooltip on a bare `<span>`. The tooltips still open, because Designsystemet
119
- // resolves the host with `closest()` on focus and hover, so the warning is a
120
- // false positive — and it is not gated on `NODE_ENV`, so it reaches
121
- // production too.
122
- //
123
- // Setting the label ourselves is exactly what Designsystemet does a moment
124
- // later (`aria-description` when the host has text, `aria-label` when it does
125
- // not), and it makes Designsystemet skip that whole branch — silencing the
126
- // warning without turning off `window.dsWarnings` for the entire app.
127
- let ref = useCallback(
128
- (element: HTMLElement | null) => {
129
- if (!element || !content) {
130
- return;
131
- }
132
-
133
- let hasText = element.getAttribute("role") !== "img" && !!element.textContent?.trim();
134
- element.setAttribute(hasText ? "aria-description" : "aria-label", content);
135
- },
136
- [content],
137
- );
138
-
139
- if (!content) {
140
- return children;
141
- }
142
-
143
- return (
144
- <Tooltip {...(other as any)} ref={ref} content={content}>
145
- {children}
146
- </Tooltip>
147
- );
148
- }
149
-
150
- function BaseTextField(props: GridSlotProps["baseTextField"]) {
151
- let {
152
- inputRef,
153
- size,
154
- error,
155
- helperText,
156
- slotProps,
157
- fullWidth: _fullWidth,
158
- color: _color,
159
- className,
160
- ...other
161
- } = props;
162
-
163
- return (
164
- <Textfield
165
- {...(other as any)}
166
- {...slotProps?.htmlInput}
167
- ref={inputRef as Ref<HTMLInputElement>}
168
- className={clsx("AfGrid-textField", className)}
169
- data-size={dsSize(size)}
170
- error={error ? helperText : undefined}
171
- aria-label={other.label ? undefined : ((other as any)["aria-label"] ?? "")}
172
- />
173
- );
174
- }
175
-
176
- function BaseSelect(props: GridSlotProps["baseSelect"]) {
177
- let {
178
- children,
179
- size,
180
- label,
181
- labelId: _labelId,
182
- native: _native,
183
- fullWidth: _fullWidth,
184
- open: _open,
185
- onOpen: _onOpen,
186
- onClose: _onClose,
187
- slotProps,
188
- className,
189
- ...other
190
- } = props;
191
-
192
- return (
193
- <Select
194
- {...(other as any)}
195
- {...slotProps?.htmlInput}
196
- className={clsx("AfGrid-select", className)}
197
- data-size={dsSize(size)}
198
- aria-label={typeof label === "string" ? label : ((other as any)["aria-label"] ?? "")}
199
- >
200
- {children}
201
- </Select>
202
- );
203
- }
204
-
205
- function BaseSelectOption(props: GridSlotProps["baseSelectOption"]) {
206
- let { native: _native, children, ...other } = props;
207
-
208
- return <Select.Option {...(other as any)}>{children}</Select.Option>;
209
- }
210
-
211
- function BaseSwitch(props: GridSlotProps["baseSwitch"]) {
212
- let { size, label, ...other } = props;
213
-
214
- return (
215
- <Switch
216
- {...(other as any)}
217
- data-size={dsSize(size)}
218
- label={label}
219
- aria-label={label ? undefined : ((other as any)["aria-label"] ?? "")}
220
- />
221
- );
222
- }
223
-
224
- function BaseChip(props: GridSlotProps["baseChip"]) {
225
- let { label, size, icon, variant, children: _children, ...other } = props;
226
-
227
- return (
228
- <Tag {...(other as any)} data-size={dsSize(size)} variant={variant === "outlined" ? "outline" : "default"}>
229
- {icon}
230
- {label}
231
- </Tag>
232
- );
233
- }
234
-
235
- function BaseBadge(props: GridSlotProps["baseBadge"]) {
236
- let { badgeContent, children, invisible, color, variant, overlap, className, ...other } = props;
237
- let count = typeof badgeContent === "number" ? badgeContent : Number(badgeContent);
238
-
239
- // The badge must wrap the element it decorates so Designsystemet anchors it to
240
- // that element's corner. Rendering it as a sibling put the badge on an empty
241
- // box next to the icon, where it was clipped by the column header.
242
- return (
243
- <Badge.Position
244
- {...(other as any)}
245
- className={clsx("AfGrid-badge", className)}
246
- placement="top-right"
247
- overlap={overlap === "circular" ? "circle" : "rectangle"}
248
- >
249
- {children}
250
- {!invisible && (
251
- <Badge
252
- aria-hidden
253
- count={variant === "dot" || Number.isNaN(count) ? undefined : count}
254
- data-color={color === "error" ? "danger" : color === "primary" ? "accent" : "neutral"}
255
- />
256
- )}
257
- </Badge.Position>
258
- );
259
- }
260
-
261
- function BaseCircularProgress(props: GridSlotProps["baseCircularProgress"]) {
262
- let { size, color: _color, ...other } = props;
263
- let numericSize = typeof size === "number" ? size : 24;
264
-
265
- return <Spinner {...(other as any)} aria-hidden data-size={numericSize <= 20 ? "xs" : "sm"} />;
266
- }
267
-
268
- function BaseLinearProgress(props: GridSlotProps["baseLinearProgress"]) {
269
- let { className, ...other } = props;
270
-
271
- return <div {...(other as any)} role="progressbar" className={clsx("AfGrid-linearProgress", className)} />;
272
- }
273
-
274
- /**
275
- * Renders Designsystemet's skeleton markup rather than its `Skeleton`
276
- * component. The component synchronizes its animation with every other
277
- * skeleton by calling `document.getAnimations()` when it mounts, and the grid's
278
- * loading overlay mounts one per visible cell in the same commit - hundreds of
279
- * scans over hundreds of animations, which froze the page for seconds on every
280
- * load. Skeletons mounted together start their animation together anyway.
281
- */
282
- function BaseSkeleton(props: GridSlotProps["baseSkeleton"]) {
283
- let { variant, width, height, className, style, ...other } = props;
284
- let dsVariant = variant === "circular" ? "circle" : variant;
285
-
286
- if (dsVariant === "text") {
287
- return (
288
- <span
289
- {...(other as any)}
290
- aria-hidden="true"
291
- className={clsx("ds-skeleton", className)}
292
- data-text={"-".repeat(Number(width) || 1)}
293
- data-variant="text"
294
- style={style}
295
- />
296
- );
297
- }
298
-
299
- return (
300
- <span
301
- {...(other as any)}
302
- aria-hidden="true"
303
- className={clsx("ds-skeleton", className)}
304
- data-variant={dsVariant}
305
- style={{ width, height, ...style }}
306
- />
307
- );
308
- }
309
-
310
- function BaseDivider(props: GridSlotProps["baseDivider"]) {
311
- return <Divider {...(props as any)} />;
312
- }
313
-
314
- /**
315
- * Designsystemet replacements for the grid's base components. Passed to the
316
- * `slots` prop of the data grid, so the grid renders Designsystemet components
317
- * instead of the MUI ones wherever it can.
318
- */
319
- export const dsBaseSlots: Partial<GridSlotsComponent> = {
320
- baseBadge: BaseBadge,
321
- baseButton: BaseButton,
322
- baseCheckbox: BaseCheckbox,
323
- baseChip: BaseChip,
324
- baseCircularProgress: BaseCircularProgress,
325
- baseDivider: BaseDivider,
326
- baseIconButton: BaseIconButton,
327
- baseLinearProgress: BaseLinearProgress,
328
- baseSelect: BaseSelect,
329
- baseSelectOption: BaseSelectOption,
330
- baseSkeleton: BaseSkeleton,
331
- baseSwitch: BaseSwitch,
332
- baseTextField: BaseTextField,
333
- baseTooltip: BaseTooltip,
334
- };
1
+ import "./slots.css";
2
+
3
+ import {
4
+ Badge,
5
+ Button,
6
+ Checkbox,
7
+ Divider,
8
+ Select,
9
+ Spinner,
10
+ Switch,
11
+ Tag,
12
+ Textfield,
13
+ Tooltip,
14
+ } from "@digdir/designsystemet-react";
15
+ import type { GridSlotProps, GridSlotsComponent } from "@mui/x-data-grid-premium";
16
+
17
+ import clsx from "clsx";
18
+ import { type Ref, useCallback } from "react";
19
+
20
+ type DsSize = "2xs" | "xs" | "sm" | "md" | "lg";
21
+
22
+ /**
23
+ * Maps the MUI slot sizes onto the Designsystemet size tokens. The grid only
24
+ * ever asks for `small` or `medium`, but the toolbar buttons also allow `large`.
25
+ */
26
+ function dsSize(size?: "small" | "medium" | "large"): DsSize {
27
+ switch (size) {
28
+ case "small":
29
+ return "sm";
30
+ case "large":
31
+ return "lg";
32
+ default:
33
+ return "sm";
34
+ }
35
+ }
36
+
37
+ /**
38
+ * The grid passes `title` as a `ReactNode`, but Designsystemet's tooltip only
39
+ * accepts a string. Anything else is rendered without a tooltip.
40
+ */
41
+ function tooltipContent(title: React.ReactNode) {
42
+ return typeof title === "string" ? title : typeof title === "number" ? String(title) : undefined;
43
+ }
44
+
45
+ function BaseCheckbox(props: GridSlotProps["baseCheckbox"]) {
46
+ let {
47
+ indeterminate,
48
+ inputRef,
49
+ label,
50
+ size,
51
+ density: _density,
52
+ fullWidth: _fullWidth,
53
+ touchRippleRef: _touchRippleRef,
54
+ slotProps,
55
+ className,
56
+ ...other
57
+ } = props;
58
+
59
+ // `indeterminate` is not a DOM attribute, so it has to be set on the input
60
+ // element itself.
61
+ let ref = useCallback(
62
+ (element: HTMLInputElement | null) => {
63
+ if (element) {
64
+ element.indeterminate = !!indeterminate;
65
+ }
66
+
67
+ if (typeof inputRef === "function") {
68
+ inputRef(element);
69
+ } else if (inputRef) {
70
+ (inputRef as React.RefObject<HTMLInputElement | null>).current = element;
71
+ }
72
+ },
73
+ [indeterminate, inputRef],
74
+ );
75
+
76
+ return (
77
+ <Checkbox
78
+ {...(other as any)}
79
+ {...slotProps?.htmlInput}
80
+ ref={ref}
81
+ className={clsx("AfGrid-checkbox", className)}
82
+ data-size={dsSize(size)}
83
+ label={label}
84
+ aria-label={label ? undefined : ((other as any)["aria-label"] ?? "")}
85
+ />
86
+ );
87
+ }
88
+
89
+ function BaseButton(props: GridSlotProps["baseButton"]) {
90
+ let { children, size, startIcon, touchRippleRef: _touchRippleRef, ...other } = props;
91
+
92
+ return (
93
+ <Button {...(other as any)} variant="tertiary" data-size={dsSize(size)}>
94
+ {startIcon}
95
+ {children}
96
+ </Button>
97
+ );
98
+ }
99
+
100
+ function BaseIconButton(props: GridSlotProps["baseIconButton"]) {
101
+ let { children, size, label, color: _color, edge: _edge, touchRippleRef: _touchRippleRef, ...other } = props;
102
+
103
+ return (
104
+ <Button {...(other as any)} icon variant="tertiary" data-size={dsSize(size)} aria-label={label}>
105
+ {children}
106
+ </Button>
107
+ );
108
+ }
109
+
110
+ function BaseTooltip(props: GridSlotProps["baseTooltip"]) {
111
+ let { children, title, enterDelay: _enterDelay, disableInteractive: _disableInteractive, ...other } = props;
112
+ let content = tooltipContent(title);
113
+
114
+ // Designsystemet labels the tooltip host itself, and logs `Missing
115
+ // tabindex="0" attribute on:` whenever that host is not tabbable. In the grid
116
+ // it never is — the column header is the tab stop and moves focus with the
117
+ // arrow keys, so MUI gives the menu button `tabindex="-1"` and hangs the sort
118
+ // tooltip on a bare `<span>`. The tooltips still open, because Designsystemet
119
+ // resolves the host with `closest()` on focus and hover, so the warning is a
120
+ // false positive — and it is not gated on `NODE_ENV`, so it reaches
121
+ // production too.
122
+ //
123
+ // Setting the label ourselves is exactly what Designsystemet does a moment
124
+ // later (`aria-description` when the host has text, `aria-label` when it does
125
+ // not), and it makes Designsystemet skip that whole branch — silencing the
126
+ // warning without turning off `window.dsWarnings` for the entire app.
127
+ let ref = useCallback(
128
+ (element: HTMLElement | null) => {
129
+ if (!element || !content) {
130
+ return;
131
+ }
132
+
133
+ let hasText = element.getAttribute("role") !== "img" && !!element.textContent?.trim();
134
+ element.setAttribute(hasText ? "aria-description" : "aria-label", content);
135
+ },
136
+ [content],
137
+ );
138
+
139
+ if (!content) {
140
+ return children;
141
+ }
142
+
143
+ return (
144
+ <Tooltip {...(other as any)} ref={ref} content={content}>
145
+ {children}
146
+ </Tooltip>
147
+ );
148
+ }
149
+
150
+ function BaseTextField(props: GridSlotProps["baseTextField"]) {
151
+ let {
152
+ inputRef,
153
+ size,
154
+ error,
155
+ helperText,
156
+ slotProps,
157
+ fullWidth: _fullWidth,
158
+ color: _color,
159
+ className,
160
+ ...other
161
+ } = props;
162
+
163
+ return (
164
+ <Textfield
165
+ {...(other as any)}
166
+ {...slotProps?.htmlInput}
167
+ ref={inputRef as Ref<HTMLInputElement>}
168
+ className={clsx("AfGrid-textField", className)}
169
+ data-size={dsSize(size)}
170
+ error={error ? helperText : undefined}
171
+ aria-label={other.label ? undefined : ((other as any)["aria-label"] ?? "")}
172
+ />
173
+ );
174
+ }
175
+
176
+ function BaseSelect(props: GridSlotProps["baseSelect"]) {
177
+ let {
178
+ children,
179
+ size,
180
+ label,
181
+ labelId: _labelId,
182
+ native: _native,
183
+ fullWidth: _fullWidth,
184
+ open: _open,
185
+ onOpen: _onOpen,
186
+ onClose: _onClose,
187
+ slotProps,
188
+ className,
189
+ ...other
190
+ } = props;
191
+
192
+ return (
193
+ <Select
194
+ {...(other as any)}
195
+ {...slotProps?.htmlInput}
196
+ className={clsx("AfGrid-select", className)}
197
+ data-size={dsSize(size)}
198
+ aria-label={typeof label === "string" ? label : ((other as any)["aria-label"] ?? "")}
199
+ >
200
+ {children}
201
+ </Select>
202
+ );
203
+ }
204
+
205
+ function BaseSelectOption(props: GridSlotProps["baseSelectOption"]) {
206
+ let { native: _native, children, ...other } = props;
207
+
208
+ return <Select.Option {...(other as any)}>{children}</Select.Option>;
209
+ }
210
+
211
+ function BaseSwitch(props: GridSlotProps["baseSwitch"]) {
212
+ let { size, label, ...other } = props;
213
+
214
+ return (
215
+ <Switch
216
+ {...(other as any)}
217
+ data-size={dsSize(size)}
218
+ label={label}
219
+ aria-label={label ? undefined : ((other as any)["aria-label"] ?? "")}
220
+ />
221
+ );
222
+ }
223
+
224
+ function BaseChip(props: GridSlotProps["baseChip"]) {
225
+ let { label, size, icon, variant, children: _children, ...other } = props;
226
+
227
+ return (
228
+ <Tag {...(other as any)} data-size={dsSize(size)} variant={variant === "outlined" ? "outline" : "default"}>
229
+ {icon}
230
+ {label}
231
+ </Tag>
232
+ );
233
+ }
234
+
235
+ function BaseBadge(props: GridSlotProps["baseBadge"]) {
236
+ let { badgeContent, children, invisible, color, variant, overlap, className, ...other } = props;
237
+ let count = typeof badgeContent === "number" ? badgeContent : Number(badgeContent);
238
+
239
+ // The badge must wrap the element it decorates so Designsystemet anchors it to
240
+ // that element's corner. Rendering it as a sibling put the badge on an empty
241
+ // box next to the icon, where it was clipped by the column header.
242
+ return (
243
+ <Badge.Position
244
+ {...(other as any)}
245
+ className={clsx("AfGrid-badge", className)}
246
+ placement="top-right"
247
+ overlap={overlap === "circular" ? "circle" : "rectangle"}
248
+ >
249
+ {children}
250
+ {!invisible && (
251
+ <Badge
252
+ aria-hidden
253
+ count={variant === "dot" || Number.isNaN(count) ? undefined : count}
254
+ data-color={color === "error" ? "danger" : color === "primary" ? "accent" : "neutral"}
255
+ />
256
+ )}
257
+ </Badge.Position>
258
+ );
259
+ }
260
+
261
+ function BaseCircularProgress(props: GridSlotProps["baseCircularProgress"]) {
262
+ let { size, color: _color, ...other } = props;
263
+ let numericSize = typeof size === "number" ? size : 24;
264
+
265
+ return <Spinner {...(other as any)} aria-hidden data-size={numericSize <= 20 ? "xs" : "sm"} />;
266
+ }
267
+
268
+ function BaseLinearProgress(props: GridSlotProps["baseLinearProgress"]) {
269
+ let { className, ...other } = props;
270
+
271
+ return <div {...(other as any)} role="progressbar" className={clsx("AfGrid-linearProgress", className)} />;
272
+ }
273
+
274
+ /**
275
+ * Renders Designsystemet's skeleton markup rather than its `Skeleton`
276
+ * component. The component synchronizes its animation with every other
277
+ * skeleton by calling `document.getAnimations()` when it mounts, and the grid's
278
+ * loading overlay mounts one per visible cell in the same commit - hundreds of
279
+ * scans over hundreds of animations, which froze the page for seconds on every
280
+ * load. Skeletons mounted together start their animation together anyway.
281
+ */
282
+ function BaseSkeleton(props: GridSlotProps["baseSkeleton"]) {
283
+ let { variant, width, height, className, style, ...other } = props;
284
+ let dsVariant = variant === "circular" ? "circle" : variant;
285
+
286
+ if (dsVariant === "text") {
287
+ return (
288
+ <span
289
+ {...(other as any)}
290
+ aria-hidden="true"
291
+ className={clsx("ds-skeleton", className)}
292
+ data-text={"-".repeat(Number(width) || 1)}
293
+ data-variant="text"
294
+ style={style}
295
+ />
296
+ );
297
+ }
298
+
299
+ return (
300
+ <span
301
+ {...(other as any)}
302
+ aria-hidden="true"
303
+ className={clsx("ds-skeleton", className)}
304
+ data-variant={dsVariant}
305
+ style={{ width, height, ...style }}
306
+ />
307
+ );
308
+ }
309
+
310
+ function BaseDivider(props: GridSlotProps["baseDivider"]) {
311
+ return <Divider {...(props as any)} />;
312
+ }
313
+
314
+ /**
315
+ * Designsystemet replacements for the grid's base components. Passed to the
316
+ * `slots` prop of the data grid, so the grid renders Designsystemet components
317
+ * instead of the MUI ones wherever it can.
318
+ */
319
+ export const dsBaseSlots: Partial<GridSlotsComponent> = {
320
+ baseBadge: BaseBadge,
321
+ baseButton: BaseButton,
322
+ baseCheckbox: BaseCheckbox,
323
+ baseChip: BaseChip,
324
+ baseCircularProgress: BaseCircularProgress,
325
+ baseDivider: BaseDivider,
326
+ baseIconButton: BaseIconButton,
327
+ baseLinearProgress: BaseLinearProgress,
328
+ baseSelect: BaseSelect,
329
+ baseSelectOption: BaseSelectOption,
330
+ baseSkeleton: BaseSkeleton,
331
+ baseSwitch: BaseSwitch,
332
+ baseTextField: BaseTextField,
333
+ baseTooltip: BaseTooltip,
334
+ };