@olenbetong/appframe-ds 1.8.0 → 1.9.1
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 +14 -0
- package/package.json +2 -2
- package/src/AfLookup/Lookup.css +16 -1
- package/src/AfLookup/Lookup.tsx +37 -12
- package/src/AfLookup/LookupCombobox.tsx +1 -9
- package/src/AfLookup/LookupEdit.tsx +5 -4
- 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 +3 -4
- 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,19 @@
|
|
|
1
1
|
# @olenbetong/appframe-ds
|
|
2
2
|
|
|
3
|
+
## 1.9.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0f57479: Lookups: the dialog lines up with the input instead of the label beside or above it, grows upwards when it is placed above the field, and follows the field's size mode also when opened with ArrowDown or F4. `GridEditLookup` takes `editable`, so a value can be typed into the cell.
|
|
8
|
+
|
|
9
|
+
## 1.9.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
15
|
+
`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.
|
|
16
|
+
|
|
3
17
|
## 1.8.0
|
|
4
18
|
|
|
5
19
|
### 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.1",
|
|
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,9 @@
|
|
|
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
|
+
/* Set by AfLookup and AfLookupEdit; the dialog lines up with the input, not its label. */
|
|
7
|
+
anchor-name: var(--ObLookup-anchor);
|
|
6
8
|
}
|
|
7
9
|
}
|
|
8
10
|
|
|
@@ -22,6 +24,14 @@
|
|
|
22
24
|
padding-inline-end: var(--ds-size-1, 0.25rem);
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
/*
|
|
28
|
+
* The buttons follow the inherited size mode, so in the same mode as the input they would be as
|
|
29
|
+
* tall as it. Fit them inside the input instead, with --ds-size-1 to spare above and below.
|
|
30
|
+
*/
|
|
31
|
+
.ObLookup-buttons > .ds-button {
|
|
32
|
+
--dsc-button-size: calc(var(--ds-size-12) - 2 * var(--ds-size-1));
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
.ObLookup-dialog {
|
|
26
36
|
border: 0;
|
|
27
37
|
margin-block-start: 0;
|
|
@@ -136,6 +146,11 @@
|
|
|
136
146
|
}
|
|
137
147
|
}
|
|
138
148
|
|
|
149
|
+
/* Placed above the field (see useLookup): grow upwards from it. */
|
|
150
|
+
&[data-above] {
|
|
151
|
+
transform-origin: bottom left;
|
|
152
|
+
}
|
|
153
|
+
|
|
139
154
|
&[data-anchor="manual"] {
|
|
140
155
|
inset: unset;
|
|
141
156
|
position: fixed;
|
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,16 +72,44 @@ 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);
|
|
77
80
|
openDialog();
|
|
81
|
+
setGrowDirection();
|
|
82
|
+
// A manually anchored dialog (in a virtualized grid) is placed in the next frame.
|
|
83
|
+
requestAnimationFrame(setGrowDirection);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// When there is no room below the field, the dialog is placed above it; it then grows upwards
|
|
87
|
+
// from the field instead of down towards it. Its bottom edge tells, whatever the opening scale.
|
|
88
|
+
function setGrowDirection() {
|
|
89
|
+
let $dialog = dialogRef.current;
|
|
90
|
+
let anchor = document.getElementById(inputId) ?? getAnchorElement?.();
|
|
91
|
+
if (!$dialog || !anchor) return;
|
|
92
|
+
|
|
93
|
+
let above = $dialog.getBoundingClientRect().bottom <= anchor.getBoundingClientRect().top + 1;
|
|
94
|
+
$dialog.toggleAttribute("data-above", above);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// useLookupState opens the dialog on ArrowDown and F4 itself, which would skip the size mode and
|
|
98
|
+
// the grow direction above; every way of opening it goes through handleOpenDialog instead.
|
|
99
|
+
function handleKeyDown(event: React.KeyboardEvent<HTMLElement>) {
|
|
100
|
+
if (event.key === "ArrowDown" || event.key === "F4") {
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
event.stopPropagation();
|
|
103
|
+
handleOpenDialog();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
onKeyDown(event);
|
|
78
107
|
}
|
|
79
108
|
|
|
80
109
|
return {
|
|
81
110
|
open,
|
|
82
111
|
openDialog: handleOpenDialog,
|
|
83
|
-
onKeyDown,
|
|
112
|
+
onKeyDown: handleKeyDown,
|
|
84
113
|
onChange: handleChange,
|
|
85
114
|
dialogRef,
|
|
86
115
|
anchorName,
|
|
@@ -132,13 +161,16 @@ export function AfLookup<T extends Record<string, unknown>>({
|
|
|
132
161
|
}
|
|
133
162
|
|
|
134
163
|
return (
|
|
135
|
-
|
|
164
|
+
// The dialog anchors to the input itself (see Lookup.css), not to the label beside or above it.
|
|
165
|
+
<div style={{ "--ObLookup-anchor": anchorName } as React.CSSProperties}>
|
|
136
166
|
<div
|
|
137
167
|
className="ObLookup-inputWrap"
|
|
138
168
|
style={
|
|
139
169
|
{
|
|
140
170
|
display: fullWidth ? "block" : "inline-block",
|
|
141
|
-
"--ObLookup-buttons-width": hasClear
|
|
171
|
+
"--ObLookup-buttons-width": hasClear
|
|
172
|
+
? "calc(2 * var(--ds-size-12) - var(--ds-size-2))"
|
|
173
|
+
: "var(--ds-size-12)",
|
|
142
174
|
} as React.CSSProperties
|
|
143
175
|
}
|
|
144
176
|
>
|
|
@@ -157,7 +189,6 @@ export function AfLookup<T extends Record<string, unknown>>({
|
|
|
157
189
|
<Button
|
|
158
190
|
icon
|
|
159
191
|
variant="tertiary"
|
|
160
|
-
data-size="sm"
|
|
161
192
|
aria-label={getLocalizedString("Clear")}
|
|
162
193
|
onKeyDown={(evt) => evt.stopPropagation()}
|
|
163
194
|
onClick={(evt) => {
|
|
@@ -169,13 +200,7 @@ export function AfLookup<T extends Record<string, unknown>>({
|
|
|
169
200
|
<XMarkIcon />
|
|
170
201
|
</Button>
|
|
171
202
|
)}
|
|
172
|
-
<Button
|
|
173
|
-
icon
|
|
174
|
-
variant="tertiary"
|
|
175
|
-
data-size="sm"
|
|
176
|
-
aria-label={getLocalizedString("Search")}
|
|
177
|
-
onClick={() => openDialog()}
|
|
178
|
-
>
|
|
203
|
+
<Button icon variant="tertiary" aria-label={getLocalizedString("Search")} onClick={() => openDialog()}>
|
|
179
204
|
<MagnifyingGlassIcon />
|
|
180
205
|
</Button>
|
|
181
206
|
</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"
|
|
@@ -73,13 +73,16 @@ export function AfLookupEdit<T extends Record<string, unknown>>({
|
|
|
73
73
|
let hasClear = !!(nullable && value && onChange);
|
|
74
74
|
|
|
75
75
|
return (
|
|
76
|
-
|
|
76
|
+
// The dialog anchors to the input itself (see Lookup.css), not to the label beside or above it.
|
|
77
|
+
<div style={{ "--ObLookup-anchor": anchorName } as React.CSSProperties}>
|
|
77
78
|
<div
|
|
78
79
|
className="ObLookup-inputWrap"
|
|
79
80
|
style={
|
|
80
81
|
{
|
|
81
82
|
display: fullWidth ? "block" : "inline-block",
|
|
82
|
-
"--ObLookup-buttons-width": hasClear
|
|
83
|
+
"--ObLookup-buttons-width": hasClear
|
|
84
|
+
? "calc(2 * var(--ds-size-12) - var(--ds-size-2))"
|
|
85
|
+
: "var(--ds-size-12)",
|
|
83
86
|
} as React.CSSProperties
|
|
84
87
|
}
|
|
85
88
|
>
|
|
@@ -98,7 +101,6 @@ export function AfLookupEdit<T extends Record<string, unknown>>({
|
|
|
98
101
|
<Button
|
|
99
102
|
icon
|
|
100
103
|
variant="tertiary"
|
|
101
|
-
data-size="sm"
|
|
102
104
|
aria-label={getLocalizedString("Clear")}
|
|
103
105
|
onKeyDown={(evt) => evt.stopPropagation()}
|
|
104
106
|
onClick={(evt) => {
|
|
@@ -113,7 +115,6 @@ export function AfLookupEdit<T extends Record<string, unknown>>({
|
|
|
113
115
|
<Button
|
|
114
116
|
icon
|
|
115
117
|
variant="tertiary"
|
|
116
|
-
data-size="sm"
|
|
117
118
|
aria-label={getLocalizedString("Search")}
|
|
118
119
|
onClick={(evt) => {
|
|
119
120
|
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
|
+
}
|