@olenbetong/appframe-ds 1.8.0 → 1.9.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 (147) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +2 -2
  3. package/src/AfLookup/Lookup.css +16 -1
  4. package/src/AfLookup/Lookup.tsx +37 -12
  5. package/src/AfLookup/LookupCombobox.tsx +1 -9
  6. package/src/AfLookup/LookupEdit.tsx +5 -4
  7. package/src/AfLookup/LookupGrid.css +3 -2
  8. package/src/AfLookup/LookupGrid.tsx +8 -4
  9. package/src/autocomplete/Autocomplete.css +6 -17
  10. package/src/binding/BoundNumberInput.tsx +42 -42
  11. package/src/binding/BoundNumericTextField.tsx +102 -102
  12. package/src/binding/BoundTextField.tsx +49 -49
  13. package/src/binding/CancelButton.tsx +30 -30
  14. package/src/binding/CancelIconButton.tsx +30 -30
  15. package/src/binding/DataEditToolbar.css +23 -23
  16. package/src/binding/DataEditToolbar.tsx +84 -84
  17. package/src/binding/DeleteButton.tsx +35 -35
  18. package/src/binding/DeleteIconButton.tsx +35 -35
  19. package/src/binding/RefreshButton.tsx +31 -31
  20. package/src/binding/RefreshIconButton.tsx +33 -33
  21. package/src/binding/RefreshRowIconButton.tsx +33 -33
  22. package/src/binding/SaveButton.tsx +37 -37
  23. package/src/binding/SaveIconButton.tsx +38 -38
  24. package/src/binding/index.ts +20 -20
  25. package/src/container/Container.tsx +50 -50
  26. package/src/container/index.ts +1 -1
  27. package/src/cutoff/CutOffPicker.css +12 -12
  28. package/src/cutoff/CutOffPicker.tsx +165 -165
  29. package/src/cutoff/context.tsx +194 -194
  30. package/src/cutoff/dsCutOffDates.ts +21 -21
  31. package/src/cutoff/index.ts +4 -4
  32. package/src/cutoff/utils.test.ts +59 -59
  33. package/src/cutoff/utils.ts +276 -276
  34. package/src/filter/ChipListInput.tsx +79 -79
  35. package/src/filter/FieldFilterPanel.css +150 -150
  36. package/src/filter/FieldFilterPanel.tsx +303 -309
  37. package/src/filter/FilterBuilder.css +152 -152
  38. package/src/filter/FilterBuilder.test.tsx +153 -153
  39. package/src/filter/FilterBuilder.tsx +435 -435
  40. package/src/filter/FilterEditor.css +231 -231
  41. package/src/filter/FilterEditor.test.tsx +285 -285
  42. package/src/filter/FilterEditor.tsx +75 -75
  43. package/src/filter/FilterGroupEditor.tsx +160 -160
  44. package/src/filter/FilterNameDialog.css +22 -22
  45. package/src/filter/FilterNameDialog.tsx +81 -81
  46. package/src/filter/FilterRow.tsx +117 -117
  47. package/src/filter/FilterShareDialog.css +28 -28
  48. package/src/filter/FilterShareDialog.tsx +195 -201
  49. package/src/filter/FilterStringField.tsx +72 -78
  50. package/src/filter/FilterValueEditor.tsx +239 -239
  51. package/src/filter/SavedFilterTree.css +107 -107
  52. package/src/filter/SavedFilterTree.test.tsx +94 -94
  53. package/src/filter/SavedFilterTree.tsx +218 -218
  54. package/src/filter/fieldFilter.test.ts +158 -158
  55. package/src/filter/fieldFilter.ts +139 -139
  56. package/src/filter/index.ts +27 -27
  57. package/src/filter/types.ts +38 -38
  58. package/src/filter/useDistinctValues.test.ts +102 -102
  59. package/src/filter/useDistinctValues.ts +230 -230
  60. package/src/global.d.ts +11 -11
  61. package/src/grid/AfGridColumnsPanel.tsx +4 -7
  62. package/src/grid/AfGridError.tsx +2 -2
  63. package/src/grid/AfHeaderFilterCell.tsx +0 -3
  64. package/src/grid/AfHeaderFilterPanel.tsx +1 -3
  65. package/src/grid/GridEditLookup.tsx +3 -4
  66. package/src/grid/Toolbar.tsx +9 -38
  67. package/src/grid/index.css +44 -0
  68. package/src/grid/index.tsx +12 -3
  69. package/src/grid/slots/index.tsx +15 -40
  70. package/src/grid/useAfGridApi.ts +0 -1
  71. package/src/grid/useAfPersistedState.ts +4 -8
  72. package/src/grid/useAfSizeMode.ts +19 -0
  73. package/src/i18n.ts +13 -13
  74. package/src/index.ts +14 -14
  75. package/src/input/NumberInput.tsx +0 -2
  76. package/src/layout/AppMenu.tsx +74 -74
  77. package/src/layout/AppMenuDrawer.css +71 -71
  78. package/src/layout/AppMenuDrawer.tsx +122 -122
  79. package/src/layout/DialogMotion.css +79 -79
  80. package/src/layout/ErrorBoundary.tsx +162 -162
  81. package/src/layout/PageContainer.css +48 -48
  82. package/src/layout/PageContainer.tsx +41 -41
  83. package/src/layout/ResponsiveDialog.css +86 -86
  84. package/src/layout/ResponsiveDialog.tsx +55 -55
  85. package/src/layout/index.tsx +70 -70
  86. package/src/link/Link.tsx +23 -23
  87. package/src/link/index.ts +1 -1
  88. package/src/list/ItemList.css +2 -1
  89. package/src/mdi/Mdi.test.tsx +91 -91
  90. package/src/mdi/Mdi.tsx +141 -141
  91. package/src/mdi/MdiConfirmCloseDialog.tsx +41 -47
  92. package/src/mdi/MdiContext.tsx +59 -59
  93. package/src/mdi/MdiDocumentContext.tsx +60 -60
  94. package/src/mdi/MdiPanel.tsx +61 -61
  95. package/src/mdi/MdiRestore.test.tsx +86 -86
  96. package/src/mdi/MdiTab.tsx +30 -30
  97. package/src/mdi/controller.test.ts +382 -382
  98. package/src/mdi/controller.ts +656 -656
  99. package/src/mdi/dockviewHost.ts +132 -132
  100. package/src/mdi/fakeHost.ts +200 -200
  101. package/src/mdi/host.ts +44 -44
  102. package/src/mdi/ids.test.ts +20 -20
  103. package/src/mdi/ids.ts +18 -18
  104. package/src/mdi/index.ts +31 -31
  105. package/src/mdi/layoutRestore.test.ts +156 -156
  106. package/src/mdi/layoutRestore.ts +311 -311
  107. package/src/mdi/layoutSpec.test.ts +143 -143
  108. package/src/mdi/layoutSpec.ts +214 -214
  109. package/src/mdi/layoutStore.test.ts +93 -93
  110. package/src/mdi/layoutStore.ts +99 -99
  111. package/src/mdi/memorySettingsStore.ts +21 -21
  112. package/src/mdi/registry.ts +34 -34
  113. package/src/mdi/testTypes.ts +37 -37
  114. package/src/mdi/theme.ts +22 -22
  115. package/src/mdi/types.ts +214 -214
  116. package/src/page/Page.css +57 -57
  117. package/src/page/PageBlock.css +64 -64
  118. package/src/page/PageBlock.tsx +93 -93
  119. package/src/page/PageHeader.css +88 -88
  120. package/src/page/PageHeader.tsx +115 -115
  121. package/src/page/PageTitle.css +5 -5
  122. package/src/page/PageTitle.tsx +21 -21
  123. package/src/page/index.ts +4 -4
  124. package/src/paper/Paper.tsx +52 -52
  125. package/src/progress/LinearProgress.css +121 -121
  126. package/src/progress/LinearProgress.tsx +106 -106
  127. package/src/progress/index.ts +1 -1
  128. package/src/report-downloader/ReportDownloader.css +67 -67
  129. package/src/report-downloader/components/StatusItem.tsx +87 -87
  130. package/src/report-downloader/components/StatusList.tsx +28 -28
  131. package/src/report-downloader/index.ts +41 -41
  132. package/src/report-downloader/status.tsx +92 -92
  133. package/src/sortable/DropIndicator.css +51 -51
  134. package/src/sortable/DropIndicator.tsx +27 -27
  135. package/src/sortable/SortableItemList.css +21 -21
  136. package/src/sortable/SortableItemList.tsx +264 -264
  137. package/src/sortable/index.ts +8 -8
  138. package/src/sortable/reorderSortOrder.ts +126 -126
  139. package/src/sortable/sortOrder.ts +101 -101
  140. package/src/sortable/types.ts +9 -9
  141. package/src/sortable/useSortableContainer.ts +53 -53
  142. package/src/sortable/useSortableItem.ts +95 -95
  143. package/src/sortable/useSortableList.ts +117 -117
  144. package/src/test/happy-dom-document-class.ts +24 -24
  145. package/src/test/setup.ts +27 -27
  146. package/src/theme/sizeMode.ts +62 -0
  147. package/src/theme/useDsColorScheme.ts +53 -53
