@olenbetong/appframe-ds 1.6.0 → 1.7.0

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 (154) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +3 -3
  3. package/scripts/copyAssets.mjs +27 -27
  4. package/src/autocomplete/Autocomplete.css +253 -253
  5. package/src/autocomplete/Autocomplete.tsx +320 -320
  6. package/src/autocomplete/AutocompleteFrame.tsx +168 -168
  7. package/src/autocomplete/Combobox.tsx +286 -286
  8. package/src/autocomplete/index.ts +4 -4
  9. package/src/autocomplete/types.ts +204 -204
  10. package/src/autocomplete/utils.ts +183 -183
  11. package/src/binding/BoundNumberInput.tsx +42 -42
  12. package/src/binding/BoundNumericTextField.tsx +102 -102
  13. package/src/binding/BoundTextField.tsx +49 -49
  14. package/src/binding/CancelButton.tsx +30 -30
  15. package/src/binding/CancelIconButton.tsx +30 -30
  16. package/src/binding/DataEditToolbar.css +23 -23
  17. package/src/binding/DataEditToolbar.tsx +84 -84
  18. package/src/binding/DeleteButton.tsx +35 -35
  19. package/src/binding/DeleteIconButton.tsx +35 -35
  20. package/src/binding/RefreshButton.tsx +31 -31
  21. package/src/binding/RefreshIconButton.tsx +33 -33
  22. package/src/binding/RefreshRowIconButton.tsx +33 -33
  23. package/src/binding/SaveButton.tsx +37 -37
  24. package/src/binding/SaveIconButton.tsx +38 -38
  25. package/src/binding/index.ts +20 -20
  26. package/src/container/Container.tsx +50 -50
  27. package/src/container/index.ts +1 -1
  28. package/src/cutoff/CutOffPicker.css +12 -12
  29. package/src/cutoff/CutOffPicker.tsx +165 -165
  30. package/src/cutoff/context.tsx +194 -194
  31. package/src/cutoff/dsCutOffDates.ts +21 -21
  32. package/src/cutoff/index.ts +4 -4
  33. package/src/cutoff/utils.test.ts +59 -59
  34. package/src/cutoff/utils.ts +276 -276
  35. package/src/filter/ChipListInput.tsx +79 -79
  36. package/src/filter/FieldFilterPanel.css +150 -150
  37. package/src/filter/FieldFilterPanel.tsx +309 -309
  38. package/src/filter/FilterBuilder.css +152 -152
  39. package/src/filter/FilterBuilder.test.tsx +153 -153
  40. package/src/filter/FilterBuilder.tsx +435 -435
  41. package/src/filter/FilterEditor.css +231 -231
  42. package/src/filter/FilterEditor.test.tsx +285 -285
  43. package/src/filter/FilterEditor.tsx +75 -75
  44. package/src/filter/FilterGroupEditor.tsx +160 -160
  45. package/src/filter/FilterNameDialog.css +22 -22
  46. package/src/filter/FilterNameDialog.tsx +81 -81
  47. package/src/filter/FilterRow.tsx +117 -117
  48. package/src/filter/FilterShareDialog.css +28 -28
  49. package/src/filter/FilterShareDialog.tsx +201 -201
  50. package/src/filter/FilterStringField.tsx +78 -78
  51. package/src/filter/FilterValueEditor.tsx +239 -239
  52. package/src/filter/SavedFilterTree.css +107 -107
  53. package/src/filter/SavedFilterTree.test.tsx +94 -94
  54. package/src/filter/SavedFilterTree.tsx +218 -218
  55. package/src/filter/fieldFilter.test.ts +158 -158
  56. package/src/filter/fieldFilter.ts +139 -139
  57. package/src/filter/index.ts +27 -27
  58. package/src/filter/types.ts +38 -38
  59. package/src/filter/useDistinctValues.test.ts +102 -102
  60. package/src/filter/useDistinctValues.ts +230 -230
  61. package/src/global.d.ts +11 -11
  62. package/src/grid/formatters.ts +23 -0
  63. package/src/grid/index.css +8 -2
  64. package/src/grid/index.tsx +73 -3
  65. package/src/grid/useAfColumns.tsx +28 -4
  66. package/src/grid/useAfGridApi.ts +3 -0
  67. package/src/grid/utcDate.test.ts +25 -0
  68. package/src/grid/utcDate.ts +28 -0
  69. package/src/grid/utcDateEditing.tsx +11 -0
  70. package/src/i18n.ts +13 -13
  71. package/src/index.ts +14 -14
  72. package/src/input/InputAdornments.css +80 -80
  73. package/src/input/InputAdornments.tsx +102 -102
  74. package/src/input/NumberInput.css +9 -9
  75. package/src/input/NumberInput.test.tsx +212 -212
  76. package/src/input/NumberInput.tsx +268 -268
  77. package/src/input/index.ts +3 -3
  78. package/src/input/numberFormat.test.ts +79 -79
  79. package/src/input/numberFormat.ts +94 -94
  80. package/src/layout/AppMenu.tsx +74 -74
  81. package/src/layout/AppMenuDrawer.css +71 -71
  82. package/src/layout/AppMenuDrawer.tsx +122 -122
  83. package/src/layout/AppMenuItem.css +6 -2
  84. package/src/layout/DialogMotion.css +79 -79
  85. package/src/layout/ErrorBoundary.tsx +162 -162
  86. package/src/layout/PageContainer.css +48 -48
  87. package/src/layout/PageContainer.tsx +41 -41
  88. package/src/layout/ResponsiveDialog.css +86 -86
  89. package/src/layout/ResponsiveDialog.tsx +55 -55
  90. package/src/layout/index.tsx +70 -70
  91. package/src/link/Link.tsx +23 -23
  92. package/src/link/index.ts +1 -1
  93. package/src/mdi/Mdi.css +8 -8
  94. package/src/mdi/Mdi.test.tsx +91 -91
  95. package/src/mdi/Mdi.tsx +141 -141
  96. package/src/mdi/MdiConfirmCloseDialog.tsx +47 -47
  97. package/src/mdi/MdiContext.tsx +59 -59
  98. package/src/mdi/MdiDocumentContext.tsx +60 -60
  99. package/src/mdi/MdiPanel.tsx +61 -61
  100. package/src/mdi/MdiRestore.test.tsx +86 -86
  101. package/src/mdi/MdiTab.tsx +30 -30
  102. package/src/mdi/controller.test.ts +382 -382
  103. package/src/mdi/controller.ts +656 -656
  104. package/src/mdi/dockviewHost.ts +132 -132
  105. package/src/mdi/fakeHost.ts +200 -200
  106. package/src/mdi/host.ts +44 -44
  107. package/src/mdi/ids.test.ts +20 -20
  108. package/src/mdi/ids.ts +18 -18
  109. package/src/mdi/index.ts +31 -31
  110. package/src/mdi/layoutRestore.test.ts +156 -156
  111. package/src/mdi/layoutRestore.ts +311 -311
  112. package/src/mdi/layoutSpec.test.ts +143 -143
  113. package/src/mdi/layoutSpec.ts +214 -214
  114. package/src/mdi/layoutStore.test.ts +93 -93
  115. package/src/mdi/layoutStore.ts +99 -99
  116. package/src/mdi/memorySettingsStore.ts +21 -21
  117. package/src/mdi/registry.ts +34 -34
  118. package/src/mdi/testTypes.ts +37 -37
  119. package/src/mdi/theme.ts +22 -22
  120. package/src/mdi/types.ts +214 -214
  121. package/src/page/Page.css +57 -57
  122. package/src/page/PageBlock.css +64 -64
  123. package/src/page/PageBlock.tsx +93 -93
  124. package/src/page/PageHeader.css +88 -88
  125. package/src/page/PageHeader.tsx +115 -115
  126. package/src/page/PageTitle.css +5 -5
  127. package/src/page/PageTitle.tsx +21 -21
  128. package/src/page/index.ts +4 -4
  129. package/src/paper/Paper.tsx +52 -52
  130. package/src/progress/LinearProgress.css +121 -121
  131. package/src/progress/LinearProgress.tsx +106 -106
  132. package/src/progress/index.ts +1 -1
  133. package/src/report-downloader/ReportDownloader.css +67 -67
  134. package/src/report-downloader/components/StatusItem.tsx +87 -87
  135. package/src/report-downloader/components/StatusList.tsx +28 -28
  136. package/src/report-downloader/index.ts +41 -41
  137. package/src/report-downloader/status.tsx +92 -92
  138. package/src/sortable/DropIndicator.css +51 -51
  139. package/src/sortable/DropIndicator.tsx +27 -27
  140. package/src/sortable/SortableItemList.css +21 -21
  141. package/src/sortable/SortableItemList.tsx +264 -264
  142. package/src/sortable/index.ts +8 -8
  143. package/src/sortable/reorderSortOrder.ts +126 -126
  144. package/src/sortable/sortOrder.ts +101 -101
  145. package/src/sortable/types.ts +9 -9
  146. package/src/sortable/useSortableContainer.ts +53 -53
  147. package/src/sortable/useSortableItem.ts +95 -95
  148. package/src/sortable/useSortableList.ts +117 -117
  149. package/src/test/happy-dom-document-class.ts +24 -24
  150. package/src/test/setup.ts +27 -27
  151. package/src/theme/useDsColorScheme.ts +53 -53
  152. package/tsconfig.build.json +9 -9
  153. package/tsconfig.json +8 -8
  154. package/vitest.config.ts +10 -10
