@olenbetong/appframe-ds 1.8.0 → 1.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.
- package/CHANGELOG.md +8 -0
- package/package.json +2 -2
- package/src/AfLookup/Lookup.css +9 -1
- package/src/AfLookup/Lookup.tsx +8 -10
- package/src/AfLookup/LookupCombobox.tsx +1 -9
- package/src/AfLookup/LookupEdit.tsx +3 -3
- package/src/AfLookup/LookupGrid.css +3 -2
- package/src/AfLookup/LookupGrid.tsx +8 -4
- package/src/autocomplete/Autocomplete.css +6 -17
- package/src/binding/BoundNumberInput.tsx +42 -42
- package/src/binding/BoundNumericTextField.tsx +102 -102
- package/src/binding/BoundTextField.tsx +49 -49
- package/src/binding/CancelButton.tsx +30 -30
- package/src/binding/CancelIconButton.tsx +30 -30
- package/src/binding/DataEditToolbar.css +23 -23
- package/src/binding/DataEditToolbar.tsx +84 -84
- package/src/binding/DeleteButton.tsx +35 -35
- package/src/binding/DeleteIconButton.tsx +35 -35
- package/src/binding/RefreshButton.tsx +31 -31
- package/src/binding/RefreshIconButton.tsx +33 -33
- package/src/binding/RefreshRowIconButton.tsx +33 -33
- package/src/binding/SaveButton.tsx +37 -37
- package/src/binding/SaveIconButton.tsx +38 -38
- package/src/binding/index.ts +20 -20
- package/src/container/Container.tsx +50 -50
- package/src/container/index.ts +1 -1
- package/src/cutoff/CutOffPicker.css +12 -12
- package/src/cutoff/CutOffPicker.tsx +165 -165
- package/src/cutoff/context.tsx +194 -194
- package/src/cutoff/dsCutOffDates.ts +21 -21
- package/src/cutoff/index.ts +4 -4
- package/src/cutoff/utils.test.ts +59 -59
- package/src/cutoff/utils.ts +276 -276
- package/src/filter/ChipListInput.tsx +79 -79
- package/src/filter/FieldFilterPanel.css +150 -150
- package/src/filter/FieldFilterPanel.tsx +303 -309
- package/src/filter/FilterBuilder.css +152 -152
- package/src/filter/FilterBuilder.test.tsx +153 -153
- package/src/filter/FilterBuilder.tsx +435 -435
- package/src/filter/FilterEditor.css +231 -231
- package/src/filter/FilterEditor.test.tsx +285 -285
- package/src/filter/FilterEditor.tsx +75 -75
- package/src/filter/FilterGroupEditor.tsx +160 -160
- package/src/filter/FilterNameDialog.css +22 -22
- package/src/filter/FilterNameDialog.tsx +81 -81
- package/src/filter/FilterRow.tsx +117 -117
- package/src/filter/FilterShareDialog.css +28 -28
- package/src/filter/FilterShareDialog.tsx +195 -201
- package/src/filter/FilterStringField.tsx +72 -78
- package/src/filter/FilterValueEditor.tsx +239 -239
- package/src/filter/SavedFilterTree.css +107 -107
- package/src/filter/SavedFilterTree.test.tsx +94 -94
- package/src/filter/SavedFilterTree.tsx +218 -218
- package/src/filter/fieldFilter.test.ts +158 -158
- package/src/filter/fieldFilter.ts +139 -139
- package/src/filter/index.ts +27 -27
- package/src/filter/types.ts +38 -38
- package/src/filter/useDistinctValues.test.ts +102 -102
- package/src/filter/useDistinctValues.ts +230 -230
- package/src/global.d.ts +11 -11
- package/src/grid/AfGridColumnsPanel.tsx +4 -7
- package/src/grid/AfGridError.tsx +2 -2
- package/src/grid/AfHeaderFilterCell.tsx +0 -3
- package/src/grid/AfHeaderFilterPanel.tsx +1 -3
- package/src/grid/GridEditLookup.tsx +0 -2
- package/src/grid/Toolbar.tsx +9 -38
- package/src/grid/index.css +44 -0
- package/src/grid/index.tsx +12 -3
- package/src/grid/slots/index.tsx +15 -40
- package/src/grid/useAfGridApi.ts +0 -1
- package/src/grid/useAfPersistedState.ts +4 -8
- package/src/grid/useAfSizeMode.ts +19 -0
- package/src/i18n.ts +13 -13
- package/src/index.ts +14 -14
- package/src/input/NumberInput.tsx +0 -2
- package/src/layout/AppMenu.tsx +74 -74
- package/src/layout/AppMenuDrawer.css +71 -71
- package/src/layout/AppMenuDrawer.tsx +122 -122
- package/src/layout/DialogMotion.css +79 -79
- package/src/layout/ErrorBoundary.tsx +162 -162
- package/src/layout/PageContainer.css +48 -48
- package/src/layout/PageContainer.tsx +41 -41
- package/src/layout/ResponsiveDialog.css +86 -86
- package/src/layout/ResponsiveDialog.tsx +55 -55
- package/src/layout/index.tsx +70 -70
- package/src/link/Link.tsx +23 -23
- package/src/link/index.ts +1 -1
- package/src/list/ItemList.css +2 -1
- package/src/mdi/Mdi.test.tsx +91 -91
- package/src/mdi/Mdi.tsx +141 -141
- package/src/mdi/MdiConfirmCloseDialog.tsx +41 -47
- package/src/mdi/MdiContext.tsx +59 -59
- package/src/mdi/MdiDocumentContext.tsx +60 -60
- package/src/mdi/MdiPanel.tsx +61 -61
- package/src/mdi/MdiRestore.test.tsx +86 -86
- package/src/mdi/MdiTab.tsx +30 -30
- package/src/mdi/controller.test.ts +382 -382
- package/src/mdi/controller.ts +656 -656
- package/src/mdi/dockviewHost.ts +132 -132
- package/src/mdi/fakeHost.ts +200 -200
- package/src/mdi/host.ts +44 -44
- package/src/mdi/ids.test.ts +20 -20
- package/src/mdi/ids.ts +18 -18
- package/src/mdi/index.ts +31 -31
- package/src/mdi/layoutRestore.test.ts +156 -156
- package/src/mdi/layoutRestore.ts +311 -311
- package/src/mdi/layoutSpec.test.ts +143 -143
- package/src/mdi/layoutSpec.ts +214 -214
- package/src/mdi/layoutStore.test.ts +93 -93
- package/src/mdi/layoutStore.ts +99 -99
- package/src/mdi/memorySettingsStore.ts +21 -21
- package/src/mdi/registry.ts +34 -34
- package/src/mdi/testTypes.ts +37 -37
- package/src/mdi/theme.ts +22 -22
- package/src/mdi/types.ts +214 -214
- package/src/page/Page.css +57 -57
- package/src/page/PageBlock.css +64 -64
- package/src/page/PageBlock.tsx +93 -93
- package/src/page/PageHeader.css +88 -88
- package/src/page/PageHeader.tsx +115 -115
- package/src/page/PageTitle.css +5 -5
- package/src/page/PageTitle.tsx +21 -21
- package/src/page/index.ts +4 -4
- package/src/paper/Paper.tsx +52 -52
- package/src/progress/LinearProgress.css +121 -121
- package/src/progress/LinearProgress.tsx +106 -106
- package/src/progress/index.ts +1 -1
- package/src/report-downloader/ReportDownloader.css +67 -67
- package/src/report-downloader/components/StatusItem.tsx +87 -87
- package/src/report-downloader/components/StatusList.tsx +28 -28
- package/src/report-downloader/index.ts +41 -41
- package/src/report-downloader/status.tsx +92 -92
- package/src/sortable/DropIndicator.css +51 -51
- package/src/sortable/DropIndicator.tsx +27 -27
- package/src/sortable/SortableItemList.css +21 -21
- package/src/sortable/SortableItemList.tsx +264 -264
- package/src/sortable/index.ts +8 -8
- package/src/sortable/reorderSortOrder.ts +126 -126
- package/src/sortable/sortOrder.ts +101 -101
- package/src/sortable/types.ts +9 -9
- package/src/sortable/useSortableContainer.ts +53 -53
- package/src/sortable/useSortableItem.ts +95 -95
- package/src/sortable/useSortableList.ts +117 -117
- package/src/test/happy-dom-document-class.ts +24 -24
- package/src/test/setup.ts +27 -27
- package/src/theme/sizeMode.ts +62 -0
- package/src/theme/useDsColorScheme.ts +53 -53
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @olenbetong/appframe-ds
|
|
2
2
|
|
|
3
|
+
## 1.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3bb1a08: Everything follows the Designsystemet size mode (`data-size` on an ancestor) instead of setting its own size. Components no longer hard-code `data-size="sm"`, and the filter components' `size` prop no longer defaults to `"sm"`: they inherit, and a caller can still pass a size. The 2026 site template sets `sm` on the app's `<main>`, so on those pages they look as before; on a page without a size mode they are now md.
|
|
8
|
+
|
|
9
|
+
`AfGrid` uses the size mode in place of MUI's density. Rows and headers are `--ds-size-10` tall (25px at xs, 31px at sm, 35px at md), the text is the size mode's body text, the toolbar, header filters, footer and slot components inherit the size, and editors and buttons in a cell fit the row. The density entries are gone from the toolbar menu, the grid renders at the standard density, and saved layouts no longer restore a density (`persistenceModel.density` is deprecated and ignored); a `rowHeight` passed to the grid still wins. The multi-column lookup grid sizes its rows the same way, the lookup popup (portaled to `<body>`) takes the size mode of its field, and the list's secondary line, the lookup grid's padding and `Autocomplete`'s padding and gap are sized from the size mode instead of fixed rem/px.
|
|
10
|
+
|
|
3
11
|
## 1.8.0
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olenbetong/appframe-ds",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.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",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "3.0.0",
|
|
65
65
|
"@base-ui/react": "1.9.0",
|
|
66
66
|
"@olenbetong/appframe-core": "^2.15.2",
|
|
67
|
-
"@olenbetong/appframe-data": "^1.8.
|
|
67
|
+
"@olenbetong/appframe-data": "^1.8.6",
|
|
68
68
|
"@olenbetong/appframe-react": "^2.2.8",
|
|
69
69
|
"clsx": "^2.1.1",
|
|
70
70
|
"dockview-react": "8.6.1",
|
package/src/AfLookup/Lookup.css
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
position: relative;
|
|
3
3
|
|
|
4
4
|
.ds-input {
|
|
5
|
-
padding-inline-end: var(--ObLookup-buttons-width, 3rem);
|
|
5
|
+
padding-inline-end: var(--ObLookup-buttons-width, var(--ds-size-12, 3rem));
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
8
|
|
|
@@ -22,6 +22,14 @@
|
|
|
22
22
|
padding-inline-end: var(--ds-size-1, 0.25rem);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/*
|
|
26
|
+
* The buttons follow the inherited size mode, so in the same mode as the input they would be as
|
|
27
|
+
* tall as it. Fit them inside the input instead, with --ds-size-1 to spare above and below.
|
|
28
|
+
*/
|
|
29
|
+
.ObLookup-buttons > .ds-button {
|
|
30
|
+
--dsc-button-size: calc(var(--ds-size-12) - 2 * var(--ds-size-1));
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
.ObLookup-dialog {
|
|
26
34
|
border: 0;
|
|
27
35
|
margin-block-start: 0;
|
package/src/AfLookup/Lookup.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import { createPortal } from "react-dom";
|
|
|
10
10
|
|
|
11
11
|
import { LookupCombobox, type LookupComboboxProps } from "./LookupCombobox.js";
|
|
12
12
|
import { getLocalizedString } from "../i18n.js";
|
|
13
|
+
import { copySizeMode } from "../theme/sizeMode.js";
|
|
13
14
|
|
|
14
15
|
export type LabelTag = "optional" | "required";
|
|
15
16
|
|
|
@@ -24,7 +25,7 @@ function labelWithTag(label: string | undefined, labelTag: LabelTag | undefined)
|
|
|
24
25
|
return (
|
|
25
26
|
<>
|
|
26
27
|
{label}
|
|
27
|
-
<Tag className="ObLookup-labelTag" data-color={LABEL_TAG_COLOR[labelTag]}
|
|
28
|
+
<Tag className="ObLookup-labelTag" data-color={LABEL_TAG_COLOR[labelTag]}>
|
|
28
29
|
{getLocalizedString(LABEL_TAG_TEXT[labelTag])}
|
|
29
30
|
</Tag>
|
|
30
31
|
</>
|
|
@@ -71,6 +72,8 @@ export function useLookup<T extends Record<string, unknown>>({
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
function handleOpenDialog(searchValue?: string) {
|
|
75
|
+
// The dialog is portaled to <body>, so it would not inherit the field's size mode.
|
|
76
|
+
copySizeMode(dialogRef.current, document.getElementById(inputId) ?? getAnchorElement?.());
|
|
74
77
|
// openDialog renders synchronously (flushSync), which includes this update, so the combobox
|
|
75
78
|
// mounts with the search value already set.
|
|
76
79
|
setInitialSearchValue(searchValue);
|
|
@@ -138,7 +141,9 @@ export function AfLookup<T extends Record<string, unknown>>({
|
|
|
138
141
|
style={
|
|
139
142
|
{
|
|
140
143
|
display: fullWidth ? "block" : "inline-block",
|
|
141
|
-
"--ObLookup-buttons-width": hasClear
|
|
144
|
+
"--ObLookup-buttons-width": hasClear
|
|
145
|
+
? "calc(2 * var(--ds-size-12) - var(--ds-size-2))"
|
|
146
|
+
: "var(--ds-size-12)",
|
|
142
147
|
} as React.CSSProperties
|
|
143
148
|
}
|
|
144
149
|
>
|
|
@@ -157,7 +162,6 @@ export function AfLookup<T extends Record<string, unknown>>({
|
|
|
157
162
|
<Button
|
|
158
163
|
icon
|
|
159
164
|
variant="tertiary"
|
|
160
|
-
data-size="sm"
|
|
161
165
|
aria-label={getLocalizedString("Clear")}
|
|
162
166
|
onKeyDown={(evt) => evt.stopPropagation()}
|
|
163
167
|
onClick={(evt) => {
|
|
@@ -169,13 +173,7 @@ export function AfLookup<T extends Record<string, unknown>>({
|
|
|
169
173
|
<XMarkIcon />
|
|
170
174
|
</Button>
|
|
171
175
|
)}
|
|
172
|
-
<Button
|
|
173
|
-
icon
|
|
174
|
-
variant="tertiary"
|
|
175
|
-
data-size="sm"
|
|
176
|
-
aria-label={getLocalizedString("Search")}
|
|
177
|
-
onClick={() => openDialog()}
|
|
178
|
-
>
|
|
176
|
+
<Button icon variant="tertiary" aria-label={getLocalizedString("Search")} onClick={() => openDialog()}>
|
|
179
177
|
<MagnifyingGlassIcon />
|
|
180
178
|
</Button>
|
|
181
179
|
</div>
|
|
@@ -183,15 +183,7 @@ export function LookupCombobox<T extends Record<string, unknown>>(props: LookupC
|
|
|
183
183
|
return (
|
|
184
184
|
// oxlint-disable-next-line jsx-a11y/click-events-have-key-events -- element is not interactive, click is used to prevent dialog close
|
|
185
185
|
// oxlint-disable-next-line jsx-a11y/no-static-element-interactions -- element is not interactive, click is used to prevent dialog close
|
|
186
|
-
|
|
187
|
-
// bottom sheet keeps the default size for touch. The size cascades to the search field,
|
|
188
|
-
// the rows and the buttons.
|
|
189
|
-
<div
|
|
190
|
-
className="ObCombobox-root"
|
|
191
|
-
id={id}
|
|
192
|
-
data-size={isDesktop ? "sm" : "md"}
|
|
193
|
-
onClick={(evt) => evt.stopPropagation()}
|
|
194
|
-
>
|
|
186
|
+
<div className="ObCombobox-root" id={id} onClick={(evt) => evt.stopPropagation()}>
|
|
195
187
|
<Textfield
|
|
196
188
|
type="search"
|
|
197
189
|
className="ObCombobox-input"
|
|
@@ -79,7 +79,9 @@ export function AfLookupEdit<T extends Record<string, unknown>>({
|
|
|
79
79
|
style={
|
|
80
80
|
{
|
|
81
81
|
display: fullWidth ? "block" : "inline-block",
|
|
82
|
-
"--ObLookup-buttons-width": hasClear
|
|
82
|
+
"--ObLookup-buttons-width": hasClear
|
|
83
|
+
? "calc(2 * var(--ds-size-12) - var(--ds-size-2))"
|
|
84
|
+
: "var(--ds-size-12)",
|
|
83
85
|
} as React.CSSProperties
|
|
84
86
|
}
|
|
85
87
|
>
|
|
@@ -98,7 +100,6 @@ export function AfLookupEdit<T extends Record<string, unknown>>({
|
|
|
98
100
|
<Button
|
|
99
101
|
icon
|
|
100
102
|
variant="tertiary"
|
|
101
|
-
data-size="sm"
|
|
102
103
|
aria-label={getLocalizedString("Clear")}
|
|
103
104
|
onKeyDown={(evt) => evt.stopPropagation()}
|
|
104
105
|
onClick={(evt) => {
|
|
@@ -113,7 +114,6 @@ export function AfLookupEdit<T extends Record<string, unknown>>({
|
|
|
113
114
|
<Button
|
|
114
115
|
icon
|
|
115
116
|
variant="tertiary"
|
|
116
|
-
data-size="sm"
|
|
117
117
|
aria-label={getLocalizedString("Search")}
|
|
118
118
|
onClick={(evt) => {
|
|
119
119
|
evt.stopPropagation();
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* row inside is moved sideways with a transform to follow it.
|
|
17
17
|
*/
|
|
18
18
|
.ObLookupGrid-header {
|
|
19
|
+
/* The height is set inline from the size mode (`useDsSize`); this is the fallback. */
|
|
19
20
|
height: 40px;
|
|
20
21
|
overflow: hidden;
|
|
21
22
|
border-bottom: 1px solid var(--ds-color-neutral-border-default);
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
display: flex;
|
|
36
37
|
align-items: center;
|
|
37
38
|
height: 100%;
|
|
38
|
-
padding: 0 8px;
|
|
39
|
+
padding: 0 var(--ds-size-2, 8px);
|
|
39
40
|
overflow: hidden;
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -123,7 +124,7 @@
|
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
.ObLookupGrid-cell {
|
|
126
|
-
padding: 0 8px;
|
|
127
|
+
padding: 0 var(--ds-size-2, 8px);
|
|
127
128
|
height: 100%;
|
|
128
129
|
display: flex;
|
|
129
130
|
align-items: center;
|
|
@@ -7,9 +7,10 @@ import clsx from "clsx";
|
|
|
7
7
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
8
8
|
import { List, type RowComponentProps, useListRef } from "react-window";
|
|
9
9
|
import { getLocalizedString } from "../i18n.js";
|
|
10
|
+
import { useDsSize } from "../theme/sizeMode.js";
|
|
10
11
|
|
|
12
|
+
/** The row and header height until `--ds-size-10` is measured, which follows the size mode like AfGrid's rows. */
|
|
11
13
|
const ROW_HEIGHT = 40;
|
|
12
|
-
const HEADER_HEIGHT = 40;
|
|
13
14
|
/** Narrowest a column can be made, and what a flexible column counts for in the minimum width */
|
|
14
15
|
const MIN_COLUMN_WIDTH = 80;
|
|
15
16
|
|
|
@@ -190,6 +191,8 @@ export function LookupGrid<T extends Record<string, unknown>>({
|
|
|
190
191
|
}: LookupGridProps<T>) {
|
|
191
192
|
let listRef = useListRef(null);
|
|
192
193
|
let headerRowRef = useRef<HTMLDivElement>(null);
|
|
194
|
+
let rootRef = useRef<HTMLDivElement>(null);
|
|
195
|
+
let rowHeight = useDsSize(rootRef, 10, ROW_HEIGHT);
|
|
193
196
|
let [resizedWidths, setResizedWidths] = useState<Partial<Record<string, number>>>({});
|
|
194
197
|
let [resizing, setResizing] = useState<{ field: string; startWidth: number } | null>(null);
|
|
195
198
|
let [listInnerWidth, setListInnerWidth] = useState<number>();
|
|
@@ -246,12 +249,13 @@ export function LookupGrid<T extends Record<string, unknown>>({
|
|
|
246
249
|
|
|
247
250
|
return (
|
|
248
251
|
<div
|
|
252
|
+
ref={rootRef}
|
|
249
253
|
role="grid"
|
|
250
254
|
aria-label={getLocalizedString("Search results")}
|
|
251
255
|
className="ObLookupGrid-root"
|
|
252
256
|
data-resizing={resizing ? "" : undefined}
|
|
253
257
|
>
|
|
254
|
-
<div className="ObLookupGrid-header">
|
|
258
|
+
<div className="ObLookupGrid-header" style={{ height: rowHeight }}>
|
|
255
259
|
<div
|
|
256
260
|
role="row"
|
|
257
261
|
ref={headerRowRef}
|
|
@@ -301,9 +305,9 @@ export function LookupGrid<T extends Record<string, unknown>>({
|
|
|
301
305
|
role="rowgroup"
|
|
302
306
|
listRef={listRef}
|
|
303
307
|
className="ObLookupGrid-list"
|
|
304
|
-
style={{ height: Math.max(0, height -
|
|
308
|
+
style={{ height: Math.max(0, height - rowHeight) }}
|
|
305
309
|
rowCount={data.length}
|
|
306
|
-
rowHeight={
|
|
310
|
+
rowHeight={rowHeight}
|
|
307
311
|
overscanCount={5}
|
|
308
312
|
onScroll={handleScroll}
|
|
309
313
|
rowComponent={LookupGridRow}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
.ObAutocomplete-root {
|
|
2
|
-
--obAutocomplete-padding: 0.25rem;
|
|
3
|
-
--obAutocomplete-gap: 0.25rem;
|
|
2
|
+
--obAutocomplete-padding: var(--ds-size-1, 0.25rem);
|
|
3
|
+
--obAutocomplete-gap: var(--ds-size-1, 0.25rem);
|
|
4
4
|
|
|
5
5
|
display: inline-grid;
|
|
6
6
|
}
|
|
@@ -25,22 +25,11 @@
|
|
|
25
25
|
|
|
26
26
|
/*
|
|
27
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
|
|
29
|
-
* `data-size` rescales
|
|
30
|
-
* `Select` of the same size.
|
|
31
|
-
*
|
|
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. The padding and gap are `--ds-size-1`, which follows
|
|
31
|
+
* the size mode on its own, so no size needs a rule of its own.
|
|
32
32
|
*/
|
|
33
|
-
.ObAutocomplete-root[data-size="sm"] {
|
|
34
|
-
--obAutocomplete-padding: 0.25rem;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.ObAutocomplete-root[data-size="md"] {
|
|
38
|
-
--obAutocomplete-padding: 0.25rem;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.ObAutocomplete-root[data-size="lg"] {
|
|
42
|
-
--obAutocomplete-padding: 0.375rem;
|
|
43
|
-
}
|
|
44
33
|
|
|
45
34
|
/*
|
|
46
35
|
* The control is the visual input: it owns the border, background, hover and
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
import { useField } from "@olenbetong/appframe-react";
|
|
2
|
-
|
|
3
|
-
import { NumberInput, type NumberInputProps } from "../input/NumberInput.js";
|
|
4
|
-
|
|
5
|
-
export type BoundNumberInputProps = Omit<NumberInputProps, "value" | "onChange" | "onKeyDown"> & {
|
|
6
|
-
/**
|
|
7
|
-
* Name of the number field in the data object from context
|
|
8
|
-
*/
|
|
9
|
-
field: string;
|
|
10
|
-
/**
|
|
11
|
-
* Called with the new value before it is written to the data object
|
|
12
|
-
*/
|
|
13
|
-
onChange?: (value: number | null) => void;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* A `NumberInput` bound to a number field of the data object from `DataObjectProvider`
|
|
18
|
-
* via `useField`. Escape resets the field, and a conversion error from the data object is
|
|
19
|
-
* shown below the input unless an `error` is passed.
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* ```jsx
|
|
23
|
-
* <BoundNumberInput field="TotHrs" label="Total hours" step={0.25} min={0} max={24} />
|
|
24
|
-
* ```
|
|
25
|
-
*/
|
|
26
|
-
export function BoundNumberInput({ field, onChange: onChangeProp, error: errorProp, ...props }: BoundNumberInputProps) {
|
|
27
|
-
let { error, value, onChange, onKeyDown } = useField<any, any>(field);
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<NumberInput
|
|
31
|
-
name={field}
|
|
32
|
-
{...props}
|
|
33
|
-
error={errorProp ?? error ?? undefined}
|
|
34
|
-
value={typeof value === "number" ? value : null}
|
|
35
|
-
onChange={(next) => {
|
|
36
|
-
onChangeProp?.(next);
|
|
37
|
-
onChange({ target: { value: next } });
|
|
38
|
-
}}
|
|
39
|
-
onKeyDown={(event) => onKeyDown(event)}
|
|
40
|
-
/>
|
|
41
|
-
);
|
|
42
|
-
}
|
|
1
|
+
import { useField } from "@olenbetong/appframe-react";
|
|
2
|
+
|
|
3
|
+
import { NumberInput, type NumberInputProps } from "../input/NumberInput.js";
|
|
4
|
+
|
|
5
|
+
export type BoundNumberInputProps = Omit<NumberInputProps, "value" | "onChange" | "onKeyDown"> & {
|
|
6
|
+
/**
|
|
7
|
+
* Name of the number field in the data object from context
|
|
8
|
+
*/
|
|
9
|
+
field: string;
|
|
10
|
+
/**
|
|
11
|
+
* Called with the new value before it is written to the data object
|
|
12
|
+
*/
|
|
13
|
+
onChange?: (value: number | null) => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A `NumberInput` bound to a number field of the data object from `DataObjectProvider`
|
|
18
|
+
* via `useField`. Escape resets the field, and a conversion error from the data object is
|
|
19
|
+
* shown below the input unless an `error` is passed.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```jsx
|
|
23
|
+
* <BoundNumberInput field="TotHrs" label="Total hours" step={0.25} min={0} max={24} />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function BoundNumberInput({ field, onChange: onChangeProp, error: errorProp, ...props }: BoundNumberInputProps) {
|
|
27
|
+
let { error, value, onChange, onKeyDown } = useField<any, any>(field);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<NumberInput
|
|
31
|
+
name={field}
|
|
32
|
+
{...props}
|
|
33
|
+
error={errorProp ?? error ?? undefined}
|
|
34
|
+
value={typeof value === "number" ? value : null}
|
|
35
|
+
onChange={(next) => {
|
|
36
|
+
onChangeProp?.(next);
|
|
37
|
+
onChange({ target: { value: next } });
|
|
38
|
+
}}
|
|
39
|
+
onKeyDown={(event) => onKeyDown(event)}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -1,102 +1,102 @@
|
|
|
1
|
-
import { Textfield, type TextfieldProps } from "@digdir/designsystemet-react";
|
|
2
|
-
import { useField } from "@olenbetong/appframe-react";
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
4
|
-
import { getLocalizedString, localize } from "../i18n.js";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* A Designsystemet `Textfield` bound to an Appframe data source field via `useField`.
|
|
8
|
-
*
|
|
9
|
-
* Adds client-side numeric validation: rejects non-numeric characters and enforces
|
|
10
|
-
* optional `precision` (total digits) and `scale` (decimal places) constraints.
|
|
11
|
-
* Validation errors are reported to the parent via the `onError` callback so the
|
|
12
|
-
* parent can track whether any field is in an error state (e.g., to disable Save).
|
|
13
|
-
*
|
|
14
|
-
* On blur the displayed value is normalized to a JS `Number` (comma decimal separators
|
|
15
|
-
* are converted to dots) so the data source always stores a proper numeric value.
|
|
16
|
-
*/
|
|
17
|
-
export type BoundNumericTextFieldProps = Omit<TextfieldProps, "onChange" | "onKeyDown" | "onError"> & {
|
|
18
|
-
field: string;
|
|
19
|
-
precision?: number;
|
|
20
|
-
scale?: number;
|
|
21
|
-
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
22
|
-
onError?: (error: string | null) => void;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export function BoundNumericTextField({
|
|
26
|
-
field,
|
|
27
|
-
precision,
|
|
28
|
-
scale,
|
|
29
|
-
onChange: onChangeProp,
|
|
30
|
-
onError,
|
|
31
|
-
...props
|
|
32
|
-
}: BoundNumericTextFieldProps) {
|
|
33
|
-
let { value, setValue, onChange, onKeyDown } = useField<any, any>(field);
|
|
34
|
-
|
|
35
|
-
let [error, setError] = useState<string | null>(null);
|
|
36
|
-
|
|
37
|
-
function checkNumber(value: string, precision: number, scale: number) {
|
|
38
|
-
if (value[0] === "0") {
|
|
39
|
-
value = value[0].replace("0", "");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let [integer, decimal] = value.split(",");
|
|
43
|
-
if (value.includes(".")) {
|
|
44
|
-
[integer, decimal] = value.split(".");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
integer = integer.replace("-", "");
|
|
48
|
-
|
|
49
|
-
if (integer.length > precision - scale) {
|
|
50
|
-
setError(localize`The amount of whole numbers is too high. Allowed limit is ${precision - scale}`);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (decimal && decimal.length > scale) {
|
|
55
|
-
setError(localize`The amount of decimal places is too high. Allowed limit is ${scale}`);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
setError(null);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function checkError(value: string, precision: number | undefined, scale: number | undefined) {
|
|
63
|
-
let regExp = /[a-zA-Z `!@#$%^&*()_+=[\]{};':"\\|<>/?~]/;
|
|
64
|
-
|
|
65
|
-
if (regExp.test(value)) {
|
|
66
|
-
setError(getLocalizedString("Please enter a valid number"));
|
|
67
|
-
} else if (value.substring(1).includes("-")) {
|
|
68
|
-
setError(getLocalizedString("Please enter a valid number"));
|
|
69
|
-
} else if (precision !== undefined && scale !== undefined) {
|
|
70
|
-
checkNumber(value, precision, scale);
|
|
71
|
-
} else {
|
|
72
|
-
setError(null);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
onError?.(error);
|
|
78
|
-
}, [error, onError]);
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<Textfield
|
|
82
|
-
name={field}
|
|
83
|
-
error={error ?? undefined}
|
|
84
|
-
{...(props as any)}
|
|
85
|
-
value={value ?? ""}
|
|
86
|
-
onChange={(event) => {
|
|
87
|
-
onChangeProp?.(event as any);
|
|
88
|
-
if (!event.defaultPrevented) {
|
|
89
|
-
checkError((event.target as HTMLInputElement).value, precision, scale);
|
|
90
|
-
onChange(event as any);
|
|
91
|
-
}
|
|
92
|
-
}}
|
|
93
|
-
onKeyDown={(evt) => onKeyDown(evt as any)}
|
|
94
|
-
onBlur={(evt) => {
|
|
95
|
-
const raw = (evt.target as HTMLInputElement).value;
|
|
96
|
-
if (raw !== "") {
|
|
97
|
-
setValue(Number(raw.replaceAll(",", ".")));
|
|
98
|
-
}
|
|
99
|
-
}}
|
|
100
|
-
/>
|
|
101
|
-
);
|
|
102
|
-
}
|
|
1
|
+
import { Textfield, type TextfieldProps } from "@digdir/designsystemet-react";
|
|
2
|
+
import { useField } from "@olenbetong/appframe-react";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { getLocalizedString, localize } from "../i18n.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A Designsystemet `Textfield` bound to an Appframe data source field via `useField`.
|
|
8
|
+
*
|
|
9
|
+
* Adds client-side numeric validation: rejects non-numeric characters and enforces
|
|
10
|
+
* optional `precision` (total digits) and `scale` (decimal places) constraints.
|
|
11
|
+
* Validation errors are reported to the parent via the `onError` callback so the
|
|
12
|
+
* parent can track whether any field is in an error state (e.g., to disable Save).
|
|
13
|
+
*
|
|
14
|
+
* On blur the displayed value is normalized to a JS `Number` (comma decimal separators
|
|
15
|
+
* are converted to dots) so the data source always stores a proper numeric value.
|
|
16
|
+
*/
|
|
17
|
+
export type BoundNumericTextFieldProps = Omit<TextfieldProps, "onChange" | "onKeyDown" | "onError"> & {
|
|
18
|
+
field: string;
|
|
19
|
+
precision?: number;
|
|
20
|
+
scale?: number;
|
|
21
|
+
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
22
|
+
onError?: (error: string | null) => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function BoundNumericTextField({
|
|
26
|
+
field,
|
|
27
|
+
precision,
|
|
28
|
+
scale,
|
|
29
|
+
onChange: onChangeProp,
|
|
30
|
+
onError,
|
|
31
|
+
...props
|
|
32
|
+
}: BoundNumericTextFieldProps) {
|
|
33
|
+
let { value, setValue, onChange, onKeyDown } = useField<any, any>(field);
|
|
34
|
+
|
|
35
|
+
let [error, setError] = useState<string | null>(null);
|
|
36
|
+
|
|
37
|
+
function checkNumber(value: string, precision: number, scale: number) {
|
|
38
|
+
if (value[0] === "0") {
|
|
39
|
+
value = value[0].replace("0", "");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let [integer, decimal] = value.split(",");
|
|
43
|
+
if (value.includes(".")) {
|
|
44
|
+
[integer, decimal] = value.split(".");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
integer = integer.replace("-", "");
|
|
48
|
+
|
|
49
|
+
if (integer.length > precision - scale) {
|
|
50
|
+
setError(localize`The amount of whole numbers is too high. Allowed limit is ${precision - scale}`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (decimal && decimal.length > scale) {
|
|
55
|
+
setError(localize`The amount of decimal places is too high. Allowed limit is ${scale}`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setError(null);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function checkError(value: string, precision: number | undefined, scale: number | undefined) {
|
|
63
|
+
let regExp = /[a-zA-Z `!@#$%^&*()_+=[\]{};':"\\|<>/?~]/;
|
|
64
|
+
|
|
65
|
+
if (regExp.test(value)) {
|
|
66
|
+
setError(getLocalizedString("Please enter a valid number"));
|
|
67
|
+
} else if (value.substring(1).includes("-")) {
|
|
68
|
+
setError(getLocalizedString("Please enter a valid number"));
|
|
69
|
+
} else if (precision !== undefined && scale !== undefined) {
|
|
70
|
+
checkNumber(value, precision, scale);
|
|
71
|
+
} else {
|
|
72
|
+
setError(null);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
onError?.(error);
|
|
78
|
+
}, [error, onError]);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Textfield
|
|
82
|
+
name={field}
|
|
83
|
+
error={error ?? undefined}
|
|
84
|
+
{...(props as any)}
|
|
85
|
+
value={value ?? ""}
|
|
86
|
+
onChange={(event) => {
|
|
87
|
+
onChangeProp?.(event as any);
|
|
88
|
+
if (!event.defaultPrevented) {
|
|
89
|
+
checkError((event.target as HTMLInputElement).value, precision, scale);
|
|
90
|
+
onChange(event as any);
|
|
91
|
+
}
|
|
92
|
+
}}
|
|
93
|
+
onKeyDown={(evt) => onKeyDown(evt as any)}
|
|
94
|
+
onBlur={(evt) => {
|
|
95
|
+
const raw = (evt.target as HTMLInputElement).value;
|
|
96
|
+
if (raw !== "") {
|
|
97
|
+
setValue(Number(raw.replaceAll(",", ".")));
|
|
98
|
+
}
|
|
99
|
+
}}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
import { Textfield, type TextfieldProps } from "@digdir/designsystemet-react";
|
|
2
|
-
import { useField } from "@olenbetong/appframe-react";
|
|
3
|
-
import { forwardRef } from "react";
|
|
4
|
-
|
|
5
|
-
/*
|
|
6
|
-
* `TextfieldProps` is a union of the input and the textarea variant. A plain `Omit` over it keeps
|
|
7
|
-
* only the props both branches share, which silently drops the textarea-only ones (`rows`,
|
|
8
|
-
* `cols`) — so `multiline` was accepted but `rows` was not. Distributing the `Omit` over the union
|
|
9
|
-
* keeps each branch intact.
|
|
10
|
-
*/
|
|
11
|
-
type OmitBoundProps<T> = T extends unknown ? Omit<T, "value" | "onChange" | "onKeyDown"> : never;
|
|
12
|
-
|
|
13
|
-
export type BoundTextFieldProps = OmitBoundProps<TextfieldProps> & {
|
|
14
|
-
field: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export const BoundTextField = forwardRef<HTMLInputElement & HTMLTextAreaElement, BoundTextFieldProps>(
|
|
18
|
-
function BoundTextField({ field, ...props }, ref) {
|
|
19
|
-
let { error, value, onChange, onKeyDown } = useField<any, any>(field);
|
|
20
|
-
|
|
21
|
-
let inputValue = value;
|
|
22
|
-
if (props.type === "date" && value instanceof Date) {
|
|
23
|
-
inputValue = value.toISOString().split("T")[0];
|
|
24
|
-
} else if (["datetime", "datetime-local"].includes(props.type ?? "") && value instanceof Date) {
|
|
25
|
-
inputValue = value.toISOString().replace("Z", "");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<Textfield
|
|
30
|
-
name={field}
|
|
31
|
-
error={error ?? undefined}
|
|
32
|
-
{...(props as any)}
|
|
33
|
-
value={inputValue ?? ""}
|
|
34
|
-
onChange={(evt) => {
|
|
35
|
-
if (["datetime", "datetime-local"].includes(props.type ?? "")) {
|
|
36
|
-
onChange(
|
|
37
|
-
evt as any,
|
|
38
|
-
(evt.target as HTMLInputElement).value ? `${(evt.target as HTMLInputElement).value}Z` : null,
|
|
39
|
-
);
|
|
40
|
-
} else {
|
|
41
|
-
onChange(evt as any);
|
|
42
|
-
}
|
|
43
|
-
}}
|
|
44
|
-
onKeyDown={(evt) => onKeyDown(evt as any)}
|
|
45
|
-
ref={ref}
|
|
46
|
-
/>
|
|
47
|
-
);
|
|
48
|
-
},
|
|
49
|
-
);
|
|
1
|
+
import { Textfield, type TextfieldProps } from "@digdir/designsystemet-react";
|
|
2
|
+
import { useField } from "@olenbetong/appframe-react";
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* `TextfieldProps` is a union of the input and the textarea variant. A plain `Omit` over it keeps
|
|
7
|
+
* only the props both branches share, which silently drops the textarea-only ones (`rows`,
|
|
8
|
+
* `cols`) — so `multiline` was accepted but `rows` was not. Distributing the `Omit` over the union
|
|
9
|
+
* keeps each branch intact.
|
|
10
|
+
*/
|
|
11
|
+
type OmitBoundProps<T> = T extends unknown ? Omit<T, "value" | "onChange" | "onKeyDown"> : never;
|
|
12
|
+
|
|
13
|
+
export type BoundTextFieldProps = OmitBoundProps<TextfieldProps> & {
|
|
14
|
+
field: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const BoundTextField = forwardRef<HTMLInputElement & HTMLTextAreaElement, BoundTextFieldProps>(
|
|
18
|
+
function BoundTextField({ field, ...props }, ref) {
|
|
19
|
+
let { error, value, onChange, onKeyDown } = useField<any, any>(field);
|
|
20
|
+
|
|
21
|
+
let inputValue = value;
|
|
22
|
+
if (props.type === "date" && value instanceof Date) {
|
|
23
|
+
inputValue = value.toISOString().split("T")[0];
|
|
24
|
+
} else if (["datetime", "datetime-local"].includes(props.type ?? "") && value instanceof Date) {
|
|
25
|
+
inputValue = value.toISOString().replace("Z", "");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Textfield
|
|
30
|
+
name={field}
|
|
31
|
+
error={error ?? undefined}
|
|
32
|
+
{...(props as any)}
|
|
33
|
+
value={inputValue ?? ""}
|
|
34
|
+
onChange={(evt) => {
|
|
35
|
+
if (["datetime", "datetime-local"].includes(props.type ?? "")) {
|
|
36
|
+
onChange(
|
|
37
|
+
evt as any,
|
|
38
|
+
(evt.target as HTMLInputElement).value ? `${(evt.target as HTMLInputElement).value}Z` : null,
|
|
39
|
+
);
|
|
40
|
+
} else {
|
|
41
|
+
onChange(evt as any);
|
|
42
|
+
}
|
|
43
|
+
}}
|
|
44
|
+
onKeyDown={(evt) => onKeyDown(evt as any)}
|
|
45
|
+
ref={ref}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
);
|