@@ -7,9 +7,6 @@ import {
7
7
  } from "@mui/x-data-grid-premium";
8
8
  import {
9
9
  ArrowsCirclepathIcon,
10
- Density1Icon,
11
- Density2Icon,
12
- Density3Icon,
13
10
  FileCsvIcon,
14
11
  FileExcelIcon,
15
12
  FilterIcon,
@@ -30,7 +27,7 @@ import { getLocalizedString } from "../i18n.js";
30
27
 
31
28
  export function GridToolbarAddButton({ onClick }: { onClick: () => void }) {
32
29
  return (
33
- <Button variant="tertiary" data-size="sm" onClick={onClick}>
30
+ <Button variant="tertiary" onClick={onClick}>
34
31
  <PlusIcon aria-hidden />
35
32
  {getLocalizedString("Add")}
36
33
  </Button>
@@ -60,7 +57,6 @@ export function GridToolbarRefreshButton({ iconOnly }: GridToolbarIconOnlyProps
60
57
  return (
61
58
  <Button
62
59
  variant="tertiary"
63
- data-size="sm"
64
60
  icon={iconOnly}
65
61
  aria-label={iconOnly ? label : undefined}
66
62
  title={iconOnly ? label : undefined}
@@ -79,7 +75,7 @@ export function GridToolbarColumnsButton() {
79
75
  let { openColumnsPanel } = useAfGridContext();
80
76
 
81
77
  return (
82
- <Button variant="tertiary" data-size="sm" onClick={openColumnsPanel}>
78
+ <Button variant="tertiary" onClick={openColumnsPanel}>
83
79
  <TableIcon aria-hidden />
84
80
  {getLocalizedString("Columns")}
85
81
  </Button>
@@ -98,8 +94,8 @@ export function GridToolbarFilterButton({ iconOnly }: GridToolbarIconOnlyProps =
98
94
  let name = filterCount > 0 ? `${label} (${filterCount})` : label;
99
95
  return (
100
96
  <BadgePosition placement="top-right" overlap="circle">
101
- {filterCount > 0 && <Badge data-color="brand" data-size="sm" count={filterCount} aria-hidden="true" />}
102
- <Button variant="tertiary" data-size="sm" icon aria-label={name} title={name} onClick={openFilterBuilder}>
97
+ {filterCount > 0 && <Badge data-color="brand" count={filterCount} aria-hidden="true" />}
98
+ <Button variant="tertiary" icon aria-label={name} title={name} onClick={openFilterBuilder}>
103
99
  <FilterIcon aria-hidden />
104
100
  </Button>
105
101
  </BadgePosition>
@@ -107,7 +103,7 @@ export function GridToolbarFilterButton({ iconOnly }: GridToolbarIconOnlyProps =
107
103
  }
108
104
 
109
105
  return (
110
- <Button variant="tertiary" data-size="sm" onClick={openFilterBuilder}>
106
+ <Button variant="tertiary" onClick={openFilterBuilder}>
111
107
  <FilterIcon aria-hidden />
112
108
  {label}
113
109
  {filterCount > 0 ? ` (${filterCount})` : ""}
@@ -123,7 +119,7 @@ export function GridToolbarResetLayoutButton() {
123
119
  }
124
120
 
125
121
  return (
126
- <Button variant="tertiary" data-size="sm" onClick={handleReset}>
122
+ <Button variant="tertiary" onClick={handleReset}>
127
123
  <ArrowsCirclepathIcon aria-hidden />
128
124
  {getLocalizedString("Reset layout")}
129
125
  </Button>
@@ -182,7 +178,7 @@ export function AfGridToolbarExcelExportButton({ fileName, title }: { fileName?:
182
178
  let dataObject = useDataObject();
183
179
 
184
180
  return (
185
- <Button variant="tertiary" data-size="sm" onClick={() => handleExportToExcel(apiRef, dataObject, fileName, title)}>
181
+ <Button variant="tertiary" onClick={() => handleExportToExcel(apiRef, dataObject, fileName, title)}>
186
182
  <FileExcelIcon aria-hidden />
187
183
  {getLocalizedString("Export")}
188
184
  </Button>
@@ -195,7 +191,7 @@ export type GridToolbarMoreOptionsButtonProps = React.PropsWithChildren<{
195
191
  }>;
196
192
 
197
193
  /**
198
- * The toolbar's "more" menu: exports, density and layout reset. `children` are extra
194
+ * The toolbar's "more" menu: exports and layout reset. `children` are extra
199
195
  * `Dropdown.Item`s placed first. The menu closes when an item is chosen.
200
196
  */
201
197
  export function GridToolbarMoreOptionsButton({ showColumns, children }: GridToolbarMoreOptionsButtonProps = {}) {
@@ -208,19 +204,12 @@ export function GridToolbarMoreOptionsButton({ showColumns, children }: GridTool
208
204
  <Dropdown.Trigger
209
205
  variant="tertiary"
210
206
  icon
211
- data-size="sm"
212
207
  aria-label={getLocalizedString("More options")}
213
208
  title={getLocalizedString("More options")}
214
209
  >
215
210
  <MenuElipsisVerticalIcon aria-hidden />
216
211
  </Dropdown.Trigger>
217
- <Dropdown
218
- open={open}
219
- onOpen={() => setOpen(true)}
220
- onClose={() => setOpen(false)}
221
- placement="bottom-end"
222
- data-size="sm"
223
- >
212
+ <Dropdown open={open} onOpen={() => setOpen(true)} onClose={() => setOpen(false)} placement="bottom-end">
224
213
  <Dropdown.List
225
214
  onClick={(event) => {
226
215
  if ((event.target as HTMLElement).closest("button, a")) setOpen(false);
@@ -242,24 +231,6 @@ export function GridToolbarMoreOptionsButton({ showColumns, children }: GridTool
242
231
  </Dropdown.Button>
243
232
  </Dropdown.Item>
244
233
  <AfGridExcelExportMenuItem />
245
- <Dropdown.Item>
246
- <Dropdown.Button onClick={() => apiRef.current.setDensity("compact")}>
247
- <Density1Icon aria-hidden />
248
- {getLocalizedString("Compact")}
249
- </Dropdown.Button>
250
- </Dropdown.Item>
251
- <Dropdown.Item>
252
- <Dropdown.Button onClick={() => apiRef.current.setDensity("standard")}>
253
- <Density2Icon aria-hidden />
254
- {getLocalizedString("Standard")}
255
- </Dropdown.Button>
256
- </Dropdown.Item>
257
- <Dropdown.Item>
258
- <Dropdown.Button onClick={() => apiRef.current.setDensity("comfortable")}>
259
- <Density3Icon aria-hidden />
260
- {getLocalizedString("Comfortable")}
261
- </Dropdown.Button>
262
- </Dropdown.Item>
263
234
  <Dropdown.Item>
264
235
  <Dropdown.Button onClick={() => (apiRef.current as AfGridApi).resetLayout?.()}>
265
236
  <ArrowsCirclepathIcon aria-hidden />
@@ -2,6 +2,50 @@
2
2
  height: 100%;
3
3
  }
4
4
 
5
+ /*
6
+ * The grid follows the Designsystemet size mode it sits in instead of MUI's density and type
7
+ * scale: its text is the size mode's body text, like the inputs around it, and its row and header
8
+ * heights come from the same mode (`useAfSizeMode`).
9
+ */
10
+ .AfGrid-root .MuiDataGrid-root {
11
+ font-size: inherit;
12
+ }
13
+
14
+ /*
15
+ * The footer is MUI's own, with a 52px minimum height and pagination text in MUI's type scale; it
16
+ * is as tall as a control and its text is the grid's.
17
+ */
18
+ .AfGrid-root {
19
+ .MuiDataGrid-footerContainer,
20
+ .MuiTablePagination-toolbar {
21
+ min-height: var(--ds-size-12, 52px);
22
+ }
23
+
24
+ .MuiTablePagination-root,
25
+ .MuiTablePagination-selectLabel,
26
+ .MuiTablePagination-displayedRows {
27
+ font-size: inherit;
28
+ }
29
+ }
30
+
31
+ /*
32
+ * Controls in a data cell - editors, lookup buttons, the row's action buttons - fit the row, which
33
+ * is `--ds-size-10` tall, rather than taking Designsystemet's `--ds-size-12` control height, which
34
+ * would overflow it at every size. The custom properties are set on the components themselves,
35
+ * so they are overridden there and not on the cell.
36
+ */
37
+ .AfGrid-root .MuiDataGrid-cell {
38
+ .ds-input {
39
+ --dsc-input-size: var(--ds-size-10, 2.25rem);
40
+ --dsc-input-padding-block: 0;
41
+ }
42
+
43
+ .ds-button {
44
+ --dsc-button-size: var(--ds-size-8, 2rem);
45
+ --dsc-button-padding: 0 var(--ds-size-2, 0.5rem);
46
+ }
47
+ }
48
+
5
49
  .AfGrid-loading {
6
50
  display: flex;
7
51
  align-items: center;
@@ -20,7 +20,7 @@ import { type DataObject, uid } from "@olenbetong/appframe-data";
20
20
  import { DataObjectProvider, useLoading } from "@olenbetong/appframe-react";
21
21
 
22
22
  import clsx from "clsx";
23
- import { useCallback, useEffect, useMemo, useState } from "react";
23
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
24
24
  import { FilterBuilder } from "../filter/index.js";
25
25
  import { AfGridColumnsPanel } from "./AfGridColumnsPanel.js";
26
26
  import { AfHeaderFilterCell } from "./AfHeaderFilterCell.js";
@@ -67,6 +67,7 @@ import { useAfKeepFocusAfterEdit } from "./useAfKeepFocusAfterEdit.js";
67
67
  import { useAfRowEditModel } from "./useAfRowEditModel.js";
68
68
  import { useAfRowGrouping } from "./useAfRowGrouping.js";
69
69
  import { useAfServerAggregation } from "./useAfServerAggregation.js";
70
+ import { useAfSizeMode } from "./useAfSizeMode.js";
70
71
  import { useAfSortModel } from "./useAfSortModel.js";
71
72
  import { getLocalizedString } from "../i18n.js";
72
73
 
@@ -442,6 +443,8 @@ function AfGrid_(props: AfGridProps) {
442
443
  let filterFields = useAfFilterFields(dataObject, columnsProp);
443
444
 
444
445
  let keyEvents = useAfKeyBindings({ apiRef, dataObject });
446
+ let rootRef = useRef<HTMLDivElement>(null);
447
+ let sizeMode = useAfSizeMode(rootRef);
445
448
 
446
449
  useEffect(() => {
447
450
  if (!snackbar) return;
@@ -499,7 +502,7 @@ function AfGrid_(props: AfGridProps) {
499
502
  return (
500
503
  <DataObjectProvider dataObject={dataObject}>
501
504
  <AfGridContextProvider value={gridContext}>
502
- <div className="AfGrid-root" style={newItemRow.rootStyle} {...keyEvents} tabIndex={-1}>
505
+ <div ref={rootRef} className="AfGrid-root" style={newItemRow.rootStyle} {...keyEvents} tabIndex={-1}>
503
506
  <DataGrid
504
507
  apiRef={apiRef}
505
508
  rows={data}
@@ -519,7 +522,13 @@ function AfGrid_(props: AfGridProps) {
519
522
  headerFilters
520
523
  {...paging}
521
524
  showToolbar
525
+ {...sizeMode}
522
526
  {...other}
527
+ /*
528
+ * The size mode sets the row height (`useAfSizeMode`), so MUI's density stays at the
529
+ * standard factor of 1 and is not offered or restored.
530
+ */
531
+ density="standard"
523
532
  {...grouping.gridProps}
524
533
  /*
525
534
  * Spread rather than passed, because the grid merges props over its defaults with
@@ -580,7 +589,7 @@ function AfGrid_(props: AfGridProps) {
580
589
  />
581
590
  {!!snackbar && (
582
591
  <div className="AfGrid-snackbar">
583
- <Alert data-color="danger" data-size="sm" onClick={() => setSnackbar(null)}>
592
+ <Alert data-color="danger" onClick={() => setSnackbar(null)}>
584
593
  {snackbar}
585
594
  </Alert>
586
595
  </div>
@@ -17,22 +17,11 @@ import type { GridSlotProps, GridSlotsComponent } from "@mui/x-data-grid-premium
17
17
  import clsx from "clsx";
18
18
  import { type Ref, useCallback } from "react";
19
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`.
20
+ /*
21
+ * The slots take no size from MUI's `size` prop: they inherit the Designsystemet size mode the
22
+ * grid sits in, like everything else on the page. Only the spinner's `data-size` is kept, which
23
+ * picks the spinner's own size rather than the mode.
25
24
  */
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
25
 
37
26
  /**
38
27
  * The grid passes `title` as a `ReactNode`, but Designsystemet's tooltip only
@@ -47,7 +36,7 @@ function BaseCheckbox(props: GridSlotProps["baseCheckbox"]) {
47
36
  indeterminate,
48
37
  inputRef,
49
38
  label,
50
- size,
39
+ size: _size,
51
40
  density: _density,
52
41
  fullWidth: _fullWidth,
53
42
  touchRippleRef: _touchRippleRef,
@@ -79,7 +68,6 @@ function BaseCheckbox(props: GridSlotProps["baseCheckbox"]) {
79
68
  {...slotProps?.htmlInput}
80
69
  ref={ref}
81
70
  className={clsx("AfGrid-checkbox", className)}
82
- data-size={dsSize(size)}
83
71
  label={label}
84
72
  aria-label={label ? undefined : ((other as any)["aria-label"] ?? "")}
85
73
  />
@@ -87,10 +75,10 @@ function BaseCheckbox(props: GridSlotProps["baseCheckbox"]) {
87
75
  }
88
76
 
89
77
  function BaseButton(props: GridSlotProps["baseButton"]) {
90
- let { children, size, startIcon, touchRippleRef: _touchRippleRef, ...other } = props;
78
+ let { children, size: _size, startIcon, touchRippleRef: _touchRippleRef, ...other } = props;
91
79
 
92
80
  return (
93
- <Button {...(other as any)} variant="tertiary" data-size={dsSize(size)}>
81
+ <Button {...(other as any)} variant="tertiary">
94
82
  {startIcon}
95
83
  {children}
96
84
  </Button>
@@ -98,17 +86,11 @@ function BaseButton(props: GridSlotProps["baseButton"]) {
98
86
  }
99
87
 
100
88
  function BaseIconButton(props: GridSlotProps["baseIconButton"]) {
101
- let { children, size, label, color: _color, edge: _edge, touchRippleRef: _touchRippleRef, ...other } = props;
89
+ let { children, size: _size, label, color: _color, edge: _edge, touchRippleRef: _touchRippleRef, ...other } = props;
102
90
 
103
91
  // `GridActionsCellItem` names its button through `aria-label`, not `label`; keep it
104
92
  return (
105
- <Button
106
- {...(other as any)}
107
- icon
108
- variant="tertiary"
109
- data-size={dsSize(size)}
110
- aria-label={label ?? (other as any)["aria-label"]}
111
- >
93
+ <Button {...(other as any)} icon variant="tertiary" aria-label={label ?? (other as any)["aria-label"]}>
112
94
  {children}
113
95
  </Button>
114
96
  );
@@ -157,7 +139,7 @@ function BaseTooltip(props: GridSlotProps["baseTooltip"]) {
157
139
  function BaseTextField(props: GridSlotProps["baseTextField"]) {
158
140
  let {
159
141
  inputRef,
160
- size,
142
+ size: _size,
161
143
  error,
162
144
  helperText,
163
145
  slotProps,
@@ -173,7 +155,6 @@ function BaseTextField(props: GridSlotProps["baseTextField"]) {
173
155
  {...slotProps?.htmlInput}
174
156
  ref={inputRef as Ref<HTMLInputElement>}
175
157
  className={clsx("AfGrid-textField", className)}
176
- data-size={dsSize(size)}
177
158
  error={error ? helperText : undefined}
178
159
  aria-label={other.label ? undefined : ((other as any)["aria-label"] ?? "")}
179
160
  />
@@ -183,7 +164,7 @@ function BaseTextField(props: GridSlotProps["baseTextField"]) {
183
164
  function BaseSelect(props: GridSlotProps["baseSelect"]) {
184
165
  let {
185
166
  children,
186
- size,
167
+ size: _size,
187
168
  label,
188
169
  labelId: _labelId,
189
170
  native: _native,
@@ -201,7 +182,6 @@ function BaseSelect(props: GridSlotProps["baseSelect"]) {
201
182
  {...(other as any)}
202
183
  {...slotProps?.htmlInput}
203
184
  className={clsx("AfGrid-select", className)}
204
- data-size={dsSize(size)}
205
185
  aria-label={typeof label === "string" ? label : ((other as any)["aria-label"] ?? "")}
206
186
  >
207
187
  {children}
@@ -216,23 +196,18 @@ function BaseSelectOption(props: GridSlotProps["baseSelectOption"]) {
216
196
  }
217
197
 
218
198
  function BaseSwitch(props: GridSlotProps["baseSwitch"]) {
219
- let { size, label, ...other } = props;
199
+ let { size: _size, label, ...other } = props;
220
200
 
221
201
  return (
222
- <Switch
223
- {...(other as any)}
224
- data-size={dsSize(size)}
225
- label={label}
226
- aria-label={label ? undefined : ((other as any)["aria-label"] ?? "")}
227
- />
202
+ <Switch {...(other as any)} label={label} aria-label={label ? undefined : ((other as any)["aria-label"] ?? "")} />
228
203
  );
229
204
  }
230
205
 
231
206
  function BaseChip(props: GridSlotProps["baseChip"]) {
232
- let { label, size, icon, variant, children: _children, ...other } = props;
207
+ let { label, size: _size, icon, variant, children: _children, ...other } = props;
233
208
 
234
209
  return (
235
- <Tag {...(other as any)} data-size={dsSize(size)} variant={variant === "outlined" ? "outline" : "default"}>
210
+ <Tag {...(other as any)} variant={variant === "outlined" ? "outline" : "default"}>
236
211
  {icon}
237
212
  {label}
238
213
  </Tag>
@@ -52,7 +52,6 @@ export function useAfGridApi(props: Partial<AfGridProps>, apiRef: React.RefObjec
52
52
  });
53
53
  }
54
54
 
55
- apiRef.current?.setDensity(propsRef.current.initialState?.density ?? "compact");
56
55
  apiRef.current?.setPageSize(propsRef.current.dataObject.options.parameters.maxRecords ?? 100);
57
56
  apiRef.current?.setSortModel(
58
57
  afSortOrderToMuiSortModel(
@@ -47,10 +47,8 @@ function removeNonPersistedState(state: GridInitialState, config?: StatePersiste
47
47
  let pagination: GridInitialState["pagination"] = { paginationModel: {} };
48
48
  let sorting: GridInitialState["sorting"] = {};
49
49
 
50
- // Persist the relevant state unless explicitly disabled in the config
51
- if (config?.density !== false) {
52
- newState.density = state.density;
53
- }
50
+ // Persist the relevant state unless explicitly disabled in the config. Not the density: the
51
+ // size mode sets the row height, and the grid stays at the standard density.
54
52
  if (config?.pinnedColumns !== false) {
55
53
  newState.pinnedColumns = state.pinnedColumns;
56
54
  }
@@ -108,10 +106,7 @@ function getInitialState({ columns, persistenceModel, id, initialState, layoutVe
108
106
  let state: GridInitialState =
109
107
  persistedState?.layoutVersion === layoutVersion
110
108
  ? removeNonPersistedState(persistedState.state, persistenceModel)
111
- : {
112
- density: "compact",
113
- ...initialState,
114
- };
109
+ : { ...initialState };
115
110
 
116
111
  if (!state.columns) {
117
112
  state.columns = {};
@@ -173,6 +168,7 @@ setTimeout(() => {
173
168
 
174
169
  export type StatePersistenceConfig = {
175
170
  aggregation?: boolean;
171
+ /** @deprecated The size mode sets the row height, and the density is no longer persisted. */
176
172
  density?: boolean;
177
173
  pinnedColumns?: boolean;
178
174
  rowGrouping?: boolean;
@@ -0,0 +1,19 @@
1
+ import type { RefObject } from "react";
2
+
3
+ import { useDsSize } from "../theme/sizeMode.js";
4
+
5
+ /** MUI's compact row height, the grid's look before it followed the size mode. */
6
+ const FALLBACK_HEIGHT = 36;
7
+
8
+ /**
9
+ * Row and header heights from the Designsystemet size mode the grid sits in, in place of MUI's
10
+ * density: `--ds-size-10`, which is 25px at xs, 31px at sm, 35px at md and 41px at lg with the
11
+ * site's 14px root. The grid renders at the standard density, so these are the heights as they
12
+ * are. The text, toolbar and footer follow the same size mode through the stylesheet and by
13
+ * inheriting `data-size`.
14
+ */
15
+ export function useAfSizeMode(rootRef: RefObject<HTMLElement | null>) {
16
+ let height = useDsSize(rootRef, 10, FALLBACK_HEIGHT);
17
+
18
+ return { rowHeight: height, columnHeaderHeight: height };
19
+ }
package/src/i18n.ts CHANGED
@@ -1,13 +1,13 @@
1
- /*
2
- * The package's strings live in their own globalization project, `appframe-ds`, so one
3
- * translation covers every app and the package can word a term its own way. The templates
4
- * render the dictionary as af.i18n["appframe-ds"]; on a page that does not (a legacy
5
- * template) the localizer falls back to the article's strings, exactly as before. See
6
- * `createLocalizer` in @olenbetong/appframe-core.
7
- */
8
-
9
- import { createLocalizer } from "@olenbetong/appframe-core";
10
-
11
- export const PROJECT = "appframe-ds";
12
-
13
- export const { getLocalizedString, localize } = createLocalizer(PROJECT);
1
+ /*
2
+ * The package's strings live in their own globalization project, `appframe-ds`, so one
3
+ * translation covers every app and the package can word a term its own way. The templates
4
+ * render the dictionary as af.i18n["appframe-ds"]; on a page that does not (a legacy
5
+ * template) the localizer falls back to the article's strings, exactly as before. See
6
+ * `createLocalizer` in @olenbetong/appframe-core.
7
+ */
8
+
9
+ import { createLocalizer } from "@olenbetong/appframe-core";
10
+
11
+ export const PROJECT = "appframe-ds";
12
+
13
+ export const { getLocalizedString, localize } = createLocalizer(PROJECT);
package/src/index.ts CHANGED
@@ -1,14 +1,14 @@
1
- export * from "./AfLookup/index.js";
2
- export * from "./autocomplete/index.js";
3
- export * from "./binding/index.js";
4
- export * from "./container/index.js";
5
- export * from "./cutoff/index.js";
6
- export * from "./filter/index.js";
7
- export * from "./input/index.js";
8
- export * from "./layout/index.js";
9
- export * from "./link/index.js";
10
- export * from "./list/index.js";
11
- export * from "./page/index.js";
12
- export * from "./paper/index.js";
13
- export * from "./progress/index.js";
14
- export * from "./sortable/index.js";
1
+ export * from "./AfLookup/index.js";
2
+ export * from "./autocomplete/index.js";
3
+ export * from "./binding/index.js";
4
+ export * from "./container/index.js";
5
+ export * from "./cutoff/index.js";
6
+ export * from "./filter/index.js";
7
+ export * from "./input/index.js";
8
+ export * from "./layout/index.js";
9
+ export * from "./link/index.js";
10
+ export * from "./list/index.js";
11
+ export * from "./page/index.js";
12
+ export * from "./paper/index.js";
13
+ export * from "./progress/index.js";
14
+ export * from "./sortable/index.js";
@@ -237,7 +237,6 @@ export function NumberInput({
237
237
  icon
238
238
  type="button"
239
239
  variant="tertiary"
240
- data-size="sm"
241
240
  className="ObNumberInput-stepper"
242
241
  tabIndex={-1}
243
242
  disabled={atMin}
@@ -250,7 +249,6 @@ export function NumberInput({
250
249
  icon
251
250
  type="button"
252
251
  variant="tertiary"
253
- data-size="sm"
254
252
  className="ObNumberInput-stepper"
255
253
  tabIndex={-1}
256
254
  disabled={atMax}
@@ -1,74 +1,74 @@
1
- import "./AppMenu.css";
2
-
3
- import clsx from "clsx";
4
- import type React from "react";
5
- import { getLocalizedString } from "../i18n.js";
6
-
7
- type MenuHeaderProps = {
8
- children?: React.ReactNode[] | React.ReactNode;
9
- primary: string;
10
- secondary?: string;
11
- };
12
-
13
- function MenuHeader({ primary, secondary }: MenuHeaderProps) {
14
- return (
15
- <>
16
- <div className="AppMenu-header">
17
- <h1 className="AppMenu-title" id="NavigationHeader">
18
- {primary}
19
- </h1>
20
- {secondary && <p className="AppMenu-subTitle">{secondary}</p>}
21
- </div>
22
- <hr className="AppMenu-divider" />
23
- </>
24
- );
25
- }
26
-
27
- function handleKeyDown(evt: React.KeyboardEvent) {
28
- let nextElement: Element | null | undefined = null;
29
- let appMenu = evt.currentTarget;
30
-
31
- if (!document.activeElement || !(appMenu instanceof HTMLElement) || !appMenu.contains(document.activeElement)) return;
32
-
33
- let links = Array.from(appMenu.querySelectorAll<HTMLElement>("a, button"));
34
- let currentIndex = links.indexOf(document.activeElement as HTMLElement);
35
-
36
- if (evt.key === "ArrowDown" || evt.key === "ArrowRight") {
37
- if (currentIndex < links.length) {
38
- nextElement = links[currentIndex + 1];
39
- }
40
- } else if (evt.key === "ArrowUp" || evt.key === "ArrowLeft") {
41
- if (currentIndex > 0) {
42
- nextElement = links[currentIndex - 1];
43
- }
44
- } else if (evt.key === "Home" || evt.key === "End") {
45
- nextElement = links.at(evt.key === "Home" ? 0 : -1);
46
- }
47
-
48
- if (nextElement instanceof HTMLElement) {
49
- evt.preventDefault();
50
- nextElement.scrollIntoView({ block: "nearest" });
51
- nextElement.focus();
52
- }
53
- }
54
-
55
- export type AppMenuProps = Pick<React.HTMLProps<HTMLElement>, "aria-label" | "aria-labelledby" | "className" | "id"> & {
56
- children?: React.ReactNode[] | React.ReactNode;
57
- headerPrimary?: string;
58
- headerSecondary?: string;
59
- };
60
-
61
- export function AppMenu({ children, className, headerPrimary, headerSecondary, ...props }: AppMenuProps) {
62
- return (
63
- <nav
64
- aria-labelledby={headerPrimary ? "NavigationHeader" : undefined}
65
- aria-label={headerPrimary ? undefined : getLocalizedString("Application pages")}
66
- className={clsx(className, "MenuItems-root")}
67
- onKeyDown={handleKeyDown}
68
- {...props}
69
- >
70
- {headerPrimary && <MenuHeader primary={headerPrimary} secondary={headerSecondary} />}
71
- {children}
72
- </nav>
73
- );
74
- }
1
+ import "./AppMenu.css";
2
+
3
+ import clsx from "clsx";
4
+ import type React from "react";
5
+ import { getLocalizedString } from "../i18n.js";
6
+
7
+ type MenuHeaderProps = {
8
+ children?: React.ReactNode[] | React.ReactNode;
9
+ primary: string;
10
+ secondary?: string;
11
+ };
12
+
13
+ function MenuHeader({ primary, secondary }: MenuHeaderProps) {
14
+ return (
15
+ <>
16
+ <div className="AppMenu-header">
17
+ <h1 className="AppMenu-title" id="NavigationHeader">
18
+ {primary}
19
+ </h1>
20
+ {secondary && <p className="AppMenu-subTitle">{secondary}</p>}
21
+ </div>
22
+ <hr className="AppMenu-divider" />
23
+ </>
24
+ );
25
+ }
26
+
27
+ function handleKeyDown(evt: React.KeyboardEvent) {
28
+ let nextElement: Element | null | undefined = null;
29
+ let appMenu = evt.currentTarget;
30
+
31
+ if (!document.activeElement || !(appMenu instanceof HTMLElement) || !appMenu.contains(document.activeElement)) return;
32
+
33
+ let links = Array.from(appMenu.querySelectorAll<HTMLElement>("a, button"));
34
+ let currentIndex = links.indexOf(document.activeElement as HTMLElement);
35
+
36
+ if (evt.key === "ArrowDown" || evt.key === "ArrowRight") {
37
+ if (currentIndex < links.length) {
38
+ nextElement = links[currentIndex + 1];
39
+ }
40
+ } else if (evt.key === "ArrowUp" || evt.key === "ArrowLeft") {
41
+ if (currentIndex > 0) {
42
+ nextElement = links[currentIndex - 1];
43
+ }
44
+ } else if (evt.key === "Home" || evt.key === "End") {
45
+ nextElement = links.at(evt.key === "Home" ? 0 : -1);
46
+ }
47
+
48
+ if (nextElement instanceof HTMLElement) {
49
+ evt.preventDefault();
50
+ nextElement.scrollIntoView({ block: "nearest" });
51
+ nextElement.focus();
52
+ }
53
+ }
54
+
55
+ export type AppMenuProps = Pick<React.HTMLProps<HTMLElement>, "aria-label" | "aria-labelledby" | "className" | "id"> & {
56
+ children?: React.ReactNode[] | React.ReactNode;
57
+ headerPrimary?: string;
58
+ headerSecondary?: string;
59
+ };
60
+
61
+ export function AppMenu({ children, className, headerPrimary, headerSecondary, ...props }: AppMenuProps) {
62
+ return (
63
+ <nav
64
+ aria-labelledby={headerPrimary ? "NavigationHeader" : undefined}
65
+ aria-label={headerPrimary ? undefined : getLocalizedString("Application pages")}
66
+ className={clsx(className, "MenuItems-root")}
67
+ onKeyDown={handleKeyDown}
68
+ {...props}
69
+ >
70
+ {headerPrimary && <MenuHeader primary={headerPrimary} secondary={headerSecondary} />}
71
+ {children}
72
+ </nav>
73
+ );
74
+ }