@@ -28,8 +28,15 @@ import React, { useMemo } from "react";
28
28
  import { AfHeaderFilterCell } from "./AfHeaderFilterCell.js";
29
29
  import { deleteRow } from "./editing.js";
30
30
  import { operators } from "./filter.js";
31
- import { dateTimeValueFormatter, dateValueFormatter } from "./formatters.js";
31
+ import {
32
+ dateTimeValueFormatter,
33
+ dateValueFormatter,
34
+ localDateTimeValueFormatter,
35
+ localDateValueFormatter,
36
+ } from "./formatters.js";
32
37
  import { isNewItemRow } from "./useAfNewItemRow.js";
38
+ import { parseUtcDate, parseUtcDateTime } from "./utcDate.js";
39
+ import { renderUtcDateEditCell, renderUtcDateTimeEditCell } from "./utcDateEditing.js";
33
40
  import { getLocalizedString } from "../i18n.js";
34
41
 
35
42
  /** `auto-generated-group-footer-`, without spelling it out: every group's total row shares it. */
@@ -48,6 +55,12 @@ export type AfGridColDef = GridColDef & {
48
55
  * from server-side sort and filter operations.
49
56
  */
50
57
  calculated?: boolean;
58
+ /**
59
+ * Date and dateTime columns hold wall-clock values by default: stored without a time zone,
60
+ * shown and edited as they are. Set this for columns holding real UTC instants (such as
61
+ * Created/Updated in newer tables) to show and edit them in the user's local time.
62
+ */
63
+ storedAsUtc?: boolean;
51
64
  };
