@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 +105 -0
- package/package.json +6 -6
- package/src/AfLookup/LookupCombobox.css +1 -1
- package/src/autocomplete/Autocomplete.css +63 -42
- package/src/autocomplete/Autocomplete.tsx +2 -0
- package/src/autocomplete/AutocompleteFrame.tsx +21 -2
- package/src/autocomplete/Combobox.tsx +15 -2
- package/src/autocomplete/types.ts +4 -0
- package/src/container/Container.tsx +49 -0
- package/src/container/index.ts +1 -0
- package/src/index.ts +1 -0
- package/src/paper/Paper.tsx +2 -0
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.
|
|
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.
|
|
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",
|
|
@@ -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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
border
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
153
|
-
|
|
154
|
-
color: var(--ds-color-neutral-
|
|
155
|
-
|
|
156
|
-
border: var(--ds-border-
|
|
157
|
-
|
|
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
|
|
180
|
-
padding: var(--ds-size-2
|
|
181
|
-
border-radius: var(--ds-border-radius-sm
|
|
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
|
|
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
|
-
|
|
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
|
|
193
|
-
|
|
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-
|
|
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
|
|
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
|
|
213
|
-
font-size: var(--ds-font-size-1
|
|
214
|
-
font-weight: var(--ds-font-weight-semibold
|
|
215
|
-
color: var(--ds-color-neutral-text-subtle
|
|
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
|
|
221
|
-
color: var(--ds-color-neutral-text-subtle
|
|
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
|
|
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
|
-
|
|
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
package/src/paper/Paper.tsx
CHANGED
|
@@ -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"
|