@olenbetong/appframe-ds 0.7.0 → 0.9.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 (76) hide show
  1. package/CHANGELOG.md +175 -0
  2. package/package.json +52 -8
  3. package/scripts/copyAssets.mjs +13 -0
  4. package/src/AfLookup/LookupCombobox.css +1 -1
  5. package/src/AfLookup/LookupCombobox.tsx +3 -1
  6. package/src/autocomplete/Autocomplete.css +63 -42
  7. package/src/autocomplete/Autocomplete.tsx +2 -0
  8. package/src/autocomplete/AutocompleteFrame.tsx +21 -2
  9. package/src/autocomplete/Combobox.tsx +15 -2
  10. package/src/autocomplete/types.ts +4 -0
  11. package/src/container/Container.tsx +49 -0
  12. package/src/container/index.ts +1 -0
  13. package/src/filter/ChipListInput.tsx +79 -0
  14. package/src/filter/FieldFilterPanel.css +127 -0
  15. package/src/filter/FieldFilterPanel.tsx +309 -0
  16. package/src/filter/FilterBuilder.css +152 -0
  17. package/src/filter/FilterBuilder.test.tsx +153 -0
  18. package/src/filter/FilterBuilder.tsx +435 -0
  19. package/src/filter/FilterEditor.css +231 -0
  20. package/src/filter/FilterEditor.test.tsx +259 -0
  21. package/src/filter/FilterEditor.tsx +74 -0
  22. package/src/filter/FilterGroupEditor.tsx +160 -0
  23. package/src/filter/FilterNameDialog.css +22 -0
  24. package/src/filter/FilterNameDialog.tsx +81 -0
  25. package/src/filter/FilterRow.tsx +117 -0
  26. package/src/filter/FilterShareDialog.css +28 -0
  27. package/src/filter/FilterShareDialog.tsx +201 -0
  28. package/src/filter/FilterStringField.tsx +77 -0
  29. package/src/filter/FilterValueEditor.tsx +220 -0
  30. package/src/filter/SavedFilterTree.css +107 -0
  31. package/src/filter/SavedFilterTree.test.tsx +94 -0
  32. package/src/filter/SavedFilterTree.tsx +222 -0
  33. package/src/filter/fieldFilter.test.ts +158 -0
  34. package/src/filter/fieldFilter.ts +139 -0
  35. package/src/filter/index.ts +27 -0
  36. package/src/filter/types.ts +38 -0
  37. package/src/filter/useDistinctValues.test.ts +102 -0
  38. package/src/filter/useDistinctValues.ts +230 -0
  39. package/src/global.d.ts +11 -0
  40. package/src/grid/AfGridColumnsPanel.tsx +118 -0
  41. package/src/grid/AfGridContext.tsx +51 -0
  42. package/src/grid/AfGridError.tsx +46 -0
  43. package/src/grid/AfHeaderFilterCell.tsx +151 -0
  44. package/src/grid/AfHeaderFilterPanel.tsx +111 -0
  45. package/src/grid/Toolbar.tsx +233 -0
  46. package/src/grid/editing.ts +90 -0
  47. package/src/grid/filter.ts +78 -0
  48. package/src/grid/formatters.ts +48 -0
  49. package/src/grid/index.css +187 -0
  50. package/src/grid/index.tsx +376 -0
  51. package/src/grid/license.ts +48 -0
  52. package/src/grid/localization.ts +369 -0
  53. package/src/grid/slots/index.tsx +275 -0
  54. package/src/grid/slots/slots.css +53 -0
  55. package/src/grid/theme.tsx +185 -0
  56. package/src/grid/useAfColumns.tsx +313 -0
  57. package/src/grid/useAfCurrentIndex.ts +55 -0
  58. package/src/grid/useAfData.ts +20 -0
  59. package/src/grid/useAfFilter.ts +160 -0
  60. package/src/grid/useAfFilterFields.ts +55 -0
  61. package/src/grid/useAfGridApi.ts +66 -0
  62. package/src/grid/useAfKeyBindings.ts +36 -0
  63. package/src/grid/useAfPagination.ts +53 -0
  64. package/src/grid/useAfPersistedState.ts +222 -0
  65. package/src/grid/useAfRowEditModel.ts +137 -0
  66. package/src/grid/useAfSortModel.ts +72 -0
  67. package/src/index.ts +2 -0
  68. package/src/input/InputAdornments.tsx +6 -1
  69. package/src/paper/Paper.tsx +2 -0
  70. package/src/report-downloader/ReportDownloader.css +67 -0
  71. package/src/report-downloader/components/StatusItem.tsx +86 -0
  72. package/src/report-downloader/components/StatusList.tsx +28 -0
  73. package/src/report-downloader/index.ts +41 -0
  74. package/src/report-downloader/status.tsx +92 -0
  75. package/src/test/setup.ts +8 -0
  76. package/vitest.config.ts +10 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,180 @@