52
65
 
53
66
  /**
@@ -133,10 +146,19 @@ export function afColumnsToMuiColumn({
133
146
 
134
147
  let valueFormatter: (value: any) => string | number | boolean | object | null | undefined = (value) => value;
135
148
 
149
+ // Wall-clock dates are shown and edited in UTC (see utcDate); real UTC instants
150
+ // use local time and MUI's own editors
151
+ let storedAsUtc = typeof column !== "string" && !!column.storedAsUtc;
152
+ let dateEditing: Pick<GridColDef, "renderEditCell" | "valueParser"> = {};
153
+
136
154
  if (type === "date") {
137
- valueFormatter = dateValueFormatter;
155
+ valueFormatter = storedAsUtc ? localDateValueFormatter : dateValueFormatter;
156
+ if (!storedAsUtc) dateEditing = { renderEditCell: renderUtcDateEditCell, valueParser: parseUtcDate };
138
157
  } else if (type === "dateTime") {
139
- valueFormatter = dateTimeValueFormatter;
158
+ valueFormatter = storedAsUtc ? localDateTimeValueFormatter : dateTimeValueFormatter;
159
+ if (!storedAsUtc) {
160
+ dateEditing = { renderEditCell: renderUtcDateTimeEditCell, valueParser: parseUtcDateTime };
161
+ }
140
162
  }
141
163
 
142
164
  let editable = allowUpdate && !fieldDef?.ignored && !fieldDef?.computed && !fieldDef?.joined;
@@ -155,18 +177,20 @@ export function afColumnsToMuiColumn({
155
177
  type: type as GridColType,
156
178
  headerName: column,
157
179
  valueFormatter,
180
+ ...dateEditing,
158
181
  editable,
159
182
  align,
160
183
  headerAlign: align,
161
184
  width: currentWidth ?? 150,
162
185
  };
163
186
  } else {
164
- let { hide, calculated, ...rest } = column;
187
+ let { hide, calculated, storedAsUtc: _storedAsUtc, ...rest } = column;
165
188
  col = {
166
189
  headerName: fieldDef?.caption ?? fieldName,
167
190
  editable,
168
191
  type: type as GridColType,
169
192
  valueFormatter,
193
+ ...dateEditing,
170
194
  align,
171
195
  headerAlign: align,
172
196
  ...rest,
@@ -62,6 +62,9 @@ export function useAfGridApi(props: Partial<AfGridProps>, apiRef: React.RefObjec
62
62
  );
63
63
  }
64
64
  apiRef.current?.setPinnedColumns(propsRef.current.initialState?.pinnedColumns ?? {});
65
+ // Grouping and aggregation are part of the layout too: back to what the grid declares.
66
+ apiRef.current?.setRowGroupingModel(propsRef.current.initialState?.rowGrouping?.model ?? []);
67
+ apiRef.current?.setAggregationModel(propsRef.current.initialState?.aggregation?.model ?? {});
65
68
  }, [apiRef]);
66
69
 
67
70
  if (apiRef.current) {
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { fromUtcInputValue, parseUtcDate, parseUtcDateTime, toUtcInputValue } from "./utcDate.js";
4
+
5
+ describe("UTC date editing in AfGrid", () => {
6
+ it("stores an entered date as UTC midnight of that date", () => {
7
+ expect(parseUtcDate("2026-10-10")?.toISOString()).toBe("2026-10-10T00:00:00.000Z");
8
+ });
9
+
10
+ it("stores an entered date and time as that wall-clock time in UTC", () => {
11
+ expect(parseUtcDateTime("2026-10-10T07:30")?.toISOString()).toBe("2026-10-10T07:30:00.000Z");
12
+ });
13
+
14
+ it("shows a stored date as its UTC date", () => {
15
+ let stored = new Date("2026-10-10T00:00:00Z");
16
+ expect(toUtcInputValue(stored, false)).toBe("2026-10-10");
17
+ expect(toUtcInputValue(stored, true)).toBe("2026-10-10T00:00");
18
+ });
19
+
20
+ it("treats empty and invalid values as no date", () => {
21
+ expect(fromUtcInputValue("", false)).toBeNull();
22
+ expect(fromUtcInputValue(null, true)).toBeNull();
23
+ expect(toUtcInputValue(new Date("invalid"), false)).toBe("");
24
+ });
25
+ });
@@ -0,0 +1,28 @@
1
+ // Appframe dates are wall-clock values stored as UTC (the grid formats them with
2
+ // timeZone "utc"). MUI's own date editors read and write local time, which shifts a date
3
+ // entered in Norway to 22:00 the day before, so date columns are edited in UTC throughout.
4
+
5
+ export function toUtcInputValue(value: unknown, withTime: boolean): string {
6
+ if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
7
+ return "";
8
+ }
9
+
10
+ return value.toISOString().slice(0, withTime ? 16 : 10);
11
+ }
12
+
13
+ export function fromUtcInputValue(value: unknown, withTime: boolean): Date | null {
14
+ if (typeof value !== "string" || !value) {
15
+ return null;
16
+ }
17
+
18
+ let date = new Date(withTime ? `${value.slice(0, 16)}:00Z` : `${value.slice(0, 10)}T00:00:00Z`);
19
+ return Number.isNaN(date.getTime()) ? null : date;
20
+ }
21
+
22
+ export function parseUtcDate(value: unknown) {
23
+ return fromUtcInputValue(value, false);
24
+ }
25
+
26
+ export function parseUtcDateTime(value: unknown) {
27
+ return fromUtcInputValue(value, true);
28
+ }
@@ -0,0 +1,11 @@
1
+ import { GridEditInputCell, type GridRenderEditCellParams } from "@mui/x-data-grid-premium";
2
+
3
+ import { toUtcInputValue } from "./utcDate.js";
4
+
5
+ export function renderUtcDateEditCell(params: GridRenderEditCellParams) {
6
+ return <GridEditInputCell {...params} type="date" value={toUtcInputValue(params.value, false)} />;
7
+ }
8
+
9
+ export function renderUtcDateTimeEditCell(params: GridRenderEditCellParams) {
10
+ return <GridEditInputCell {...params} type="datetime-local" value={toUtcInputValue(params.value, true)} />;
11
+ }
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";
@@ -1,80 +1,80 @@
1
- /*
2
- * The wrapper is the visual input: it owns the border, background, hover and
3
- * focus styling, while the inner Designsystemet input is rendered bare so the
4
- * input and the adornments read as a single control.
5
- */
6
- .ObInputAdornments-root {
7
- --obInputAdornments-padding: 0.25rem;
8
- --obInputAdornments-gap: 0.25rem;
9
- --obInputAdornments-fontSize: var(--ds-font-size-3, 1rem);
10
-
11
- display: flex;
12
- align-items: center;
13
- box-sizing: border-box;
14
- gap: var(--obInputAdornments-gap);
15
- padding: var(--obInputAdornments-padding);
16
- inline-size: 100%;
17
- color: var(--ds-color-neutral-text-default, #1e2b3c);
18
- background-color: var(--ds-color-neutral-surface-default, #fff);
19
- border: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-default, #b3b3b3);
20
- border-radius: var(--ds-border-radius-md, 0.375rem);
21
- }
22
-
23
- /* Designsystemet components come in sm, md and lg; xs and xl are typography-only sizes */
24
- .ObInputAdornments-root[data-size="sm"] {
25
- --obInputAdornments-padding: 0.25rem;
26
- --obInputAdornments-fontSize: var(--ds-font-size-2, 0.875rem);
27
- }
28
-
29
- .ObInputAdornments-root[data-size="md"] {
30
- --obInputAdornments-padding: 0.25rem;
31
- --obInputAdornments-fontSize: var(--ds-font-size-3, 1rem);
32
- }
33
-
34
- .ObInputAdornments-root[data-size="lg"] {
35
- --obInputAdornments-padding: 0.375rem;
36
- --obInputAdornments-fontSize: var(--ds-font-size-4, 1.125rem);
37
- }
38
-
39
- /* The Designsystemet input hover (an inner outline doubling the border), drawn on the wrapper */
40
- .ObInputAdornments-root:hover:not(
41
- :focus-within,
42
- :has(.ObInputAdornments-field:disabled, .ObInputAdornments-field[readonly])
43
- ) {
44
- outline: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-default, #b3b3b3);
45
- outline-offset: -0.1px;
46
- }
47
-
48
- .ObInputAdornments-root:has(.ObInputAdornments-field:focus-visible) {
49
- box-shadow: var(--dsc-focus-boxShadow);
50
- outline: var(--dsc-focus-outline);
51
- outline-offset: var(--ds-border-width-focus, 3px);
52
- }
53
-
54
- .ObInputAdornments-root:has(.ObInputAdornments-field:disabled) {
55
- cursor: not-allowed;
56
- opacity: var(--ds-disabled-opacity, 0.3);
57
- }
58
-
59
- /* The bare input: all the visual styling lives on the wrapper instead */
60
- .ObInputAdornments-root .ObInputAdornments-field {
61
- flex: 1;
62
- min-inline-size: 0;
63
- font-size: var(--obInputAdornments-fontSize);
64
- background: transparent;
65
- border: none;
66
- }
67
-
68
- .ObInputAdornments-root .ObInputAdornments-field:disabled {
69
- opacity: 1;
70
- }
71
-
72
- .ObInputAdornments-root .ObInputAdornments-field:hover {
73
- outline: none;
74
- }
75
-
76
- .ObInputAdornments-root .ObInputAdornments-field:focus-visible {
77
- box-shadow: none;
78
- outline: none;
79
- outline-offset: 0;
80
- }
1
+ /*
2
+ * The wrapper is the visual input: it owns the border, background, hover and
3
+ * focus styling, while the inner Designsystemet input is rendered bare so the
4
+ * input and the adornments read as a single control.
5
+ */
6
+ .ObInputAdornments-root {
7
+ --obInputAdornments-padding: 0.25rem;
8
+ --obInputAdornments-gap: 0.25rem;
9
+ --obInputAdornments-fontSize: var(--ds-font-size-3, 1rem);
10
+
11
+ display: flex;
12
+ align-items: center;
13
+ box-sizing: border-box;
14
+ gap: var(--obInputAdornments-gap);
15
+ padding: var(--obInputAdornments-padding);
16
+ inline-size: 100%;
17
+ color: var(--ds-color-neutral-text-default, #1e2b3c);
18
+ background-color: var(--ds-color-neutral-surface-default, #fff);
19
+ border: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-default, #b3b3b3);
20
+ border-radius: var(--ds-border-radius-md, 0.375rem);
21
+ }
22
+
23
+ /* Designsystemet components come in sm, md and lg; xs and xl are typography-only sizes */
24
+ .ObInputAdornments-root[data-size="sm"] {
25
+ --obInputAdornments-padding: 0.25rem;
26
+ --obInputAdornments-fontSize: var(--ds-font-size-2, 0.875rem);
27
+ }
28
+
29
+ .ObInputAdornments-root[data-size="md"] {
30
+ --obInputAdornments-padding: 0.25rem;
31
+ --obInputAdornments-fontSize: var(--ds-font-size-3, 1rem);
32
+ }
33
+
34
+ .ObInputAdornments-root[data-size="lg"] {
35
+ --obInputAdornments-padding: 0.375rem;
36
+ --obInputAdornments-fontSize: var(--ds-font-size-4, 1.125rem);
37
+ }
38
+
39
+ /* The Designsystemet input hover (an inner outline doubling the border), drawn on the wrapper */
40
+ .ObInputAdornments-root:hover:not(
41
+ :focus-within,
42
+ :has(.ObInputAdornments-field:disabled, .ObInputAdornments-field[readonly])
43
+ ) {
44
+ outline: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-default, #b3b3b3);
45
+ outline-offset: -0.1px;
46
+ }
47
+
48
+ .ObInputAdornments-root:has(.ObInputAdornments-field:focus-visible) {
49
+ box-shadow: var(--dsc-focus-boxShadow);
50
+ outline: var(--dsc-focus-outline);
51
+ outline-offset: var(--ds-border-width-focus, 3px);
52
+ }
53
+
54
+ .ObInputAdornments-root:has(.ObInputAdornments-field:disabled) {
55
+ cursor: not-allowed;
56
+ opacity: var(--ds-disabled-opacity, 0.3);
57
+ }
58
+
59
+ /* The bare input: all the visual styling lives on the wrapper instead */
60
+ .ObInputAdornments-root .ObInputAdornments-field {
61
+ flex: 1;
62
+ min-inline-size: 0;
63
+ font-size: var(--obInputAdornments-fontSize);
64
+ background: transparent;
65
+ border: none;
66
+ }
67
+
68
+ .ObInputAdornments-root .ObInputAdornments-field:disabled {
69
+ opacity: 1;
70
+ }
71
+
72
+ .ObInputAdornments-root .ObInputAdornments-field:hover {
73
+ outline: none;
74
+ }
75
+
76
+ .ObInputAdornments-root .ObInputAdornments-field:focus-visible {
77
+ box-shadow: none;
78
+ outline: none;
79
+ outline-offset: 0;
80
+ }
@@ -1,102 +1,102 @@
1
- import "./InputAdornments.css";
2
-
3
- import { Input, type InputProps } from "@digdir/designsystemet-react";
4
- import clsx from "clsx";
5
- import { Children, isValidElement, type ReactElement, type ReactNode, type Ref } from "react";
6
-
7
- export type InputSlotProps = {
8
- /**
9
- * Extra classes for the input rendered in this slot
10
- */
11
- className?: string;
12
- };
13
-
14
- /**
15
- * Marks where the input should be rendered among the children of
16
- * `InputAdornments`. Renders nothing on its own; `InputAdornments` swaps it
17
- * for the actual input. When it is left out, the input is rendered before the
18
- * adornments.
19
- *
20
- * @example
21
- * ```jsx
22
- * <InputAdornments value={value} onChange={handleChange}>
23
- * <SearchIcon aria-hidden />
24
- * <InputSlot />
25
- * <Button icon variant="tertiary"><XMarkIcon aria-hidden /></Button>
26
- * </InputAdornments>
27
- * ```
28
- */
29
- export function InputSlot(_props: InputSlotProps) {
30
- return null;
31
- }
32
-
33
- export type InputAdornmentsProps = Omit<InputProps, "children"> & {
34
- /**
35
- * Adornments rendered next to the input, e.g. icons or buttons. Include an
36
- * `InputSlot` to control where the input itself ends up.
37
- */
38
- children?: ReactNode;
39
- /**
40
- * Classes for the wrapper that carries the input styling
41
- */
42
- className?: string;
43
- /**
44
- * Classes for the bare input element itself
45
- */
46
- inputClassName?: string;
47
- /**
48
- * Ref to the input element. The wrapper is an implementation detail, so the
49
- * ref points at the input rather than at the element carrying the styling.
50
- */
51
- ref?: Ref<HTMLInputElement>;
52
- };
53
-
54
- /**
55
- * Input with adornments, equivalent to a MUI `TextField` with
56
- * `InputProps.startAdornment` / `endAdornment`. Designsystemet has no
57
- * equivalent, so the border, background, hover and focus styling is moved from
58
- * the input onto a wrapper element, making the input and its adornments read
59
- * as a single control.
60
- *
61
- * All other props are forwarded to the Designsystemet `Input`, so `ref`,
62
- * `value`, `onChange`, `data-size` and `data-color` work as usual.
63
- *
64
- * @example
65
- * ```jsx
66
- * // Input first (default), adornments after
67
- * <InputAdornments aria-label="Search" value={value} onChange={handleChange}>
68
- * <Button icon variant="tertiary"><MagnifyingGlassIcon aria-hidden /></Button>
69
- * </InputAdornments>
70
- *
71
- * // Explicit placement with InputSlot
72
- * <InputAdornments aria-label="Amount" data-size="lg">
73
- * <span aria-hidden>kr</span>
74
- * <InputSlot />
75
- * <span aria-hidden>,-</span>
76
- * </InputAdornments>
77
- * ```
78
- */
79
- export function InputAdornments({ children, className, inputClassName, ...props }: InputAdornmentsProps) {
80
- let size = props["data-size"];
81
- let color = props["data-color"];
82
- let childArray = Children.toArray(children);
83
- let slot = childArray.find((child) => isValidElement(child) && child.type === InputSlot) as
84
- | ReactElement<InputSlotProps>
85
- | undefined;
86
-
87
- let input = (
88
- <Input
89
- key="ObInputAdornments-input"
90
- {...props}
91
- className={clsx("ObInputAdornments-field", inputClassName, slot?.props.className)}
92
- />
93
- );
94
-
95
- let content = slot ? childArray.map((child) => (child === slot ? input : child)) : [input, ...childArray];
96
-
97
- return (
98
- <div className={clsx("ObInputAdornments-root", className)} data-size={size} data-color={color}>
99
- {content}
100
- </div>
101
- );
102
- }
1
+ import "./InputAdornments.css";
2
+
3
+ import { Input, type InputProps } from "@digdir/designsystemet-react";
4
+ import clsx from "clsx";
5
+ import { Children, isValidElement, type ReactElement, type ReactNode, type Ref } from "react";
6
+
7
+ export type InputSlotProps = {
8
+ /**
9
+ * Extra classes for the input rendered in this slot
10
+ */
11
+ className?: string;
12
+ };
13
+
14
+ /**
15
+ * Marks where the input should be rendered among the children of
16
+ * `InputAdornments`. Renders nothing on its own; `InputAdornments` swaps it
17
+ * for the actual input. When it is left out, the input is rendered before the
18
+ * adornments.
19
+ *
20
+ * @example
21
+ * ```jsx
22
+ * <InputAdornments value={value} onChange={handleChange}>
23
+ * <SearchIcon aria-hidden />
24
+ * <InputSlot />
25
+ * <Button icon variant="tertiary"><XMarkIcon aria-hidden /></Button>
26
+ * </InputAdornments>
27
+ * ```
28
+ */
29
+ export function InputSlot(_props: InputSlotProps) {
30
+ return null;
31
+ }
32
+
33
+ export type InputAdornmentsProps = Omit<InputProps, "children"> & {
34
+ /**
35
+ * Adornments rendered next to the input, e.g. icons or buttons. Include an
36
+ * `InputSlot` to control where the input itself ends up.
37
+ */
38
+ children?: ReactNode;
39
+ /**
40
+ * Classes for the wrapper that carries the input styling
41
+ */
42
+ className?: string;
43
+ /**
44
+ * Classes for the bare input element itself
45
+ */
46
+ inputClassName?: string;
47
+ /**
48
+ * Ref to the input element. The wrapper is an implementation detail, so the
49
+ * ref points at the input rather than at the element carrying the styling.
50
+ */
51
+ ref?: Ref<HTMLInputElement>;
52
+ };
53
+
54
+ /**
55
+ * Input with adornments, equivalent to a MUI `TextField` with
56
+ * `InputProps.startAdornment` / `endAdornment`. Designsystemet has no
57
+ * equivalent, so the border, background, hover and focus styling is moved from
58
+ * the input onto a wrapper element, making the input and its adornments read
59
+ * as a single control.
60
+ *
61
+ * All other props are forwarded to the Designsystemet `Input`, so `ref`,
62
+ * `value`, `onChange`, `data-size` and `data-color` work as usual.
63
+ *
64
+ * @example
65
+ * ```jsx
66
+ * // Input first (default), adornments after
67
+ * <InputAdornments aria-label="Search" value={value} onChange={handleChange}>
68
+ * <Button icon variant="tertiary"><MagnifyingGlassIcon aria-hidden /></Button>
69
+ * </InputAdornments>
70
+ *
71
+ * // Explicit placement with InputSlot
72
+ * <InputAdornments aria-label="Amount" data-size="lg">
73
+ * <span aria-hidden>kr</span>
74
+ * <InputSlot />
75
+ * <span aria-hidden>,-</span>
76
+ * </InputAdornments>
77
+ * ```
78
+ */
79
+ export function InputAdornments({ children, className, inputClassName, ...props }: InputAdornmentsProps) {
80
+ let size = props["data-size"];
81
+ let color = props["data-color"];
82
+ let childArray = Children.toArray(children);
83
+ let slot = childArray.find((child) => isValidElement(child) && child.type === InputSlot) as
84
+ | ReactElement<InputSlotProps>
85
+ | undefined;
86
+
87
+ let input = (
88
+ <Input
89
+ key="ObInputAdornments-input"
90
+ {...props}
91
+ className={clsx("ObInputAdornments-field", inputClassName, slot?.props.className)}
92
+ />
93
+ );
94
+
95
+ let content = slot ? childArray.map((child) => (child === slot ? input : child)) : [input, ...childArray];
96
+
97
+ return (
98
+ <div className={clsx("ObInputAdornments-root", className)} data-size={size} data-color={color}>
99
+ {content}
100
+ </div>
101
+ );
102
+ }
@@ -1,9 +1,9 @@
1
- .ObNumberInput-root .ObInputAdornments-field {
2
- /* Numbers read best right aligned, and the steppers sit at the end anyway */
3
- text-align: end;
4
- font-variant-numeric: tabular-nums;
5
- }
6
-
7
- .ObNumberInput-root .ObNumberInput-stepper {
8
- flex-shrink: 0;
9
- }
1
+ .ObNumberInput-root .ObInputAdornments-field {
2
+ /* Numbers read best right aligned, and the steppers sit at the end anyway */
3
+ text-align: end;
4
+ font-variant-numeric: tabular-nums;
5
+ }
6
+
7
+ .ObNumberInput-root .ObNumberInput-stepper {
8
+ flex-shrink: 0;
9
+ }