@olenbetong/appframe-ds 0.7.0 → 0.8.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,110 @@
1
1
  # @olenbetong/appframe-ds
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 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`).
8
+ - 27ef468: Add `FilterEditor`, the criteria tree of the web filter builder.
9
+
10
+ `FilterEditor` renders a nested criteria tree, matching the advanced filter
11
+ editor in the Windows client: groups can be nested to any depth and each group
12
+ chooses whether its items are combined with `and` or `or`. An empty filter is a
13
+ single `and` group. Every group ends with
14
+ a blank row, so a condition is added just by picking a field. Value editors are
15
+ chosen from the field type and the operator's arity, and cover text, number and
16
+ date inputs, ranges, value lists and relative dates such as "Today" or "Current
17
+ user". Set `showFilterString` to show the equivalent filter string, which can
18
+ also be edited and parsed back into the tree. A group's conditions are indented
19
+ and boxed below its header so it is clear which group they belong to. On narrow
20
+ viewports, where the parts of a condition wrap onto several lines, conditions are
21
+ separated by extra spacing and a rule, and the group buttons are reduced to
22
+ icons so they no longer overflow.
23
+
24
+ `@olenbetong/appframe-core` gained path-based filter tree helpers —
25
+ `getNodeAtPath`, `updateNodeAtPath`, `insertNodeAtPath`, `removeNodeAtPath`,
26
+ `setGroupMode` and `addGroupAtPath` — replacing the group/item index helpers
27
+ `addExpression`, `updateExpression`, `removeExpression`, `addGroup` and
28
+ `removeGroup`, which only worked on the flat standard layout. `pruneFilter` now
29
+ prunes nested groups, `toFilterTree` normalizes a filter without flattening it,
30
+ and `isStandardLayout` reports whether a filter fits the simplified layout the
31
+ Windows flow editor can render.
32
+
33
+ `Combobox` in `@olenbetong/appframe-ds` gained two related fixes: it now accepts
34
+ `aria-label` for use without a visible label, and the input shows the selected
35
+ option's label on mount and whenever the selection is changed from the outside.
36
+ `Combobox` also no longer renders text one step smaller than a `Textfield` or
37
+ `Select` of the same `data-size`. Designsystemet inputs inherit their font size
38
+ when `data-size` is set, and `data-size` rescales `--ds-font-size-*`, so
39
+ referencing the type scale at all made the text smaller — `Combobox` now
40
+ inherits its font size the same way `Textfield` does, and has a minimum height
41
+ matching `--dsc-input-size` so it lines up exactly with an input beside it.
42
+
43
+ `Combobox` options were also unreadable in dark mode: the hovered and selected
44
+ states referenced `--ds-color-accent-*`, a colour set that does not exist in
45
+ these themes, so every option fell back to a hard-coded light background while
46
+ the text stayed light. The option states now use the contextual `--ds-color-*`
47
+ aliases, and all hard-coded colour fallbacks — which only ever encoded the light
48
+ theme — have been removed from `Combobox` and `AfLookup`.
49
+
50
+ ### Patch Changes
51
+
52
+ - b50190d: `Paper` now accepts a `ref`, which is forwarded to the rendered element.
53
+ - 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.
54
+ - a0df74a: Add the report metadata model, `useReport` and `ReportParameters`.
55
+
56
+ `@olenbetong/appframe-core` gained a pure report model: `Report`,
57
+ `ReportParameter`, `ReportRecordSource` and their raw record counterparts, plus
58
+ `toReport`, `getReportFormats`, `parseRecordSource` and the parameter helpers
59
+ `toReportParameters`, `coerceParameterValue`, `getDefaultParameterValues`,
60
+ `serializeParameters`, `parseParameters` and `restoreParameterValues`. The
61
+ parameters XML is the same `<Parameters Version="1">` document the Windows
62
+ launcher writes, so a filter saved in either client restores in the other. A
63
+ parameter's editor is derived the way the Windows launcher derives it: a lookup
64
+ wins over everything, a fixed item list comes next, and otherwise the data type
65
+ decides. `FormatCSV` enables XLSX and XLS as well, matching the single
66
+ DevExpress export the flag stands for.
67
+
68
+ `@olenbetong/appframe-react` gained `useReport`, which loads a report from
69
+ `sviw_WinClient_MyReports` — so a report that does not come back is one the user
70
+ may not run — merges the `HasTitle`/`HasHeader` bands from
71
+ `sviw_WinClient_ReportsWeb` and the parameters from
72
+ `sviw_WinClient_ReportsParameters`. It returns the filter the report is designed
73
+ with, overridable through an `initialFilter` option the way opening a report
74
+ programmatically overrides it in the Windows client, and the literal parameter
75
+ defaults. Defaults naming a stored procedure are left unset, to be resolved
76
+ server-side. `useParameterOptions` loads the options for a lookup parameter from
77
+ its `afRecordSource`.
78
+
79
+ `@olenbetong/synergi-react` gained `ReportParameters`, the parameter form of the
80
+ report launcher. It renders nothing when a report has no parameters, so the
81
+ launcher can include it unconditionally, and gives each parameter the editor its
82
+ metadata asks for: a lookup combobox, a select, a checkbox, or a text, number or
83
+ date input. Values are coerced to the parameter's type as they are entered.
84
+
85
+ `Combobox` in `@olenbetong/appframe-ds` had no spacing between its label and its
86
+ control: the root is not a `.ds-field`, which is what normally spaces a label,
87
+ description and input apart, so it now does that spacing itself and lines up
88
+ with a `Textfield` or `Select` beside it.
89
+
90
+ - 1bb10de: `Autocomplete` and `Combobox` now open their option list inside the dialog they
91
+ are used in. The list is portalled, and a modal dialog is painted in the top
92
+ layer, so a list portalled to the body ended up behind the dialog backdrop and
93
+ appeared not to open at all.
94
+ - Updated dependencies [448c378]
95
+ - Updated dependencies [54e5344]
96
+ - Updated dependencies [448c378]
97
+ - Updated dependencies [a0df74a]
98
+ - Updated dependencies [7a54755]
99
+ - Updated dependencies [3954bc9]
100
+ - Updated dependencies [0a23d5b]
101
+ - Updated dependencies [3ff7c84]
102
+ - Updated dependencies [ace635e]
103
+ - Updated dependencies [27ef468]
104
+ - @olenbetong/appframe-react@2.1.0
105
+ - @olenbetong/appframe-core@2.12.0
106
+ - @olenbetong/appframe-data@1.6.0
107
+
3
108
  ## 0.7.0
4
109
 
5
110
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olenbetong/appframe-ds",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Components that bind Designsystemet components to Appframe data objects",
5
5
  "type": "module",
6
6
  "types": "./es/index.d.ts",
@@ -23,15 +23,15 @@
23
23
  "react-router": ">=8.3.0"
24
24
  },
25
25
  "dependencies": {
26
- "@atlaskit/pragmatic-drag-and-drop": "2.0.1",
26
+ "@atlaskit/pragmatic-drag-and-drop": "2.0.2",
27
27
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "2.0.0",
28
28
  "@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
29
  "clsx": "^2.1.1",
33
30
  "react-virtualized-auto-sizer": "^1.0.26",
34
- "react-window": "^1.8.11"
31
+ "react-window": "^1.8.11",
32
+ "@olenbetong/appframe-data": "1.6.0",
33
+ "@olenbetong/appframe-core": "2.12.0",
34
+ "@olenbetong/appframe-react": "2.1.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@digdir/designsystemet-react": "1.18.0",
@@ -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
 
@@ -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>
@@ -4,7 +4,7 @@ import { Combobox as BaseCombobox } from "@base-ui/react/combobox";
4
4
  import { ChipRemovable } from "@digdir/designsystemet-react";
5
5
  import { ChevronDownIcon } from "@navikt/aksel-icons";
6
6
  import { getLocalizedString } from "@olenbetong/appframe-core";
7
- import { useId, useMemo } from "react";
7
+ import { useEffect, useId, useMemo, useRef } from "react";
8
8
 
9
9
  import { AutocompleteFrame } from "./AutocompleteFrame.js";
10
10
  import type { AutocompleteBaseProps, AutocompleteChangeReason, AutocompleteValue } from "./types.js";
@@ -107,6 +107,7 @@ export function Combobox<Option, Multiple extends boolean | undefined = false>(p
107
107
  disableClearable,
108
108
  placeholder,
109
109
  inputRef,
110
+ "aria-label": ariaLabel,
110
111
  "data-size": size,
111
112
  "data-color": color,
112
113
  } = props;
@@ -116,7 +117,18 @@ export function Combobox<Option, Multiple extends boolean | undefined = false>(p
116
117
 
117
118
  let emptyValue = (multiple ? [] : null) as AutocompleteValue<Option, Multiple>;
118
119
  let [value, setValue] = useControlled(valueProp, defaultValue ?? emptyValue);
119
- let [inputValue, setInputValue] = useControlled(inputValueProp, "");
120
+ // The input shows the selected option's label. Base UI keeps it in sync while
121
+ // the user interacts, but not when the selection is set from the outside.
122
+ let selectedLabel = multiple || value == null ? "" : getOptionLabel(value as Option);
123
+ let [inputValue, setInputValue] = useControlled(inputValueProp, selectedLabel);
124
+ let previousLabel = useRef(selectedLabel);
125
+
126
+ useEffect(() => {
127
+ if (previousLabel.current === selectedLabel) return;
128
+
129
+ previousLabel.current = selectedLabel;
130
+ setInputValue(selectedLabel);
131
+ }, [selectedLabel, setInputValue]);
120
132
 
121
133
  let filteredOptions = useMemo(() => {
122
134
  let filter = (filterOptions ?? defaultFilterOptions) as (
@@ -240,6 +252,7 @@ export function Combobox<Option, Multiple extends boolean | undefined = false>(p
240
252
  loading={loading}
241
253
  disableClearable={disableClearable}
242
254
  inputRef={inputRef}
255
+ aria-label={ariaLabel}
243
256
  chips={chips}
244
257
  trigger={
245
258
  <BaseCombobox.Trigger
@@ -195,6 +195,10 @@ export interface AutocompleteBaseProps<Option, Multiple extends boolean | undefi
195
195
  */
196
196
  popupClassName?: string;
197
197
  inputRef?: Ref<HTMLInputElement>;
198
+ /**
199
+ * Accessible name for the input. Use when the field has no visible label.
200
+ */
201
+ "aria-label"?: string;
198
202
  "data-size"?: AutocompleteSize;
199
203
  "data-color"?: string;
200
204
  }
@@ -0,0 +1,49 @@
1
+ import { PageBlock, type PageBlockProps, type PageBlockWidth } from "../page/PageBlock.js";
2
+
3
+ export const containerMaxWidths = ["xs", "sm", "md", "lg", "xl"] as const;
4
+
5
+ export type ContainerMaxWidth = (typeof containerMaxWidths)[number];
6
+
7
+ const maxWidthToBlockWidth: Record<ContainerMaxWidth, PageBlockWidth> = {
8
+ xs: "text",
9
+ sm: "text",
10
+ md: "md",
11
+ lg: "lg",
12
+ xl: "xl",
13
+ };
14
+
15
+ export type ContainerProps = Omit<PageBlockProps, "width"> & {
16
+ /**
17
+ * Predefined max-width, using the same scale as MUI `Container`. Content is
18
+ * horizontally centered.
19
+ *
20
+ * - `xs`/`sm`: 36rem (576px) — recommended line-length for running text
21
+ * - `md`: 48rem (768px)
22
+ * - `lg`: 64rem (1024px)
23
+ * - `xl`: 80rem (1280px)
24
+ * - `false`: no max-width
25
+ *
26
+ * @default "lg"
27
+ */
28
+ maxWidth?: ContainerMaxWidth | false;
29
+ };
30
+
31
+ /**
32
+ * Centered, max-width content wrapper — the MUI `Container` equivalent.
33
+ *
34
+ * A thin alias over `Page.Block` with gutters enabled by default, for the very
35
+ * common case of "center this content and give it sensible side padding"
36
+ * without having to reach for the full `Page` composition.
37
+ *
38
+ * @example
39
+ * ```jsx
40
+ * <Container maxWidth="sm">
41
+ * <Heading>Expenses</Heading>
42
+ * </Container>
43
+ * ```
44
+ */
45
+ export function Container({ gutters = true, maxWidth = "lg", ...props }: ContainerProps) {
46
+ let width = maxWidth === false ? undefined : maxWidthToBlockWidth[maxWidth];
47
+
48
+ return <PageBlock gutters={gutters} width={width} {...props} />;
49
+ }
@@ -0,0 +1 @@
1
+ export * from "./Container.js";
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./AfLookup/index.js";
2
2
  export * from "./autocomplete/index.js";
3
3
  export * from "./binding/index.js";
4
+ export * from "./container/index.js";
4
5
  export * from "./input/index.js";
5
6
  export * from "./layout/index.js";
6
7
  export * from "./list/index.js";
@@ -20,6 +20,8 @@ export type PaperProps = React.HTMLAttributes<HTMLElement> & {
20
20
  */
21
21
  border?: boolean;
22
22
  children?: React.ReactNode;
23
+ /** Forwarded to the rendered element. */
24
+ ref?: React.Ref<any>;
23
25
  /**
24
26
  * Elevation shadow, using the designsystemet `--ds-shadow-*` tokens.
25
27
  * @default "sm"