1
1
  # @olenbetong/appframe-ds
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cfc94c7: Rework the `AfGrid` header filter, and stop the toolbar refresh button from shifting while loading.
8
+
9
+ The header filters and the toolbar's filter builder now edit **one** filter tree in a single named
10
+ filter slot keyed on the grid `id`, instead of writing two slots that could not see each other. MUI's
11
+ `GridFilterModel` is no longer used as a transport — it cannot represent nested groups or the full
12
+ Appframe operator set. `useAfFilterModel` is replaced by `useAfFilter`, which exposes the tree plus
13
+ per-field helpers.
14
+
15
+ Each header filter cell is now an unlabelled input with a funnel button:
16
+ - one criterion taking a single value is edited straight from the input, debounced,
17
+ - anything else shows a read-only summary such as `In list (3)`, with a clear button,
18
+ - the funnel opens a per-column panel with a row per criterion, a `+ Criteria` button, and — for
19
+ string, number and uniqueidentifier columns — a searchable list of the column's distinct values
20
+ with row counts, checked to build an `in list` criterion.
21
+
22
+ The distinct list respects every other active filter but ignores the column's own criteria, so
23
+ unchecking a value does not make the remaining values disappear. Counts are best effort: views that
24
+ cannot be grouped fall back to a plain distinct list. Panel edits are staged and only applied on
25
+ `Apply`, so a half-built criterion never reaches the server.
26
+
27
+ `GridToolbarRefreshButton` now hides its icon while loading so the spinner replaces it instead of
28
+ being added next to it, and the columns panel has spacing between its search field and the column
29
+ list. `InputAdornments` accepts a `ref`, which is forwarded to the input element.
30
+
31
+ The panel body is exported on its own as **`FieldFilterPanel`**, a controlled component with no
32
+ dependency on the grid, so a single field can be filtered anywhere — a report parameter, a sidebar
33
+ facet, a mobile filter sheet. Its distinct value query is exported too, as `useDistinctValues`.
34
+
35
+ The header filter input no longer drives MUI's header filter edit mode from `onFocus`/`onBlur`. The
36
+ header filter cell refocuses itself whenever that state changes, so any mousedown leaving the input
37
+ pulled focus straight back in — inputs in the filter panel could not be focused, and starting a
38
+ column drag from a header cancelled the drag. Nothing is lost: MUI focuses the cell's focusable
39
+ element anyway, so typing and keyboard navigation still work.
40
+
41
+ - cfc94c7: Add `@olenbetong/appframe-ds/grid`, a Designsystemet flavoured `AfGrid`.
42
+
43
+ The grid keeps MUI X Data Grid Pro as its engine — Designsystemet has no data grid — but replaces
44
+ the chrome:
45
+ - the base slots (checkbox, button, icon button, text field, select, switch, tooltip, chip, badge,
46
+ progress, skeleton, divider) render Designsystemet components,
47
+ - the toolbar and the column panel are rebuilt with Designsystemet components and Aksel icons,
48
+ - the filter panel is replaced by `FilterBuilder` in dialog mode,
49
+ - the MUI theme is derived from the `--ds-color-*` tokens and follows `data-color-scheme`, so the
50
+ grid works in light and dark mode without an app-level `ThemeProvider`,
51
+ - the MUI X license is registered by the grid itself, from a build-time injected
52
+ `__MUI_X_LICENSE_KEY__` (supplied by the `appframe()` Vite plugin) or
53
+ `import.meta.env.VITE_MUI_X_LICENSE_KEY`.
54
+
55
+ The API matches the `@olenbetong/appframe-mui` `AfGrid`, so migrating is an import path change plus
56
+ an optional `filterViewName` for saved filters. Layout persistence uses the same store, so users
57
+ keep their saved column layouts.
58
+
59
+ The filter builder (`FilterBuilder`, `FilterEditor`, `SavedFilterTree`, …) moved here from
60
+ `@olenbetong/synergi-react` and is exported from the package root.
61
+
62
+ `ReportDownloader` (`openDataObjectReportWithProgress`) is available from
63
+ `@olenbetong/appframe-ds/report-downloader`, built with Designsystemet components.
64
+
65
+ ### Patch Changes
66
+
67
+ - Updated dependencies [62a0ad5]
68
+ - Updated dependencies [5155cdb]
69
+ - @olenbetong/appframe-data@1.6.1
70
+ - @olenbetong/appframe-core@2.13.0
71
+ - @olenbetong/appframe-react@2.1.1
72
+
73
+ ## 0.8.0
74
+
75
+ ### Minor Changes
76
+
77
+ - b50190d: Add a `Container` component: a centered, max-width content wrapper equivalent to MUI `Container`. It is a thin alias over `Page.Block` with gutters enabled by default and a MUI-familiar `maxWidth` prop (`xs` | `sm` | `md` | `lg` | `xl` | `false`).
78
+ - 27ef468: Add `FilterEditor`, the criteria tree of the web filter builder.
79
+
80
+ `FilterEditor` renders a nested criteria tree, matching the advanced filter
81
+ editor in the Windows client: groups can be nested to any depth and each group
82
+ chooses whether its items are combined with `and` or `or`. An empty filter is a
83
+ single `and` group. Every group ends with
84
+ a blank row, so a condition is added just by picking a field. Value editors are
85
+ chosen from the field type and the operator's arity, and cover text, number and
86
+ date inputs, ranges, value lists and relative dates such as "Today" or "Current
87
+ user". Set `showFilterString` to show the equivalent filter string, which can
88
+ also be edited and parsed back into the tree. A group's conditions are indented
89
+ and boxed below its header so it is clear which group they belong to. On narrow
90
+ viewports, where the parts of a condition wrap onto several lines, conditions are
91
+ separated by extra spacing and a rule, and the group buttons are reduced to
92
+ icons so they no longer overflow.
93
+
94
+ `@olenbetong/appframe-core` gained path-based filter tree helpers —
95
+ `getNodeAtPath`, `updateNodeAtPath`, `insertNodeAtPath`, `removeNodeAtPath`,
96
+ `setGroupMode` and `addGroupAtPath` — replacing the group/item index helpers
97
+ `addExpression`, `updateExpression`, `removeExpression`, `addGroup` and
98
+ `removeGroup`, which only worked on the flat standard layout. `pruneFilter` now
99
+ prunes nested groups, `toFilterTree` normalizes a filter without flattening it,
100
+ and `isStandardLayout` reports whether a filter fits the simplified layout the
101
+ Windows flow editor can render.
102
+
103
+ `Combobox` in `@olenbetong/appframe-ds` gained two related fixes: it now accepts
104
+ `aria-label` for use without a visible label, and the input shows the selected
105
+ option's label on mount and whenever the selection is changed from the outside.
106
+ `Combobox` also no longer renders text one step smaller than a `Textfield` or
107
+ `Select` of the same `data-size`. Designsystemet inputs inherit their font size
108
+ when `data-size` is set, and `data-size` rescales `--ds-font-size-*`, so
109
+ referencing the type scale at all made the text smaller — `Combobox` now
110
+ inherits its font size the same way `Textfield` does, and has a minimum height
111
+ matching `--dsc-input-size` so it lines up exactly with an input beside it.
112
+
113
+ `Combobox` options were also unreadable in dark mode: the hovered and selected
114
+ states referenced `--ds-color-accent-*`, a colour set that does not exist in
115
+ these themes, so every option fell back to a hard-coded light background while
116
+ the text stayed light. The option states now use the contextual `--ds-color-*`
117
+ aliases, and all hard-coded colour fallbacks — which only ever encoded the light
118
+ theme — have been removed from `Combobox` and `AfLookup`.
119
+
120
+ ### Patch Changes
121
+
122
+ - b50190d: `Paper` now accepts a `ref`, which is forwarded to the rendered element.
123
+ - 3ff7c84: Depend on the workspace versions of `@olenbetong/appframe-core`, `@olenbetong/appframe-data` and `@olenbetong/appframe-react` instead of the published versions from the catalog, so the package is built and tested against the current source like the other workspace packages.
124
+ - a0df74a: Add the report metadata model, `useReport` and `ReportParameters`.
125
+
126
+ `@olenbetong/appframe-core` gained a pure report model: `Report`,
127
+ `ReportParameter`, `ReportRecordSource` and their raw record counterparts, plus
128
+ `toReport`, `getReportFormats`, `parseRecordSource` and the parameter helpers
129
+ `toReportParameters`, `coerceParameterValue`, `getDefaultParameterValues`,
130
+ `serializeParameters`, `parseParameters` and `restoreParameterValues`. The
131
+ parameters XML is the same `<Parameters Version="1">` document the Windows
132
+ launcher writes, so a filter saved in either client restores in the other. A
133
+ parameter's editor is derived the way the Windows launcher derives it: a lookup
134
+ wins over everything, a fixed item list comes next, and otherwise the data type
135
+ decides. `FormatCSV` enables XLSX and XLS as well, matching the single
136
+ DevExpress export the flag stands for.
137
+
138
+ `@olenbetong/appframe-react` gained `useReport`, which loads a report from
139
+ `sviw_WinClient_MyReports` — so a report that does not come back is one the user
140
+ may not run — merges the `HasTitle`/`HasHeader` bands from
141
+ `sviw_WinClient_ReportsWeb` and the parameters from
142
+ `sviw_WinClient_ReportsParameters`. It returns the filter the report is designed
143
+ with, overridable through an `initialFilter` option the way opening a report
144
+ programmatically overrides it in the Windows client, and the literal parameter
145
+ defaults. Defaults naming a stored procedure are left unset, to be resolved
146
+ server-side. `useParameterOptions` loads the options for a lookup parameter from
147
+ its `afRecordSource`.
148
+
149
+ `@olenbetong/synergi-react` gained `ReportParameters`, the parameter form of the
150
+ report launcher. It renders nothing when a report has no parameters, so the
151
+ launcher can include it unconditionally, and gives each parameter the editor its
152
+ metadata asks for: a lookup combobox, a select, a checkbox, or a text, number or
153
+ date input. Values are coerced to the parameter's type as they are entered.
154
+
155
+ `Combobox` in `@olenbetong/appframe-ds` had no spacing between its label and its
156
+ control: the root is not a `.ds-field`, which is what normally spaces a label,
157
+ description and input apart, so it now does that spacing itself and lines up
158
+ with a `Textfield` or `Select` beside it.
159
+
160
+ - 1bb10de: `Autocomplete` and `Combobox` now open their option list inside the dialog they
161
+ are used in. The list is portalled, and a modal dialog is painted in the top
162
+ layer, so a list portalled to the body ended up behind the dialog backdrop and
163
+ appeared not to open at all.
164
+ - Updated dependencies [448c378]
165
+ - Updated dependencies [54e5344]
166
+ - Updated dependencies [448c378]
167
+ - Updated dependencies [a0df74a]
168
+ - Updated dependencies [7a54755]
169
+ - Updated dependencies [3954bc9]
170
+ - Updated dependencies [0a23d5b]
171
+ - Updated dependencies [3ff7c84]
172
+ - Updated dependencies [ace635e]
173
+ - Updated dependencies [27ef468]
174
+ - @olenbetong/appframe-react@2.1.0
175
+ - @olenbetong/appframe-core@2.12.0
176
+ - @olenbetong/appframe-data@1.6.0
177
+
3
178
  ## 0.7.0
4
179
 
5
180
  ### Minor Changes
package/package.json CHANGED
@@ -1,12 +1,21 @@
1
1
  {
2
2
  "name": "@olenbetong/appframe-ds",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Components that bind Designsystemet components to Appframe data objects",
5
5
  "type": "module",
6
6
  "types": "./es/index.d.ts",
7
7
  "exports": {
8
8
  ".": {
9
+ "types": "./es/index.d.ts",
9
10
  "import": "./es/index.js"
11
+ },
12
+ "./grid": {
13
+ "types": "./es/grid/index.d.ts",
14
+ "import": "./es/grid/index.js"
15
+ },
16
+ "./report-downloader": {
17
+ "types": "./es/report-downloader/index.d.ts",
18
+ "import": "./es/report-downloader/index.js"
10
19
  }
11
20
  },
12
21
  "sideEffects": [
@@ -16,29 +25,63 @@
16
25
  "license": "MIT",
17
26
  "peerDependencies": {
18
27
  "@digdir/designsystemet-react": ">=1.18.0",
28
+ "@emotion/react": ">=11.14.0",
29
+ "@emotion/styled": ">=11.14.1",
30
+ "@mui/material": ">=9.0.0",
31
+ "@mui/x-data-grid-pro": ">=8.0.0 <9.0.0",
32
+ "@mui/x-license": ">=8.0.0 <9.0.0",
19
33
  "@navikt/aksel-icons": ">=8.16.1",
20
34
  "react": ">=19.2.8",
21
35
  "react-dom": ">=19.2.8",
22
36
  "react-error-boundary": ">=6.1.2",
23
37
  "react-router": ">=8.3.0"
24
38
  },
39
+ "peerDependenciesMeta": {
40
+ "@emotion/react": {
41
+ "optional": true
42
+ },
43
+ "@emotion/styled": {
44
+ "optional": true
45
+ },
46
+ "@mui/material": {
47
+ "optional": true
48
+ },
49
+ "@mui/x-data-grid-pro": {
50
+ "optional": true
51
+ },
52
+ "@mui/x-license": {
53
+ "optional": true
54
+ }
55
+ },
25
56
  "dependencies": {
26
- "@atlaskit/pragmatic-drag-and-drop": "2.0.1",
57
+ "@atlaskit/pragmatic-drag-and-drop": "2.0.2",
27
58
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "2.0.0",
28
59
  "@base-ui/react": "1.7.0",
29
- "@olenbetong/appframe-core": "2.11.10",
30
- "@olenbetong/appframe-data": "1.5.0",
31
- "@olenbetong/appframe-react": "2.0.0",
32
60
  "clsx": "^2.1.1",
61
+ "localforage": "^1.10.0",
62
+ "mitt": "^3.0.1",
33
63
  "react-virtualized-auto-sizer": "^1.0.26",
34
- "react-window": "^1.8.11"
64
+ "react-window": "^1.8.11",
65
+ "@olenbetong/appframe-core": "2.13.0",
66
+ "@olenbetong/appframe-react": "2.1.1",
67
+ "@olenbetong/appframe-data": "1.6.1"
35
68
  },
36
69
  "devDependencies": {
37
70
  "@digdir/designsystemet-react": "1.18.0",
71
+ "@emotion/react": "^11.14.0",
72
+ "@emotion/styled": "^11.14.1",
73
+ "@mui/material": "9.3.1",
74
+ "@mui/x-data-grid-pro": "8.29.2",
75
+ "@mui/x-license": "8.29.2",
38
76
  "@navikt/aksel-icons": "8.16.1",
77
+ "@testing-library/dom": "^10.4.1",
78
+ "@testing-library/jest-dom": "^7.0.1",
79
+ "@testing-library/react": "16.3.2",
80
+ "@testing-library/user-event": "^14.6.3",
39
81
  "@types/react": "19.2.18",
40
82
  "@types/react-dom": "19.2.4",
41
83
  "@types/react-window": "^1.8.8",
84
+ "happy-dom": "^20.11.1",
42
85
  "react": "19.2.8",
43
86
  "react-dom": "19.2.8",
44
87
  "react-error-boundary": "6.1.2",
@@ -46,8 +89,9 @@
46
89
  "typescript": "7.0.2"
47
90
  },
48
91
  "scripts": {
49
- "build": "pnpm run build:tsc && pnpm run build:css",
92
+ "build": "pnpm run build:tsc && pnpm test && pnpm run build:css",
50
93
  "build:tsc": "node --no-warnings ../../scripts/clean.ts ./es tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
51
- "build:css": "node scripts/copyAssets.mjs"
94
+ "build:css": "node scripts/copyAssets.mjs",
95
+ "test": "vitest run"
52
96
  }
53
97
  }
@@ -22,6 +22,15 @@ const files = [
22
22
  ["src/sortable/DropIndicator.css", "es/sortable/DropIndicator.css"],
23
23
  ["src/sortable/SortableItemList.css", "es/sortable/SortableItemList.css"],
24
24
  ["src/autocomplete/Autocomplete.css", "es/autocomplete/Autocomplete.css"],
25
+ ["src/filter/FilterBuilder.css", "es/filter/FilterBuilder.css"],
26
+ ["src/filter/FieldFilterPanel.css", "es/filter/FieldFilterPanel.css"],
27
+ ["src/filter/FilterEditor.css", "es/filter/FilterEditor.css"],
28
+ ["src/filter/FilterNameDialog.css", "es/filter/FilterNameDialog.css"],
29
+ ["src/filter/FilterShareDialog.css", "es/filter/FilterShareDialog.css"],
30
+ ["src/filter/SavedFilterTree.css", "es/filter/SavedFilterTree.css"],
31
+ ["src/grid/index.css", "es/grid/index.css"],
32
+ ["src/grid/slots/slots.css", "es/grid/slots/slots.css"],
33
+ ["src/report-downloader/ReportDownloader.css", "es/report-downloader/ReportDownloader.css"],
25
34
  ];
26
35
 
27
36
  mkdirSync("es/layout", { recursive: true });
@@ -32,6 +41,10 @@ mkdirSync("es/paper", { recursive: true });
32
41
  mkdirSync("es/progress", { recursive: true });
33
42
  mkdirSync("es/sortable", { recursive: true });
34
43
  mkdirSync("es/autocomplete", { recursive: true });
44
+ mkdirSync("es/filter", { recursive: true });
45
+ mkdirSync("es/grid", { recursive: true });
46
+ mkdirSync("es/grid/slots", { recursive: true });
47
+ mkdirSync("es/report-downloader", { recursive: true });
35
48
 
36
49
  for (let [src, dest] of files) {
37
50
  cpSync(src, dest);
@@ -64,7 +64,7 @@
64
64
  left: -100%;
65
65
  height: 100%;
66
66
  width: 50%;
67
- background-color: var(--ds-color-accent-base-default, #0062ba);
67
+ background-color: var(--ds-color-base-default);
68
68
  animation: ObCombobox-progress-anim 1.5s ease-in-out infinite;
69
69
  }
70
70
 
@@ -39,7 +39,9 @@ export interface LookupComboboxGridProps<T extends Record<string, unknown>> exte
39
39
  columns: LookupColumn<T>[];
40
40
  }
41
41
 
42
- export type LookupComboboxProps<T extends Record<string, unknown>> = LookupComboboxListProps<T> | LookupComboboxGridProps<T>;
42
+ export type LookupComboboxProps<T extends Record<string, unknown>> =
43
+ | LookupComboboxListProps<T>
44
+ | LookupComboboxGridProps<T>;
43
45
 
44
46
  export function LookupCombobox<T extends Record<string, unknown>>(props: LookupComboboxProps<T>) {
45
47
  let {
@@ -1,34 +1,48 @@
1
1
  .ObAutocomplete-root {
2
2
  --obAutocomplete-padding: 0.25rem;
3
3
  --obAutocomplete-gap: 0.25rem;
4
- --obAutocomplete-fontSize: var(--ds-font-size-3, 1rem);
5
4
 
6
5
  display: inline-grid;
7
6
  }
8
7
 
8
+ /*
9
+ * The root is not a `.ds-field`, so it has to space its label, description,
10
+ * control and validation message itself. Mirrors `.ds-field > * + *`, including
11
+ * the description sitting tight under the label.
12
+ */
13
+ .ObAutocomplete-root > * + * {
14
+ margin-block-start: var(--ds-size-2);
15
+ }
16
+
17
+ .ObAutocomplete-root > [data-field="description"] {
18
+ margin-block-start: 0;
19
+ }
20
+
9
21
  .ObAutocomplete-root[data-full-width="true"] {
10
22
  display: grid;
11
23
  inline-size: 100%;
12
24
  }
13
25
 
26
+ /*
27
+ * Font size is deliberately never set: `ds-input` inherits its font size when a
28
+ * `data-size` attribute is present, so referencing `--ds-font-size-*` here — which
29
+ * `data-size` rescales — would render the text smaller than a `Textfield` or
30
+ * `Select` of the same size. Only the padding varies per size.
31
+ */
14
32
  .ObAutocomplete-root[data-size="xs"] {
15
33
  --obAutocomplete-padding: 0.125rem;
16
- --obAutocomplete-fontSize: var(--ds-font-size-1, 0.75rem);
17
34
  }
18
35
 
19
36
  .ObAutocomplete-root[data-size="sm"] {
20
37
  --obAutocomplete-padding: 0.25rem;
21
- --obAutocomplete-fontSize: var(--ds-font-size-2, 0.875rem);
22
38
  }
23
39
 
24
40
  .ObAutocomplete-root[data-size="md"] {
25
41
  --obAutocomplete-padding: 0.25rem;
26
- --obAutocomplete-fontSize: var(--ds-font-size-3, 1rem);
27
42
  }
28
43
 
29
44
  .ObAutocomplete-root[data-size="lg"] {
30
45
  --obAutocomplete-padding: 0.375rem;
31
- --obAutocomplete-fontSize: var(--ds-font-size-4, 1.125rem);
32
46
  }
33
47
 
34
48
  /*
@@ -40,34 +54,36 @@
40
54
  .ObAutocomplete-control {
41
55
  display: flex;
42
56
  flex-direction: column;
57
+ justify-content: center;
43
58
  box-sizing: border-box;
44
59
  gap: var(--obAutocomplete-gap);
45
60
  padding: var(--obAutocomplete-padding);
46
61
  inline-size: 100%;
47
- font-size: var(--obAutocomplete-fontSize);
48
- color: var(--ds-color-neutral-text-default, #1e2b3c);
49
- background-color: var(--ds-color-neutral-surface-default, #fff);
50
- border: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-default, #b3b3b3);
51
- border-radius: var(--ds-border-radius-md, 0.375rem);
62
+ /* Matches `--dsc-input-size`, so the control lines up with a Textfield. */
63
+ min-block-size: var(--ds-size-12);
64
+ color: var(--ds-color-neutral-text-default);
65
+ background-color: var(--ds-color-neutral-surface-default);
66
+ border: var(--ds-border-width-default) solid var(--ds-color-neutral-border-default);
67
+ border-radius: var(--ds-border-radius-md);
52
68
  }
53
69
 
54
70
  .ObAutocomplete-control:hover:not(:focus-within) {
55
- border-color: var(--ds-color-neutral-border-strong, #767676);
71
+ border-color: var(--ds-color-neutral-border-strong);
56
72
  }
57
73
 
58
74
  .ObAutocomplete-control:has(.ObAutocomplete-input:focus-visible) {
59
75
  box-shadow: var(--dsc-focus-boxShadow);
60
76
  outline: var(--dsc-focus-outline);
61
- outline-offset: var(--ds-border-width-focus, 3px);
77
+ outline-offset: var(--ds-border-width-focus);
62
78
  }
63
79
 
64
80
  .ObAutocomplete-control[data-invalid="true"] {
65
- border-color: var(--ds-color-danger-border-default, #b3253a);
81
+ border-color: var(--ds-color-danger-border-default);
66
82
  }
67
83
 
68
84
  .ObAutocomplete-control[data-disabled="true"] {
69
85
  cursor: not-allowed;
70
- opacity: var(--ds-disabled-opacity, 0.3);
86
+ opacity: var(--ds-opacity-disabled);
71
87
  }
72
88
 
73
89
  .ObAutocomplete-chips {
@@ -88,7 +104,7 @@
88
104
  .ObAutocomplete-input {
89
105
  flex: 1;
90
106
  min-inline-size: 0;
91
- padding: var(--ds-size-1, 0.25rem);
107
+ padding: var(--ds-size-1);
92
108
  font: inherit;
93
109
  color: inherit;
94
110
  background: transparent;
@@ -105,17 +121,16 @@
105
121
  align-items: center;
106
122
  justify-content: center;
107
123
  flex: none;
108
- padding: var(--ds-size-1, 0.25rem);
109
- font-size: var(--obAutocomplete-fontSize);
124
+ padding: var(--ds-size-1);
110
125
  color: inherit;
111
126
  background: transparent;
112
127
  border: none;
113
- border-radius: var(--ds-border-radius-sm, 0.25rem);
128
+ border-radius: var(--ds-border-radius-sm);
114
129
  cursor: pointer;
115
130
  }
116
131
 
117
132
  .ObAutocomplete-button:hover:not(:disabled) {
118
- background-color: var(--ds-color-neutral-surface-hover, #e6e6e6);
133
+ background-color: var(--ds-color-neutral-surface-hover);
119
134
  }
120
135
 
121
136
  .ObAutocomplete-button:disabled {
@@ -134,7 +149,7 @@
134
149
  flex: none;
135
150
  display: inline-flex;
136
151
  align-items: center;
137
- padding-inline: var(--ds-size-1, 0.25rem);
152
+ padding-inline: var(--ds-size-1);
138
153
  }
139
154
 
140
155
  .ObAutocomplete-positioner {
@@ -149,13 +164,12 @@
149
164
  max-block-size: min(24rem, var(--available-height));
150
165
  overflow-y: auto;
151
166
  overscroll-behavior: contain;
152
- padding: var(--ds-size-1, 0.25rem);
153
- font-size: var(--obAutocomplete-fontSize, var(--ds-font-size-3, 1rem));
154
- color: var(--ds-color-neutral-text-default, #1e2b3c);
155
- background-color: var(--ds-color-neutral-background-default, #fff);
156
- border: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-subtle, #e0e0e0);
157
- border-radius: var(--ds-border-radius-md, 0.375rem);
158
- box-shadow: var(--ds-shadow-md, 0 2px 8px 0 rgb(0 0 0 / 0.12));
167
+ padding: var(--ds-size-1);
168
+ color: var(--ds-color-neutral-text-default);
169
+ background-color: var(--ds-color-neutral-surface-default);
170
+ border: var(--ds-border-width-default) solid var(--ds-color-neutral-border-subtle);
171
+ border-radius: var(--ds-border-radius-md);
172
+ box-shadow: var(--ds-shadow-md);
159
173
  transform-origin: var(--transform-origin);
160
174
  transition:
161
175
  opacity 0.12s ease-out,
@@ -176,30 +190,37 @@
176
190
  .ObAutocomplete-item {
177
191
  display: flex;
178
192
  align-items: center;
179
- gap: var(--ds-size-2, 0.5rem);
180
- padding: var(--ds-size-2, 0.5rem);
181
- border-radius: var(--ds-border-radius-sm, 0.25rem);
193
+ gap: var(--ds-size-2);
194
+ padding: var(--ds-size-2);
195
+ border-radius: var(--ds-border-radius-sm);
182
196
  cursor: default;
183
197
  user-select: none;
184
- scroll-margin-block: var(--ds-size-1, 0.25rem);
198
+ scroll-margin-block: var(--ds-size-1);
185
199
  }
186
200
 
201
+ /*
202
+ * The `accent` colour set does not exist in every Designsystemet theme, so the
203
+ * option states use the contextual `--ds-color-*` aliases — the same tokens
204
+ * Designsystemet's own Suggestion list uses.
205
+ */
187
206
  .ObAutocomplete-item[data-highlighted] {
188
- background-color: var(--ds-color-accent-surface-hover, #d6e2f2);
207
+ color: var(--ds-color-text-default);
208
+ background-color: var(--ds-color-surface-hover);
189
209
  }
190
210
 
191
211
  .ObAutocomplete-item[data-selected] {
192
- font-weight: var(--ds-font-weight-medium, 500);
193
- background-color: var(--ds-color-accent-surface-default, #e6effa);
212
+ font-weight: var(--ds-font-weight-medium);
213
+ color: var(--ds-color-text-default);
214
+ background-color: var(--ds-color-surface-tinted);
194
215
  }
195
216
 
196
217
  .ObAutocomplete-item[data-selected][data-highlighted] {
197
- background-color: var(--ds-color-accent-surface-active, #c2d5ec);
218
+ background-color: var(--ds-color-surface-active);
198
219
  }
199
220
 
200
221
  .ObAutocomplete-item[data-disabled] {
201
222
  cursor: not-allowed;
202
- opacity: var(--ds-disabled-opacity, 0.3);
223
+ opacity: var(--ds-opacity-disabled);
203
224
  }
204
225
 
205
226
  .ObAutocomplete-itemIndicator {
@@ -209,14 +230,14 @@
209
230
  }
210
231
 
211
232
  .ObAutocomplete-groupLabel {
212
- padding: var(--ds-size-2, 0.5rem);
213
- font-size: var(--ds-font-size-1, 0.75rem);
214
- font-weight: var(--ds-font-weight-semibold, 600);
215
- color: var(--ds-color-neutral-text-subtle, #4c5866);
233
+ padding: var(--ds-size-2);
234
+ font-size: var(--ds-font-size-1);
235
+ font-weight: var(--ds-font-weight-semibold);
236
+ color: var(--ds-color-neutral-text-subtle);
216
237
  text-transform: uppercase;
217
238
  }
218
239
 
219
240
  .ObAutocomplete-empty:not(:empty) {
220
- padding: var(--ds-size-2, 0.5rem);
221
- color: var(--ds-color-neutral-text-subtle, #4c5866);
241
+ padding: var(--ds-size-2);
242
+ color: var(--ds-color-neutral-text-subtle);
222
243
  }
@@ -117,6 +117,7 @@ export function Autocomplete<
117
117
  disableClearable,
118
118
  placeholder,
119
119
  inputRef,
120
+ "aria-label": ariaLabel,
120
121
  "data-size": size,
121
122
  "data-color": color,
122
123
  } = props;
@@ -294,6 +295,7 @@ export function Autocomplete<
294
295
  loading={loading}
295
296
  disableClearable={disableClearable}
296
297
  inputRef={inputRef}
298
+ aria-label={ariaLabel}
297
299
  chips={chips}
298
300
  trigger={
299
301
  <BaseAutocomplete.Trigger
@@ -3,7 +3,7 @@ import { FieldDescription, Label, Spinner, ValidationMessage } from "@digdir/des
3
3
  import { getLocalizedString } from "@olenbetong/appframe-core";
4
4
  import { XMarkIcon } from "@navikt/aksel-icons";
5
5
  import clsx from "clsx";
6
- import type { ReactNode, Ref } from "react";
6
+ import { type ReactNode, type Ref, useEffect, useRef, useState } from "react";
7
7
 
8
8
  import type { AutocompleteSize } from "./types.js";
9
9
 
@@ -26,6 +26,10 @@ export interface AutocompleteFrameProps {
26
26
  loading?: boolean;
27
27
  disableClearable?: boolean;
28
28
  inputRef?: Ref<HTMLInputElement>;
29
+ /**
30
+ * Accessible name for the input. Use when the field has no visible label.
31
+ */
32
+ "aria-label"?: string;
29
33
  /**
30
34
  * Chips for the selected values, rendered above the input.
31
35
  */
@@ -75,6 +79,7 @@ export function AutocompleteFrame({
75
79
  loading,
76
80
  disableClearable,
77
81
  inputRef,
82
+ "aria-label": ariaLabel,
78
83
  chips,
79
84
  trigger,
80
85
  emptyContent,
@@ -82,10 +87,23 @@ export function AutocompleteFrame({
82
87
  onInputKeyDown,
83
88
  onInputBlur,
84
89
  }: AutocompleteFrameProps) {
90
+ let rootRef = useRef<HTMLDivElement>(null);
91
+ let [container, setContainer] = useState<HTMLElement | null>(null);
92
+
93
+ /*
94
+ * A modal dialog is painted in the top layer, above everything else in the
95
+ * document, so a popup portalled to the body ends up behind its backdrop and
96
+ * is invisible. Portal into the dialog itself when there is one.
97
+ */
98
+ useEffect(() => {
99
+ setContainer(rootRef.current?.closest("dialog") ?? null);
100
+ }, []);
101
+
85
102
  return (
86
103
  // Designsystemet's `Field` is a custom element that expects to own the
87
104
  // layout, so the root here is a plain grid instead.
88
105
  <div
106
+ ref={rootRef}
89
107
  className={clsx("ObAutocomplete-root", className)}
90
108
  style={style}
91
109
  data-size={size}
@@ -111,6 +129,7 @@ export function AutocompleteFrame({
111
129
  ref={inputRef}
112
130
  className="ObAutocomplete-input"
113
131
  placeholder={placeholder}
132
+ aria-label={ariaLabel}
114
133
  disabled={disabled}
115
134
  readOnly={readOnly}
116
135
  required={required}
@@ -136,7 +155,7 @@ export function AutocompleteFrame({
136
155
  </BaseCombobox.InputGroup>
137
156
  </div>
138
157
  {error && <ValidationMessage data-size={size}>{error}</ValidationMessage>}
139
- <BaseCombobox.Portal>
158
+ <BaseCombobox.Portal container={container ?? undefined}>
140
159
  <BaseCombobox.Positioner className="ObAutocomplete-positioner" sideOffset={4}>
141
160
  <BaseCombobox.Popup className={clsx("ObAutocomplete-popup", popupClassName)} data-size={size}>
142
161
  <BaseCombobox.Empty className="ObAutocomplete-empty">{emptyContent}</BaseCombobox.Empty>