@olenbetong/appframe-ds 1.6.0 → 1.7.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 +21 -0
- package/package.json +3 -3
- package/scripts/copyAssets.mjs +27 -27
- package/src/autocomplete/Autocomplete.css +253 -253
- package/src/autocomplete/Autocomplete.tsx +320 -320
- package/src/autocomplete/AutocompleteFrame.tsx +168 -168
- package/src/autocomplete/Combobox.tsx +286 -286
- package/src/autocomplete/index.ts +4 -4
- package/src/autocomplete/types.ts +204 -204
- package/src/autocomplete/utils.ts +183 -183
- 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 +309 -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 +201 -201
- package/src/filter/FilterStringField.tsx +78 -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/formatters.ts +23 -0
- package/src/grid/index.css +8 -2
- package/src/grid/index.tsx +73 -3
- package/src/grid/useAfColumns.tsx +28 -4
- package/src/grid/useAfGridApi.ts +3 -0
- package/src/grid/utcDate.test.ts +25 -0
- package/src/grid/utcDate.ts +28 -0
- package/src/grid/utcDateEditing.tsx +11 -0
- package/src/i18n.ts +13 -13
- package/src/index.ts +14 -14
- package/src/input/InputAdornments.css +80 -80
- package/src/input/InputAdornments.tsx +102 -102
- package/src/input/NumberInput.css +9 -9
- package/src/input/NumberInput.test.tsx +212 -212
- package/src/input/NumberInput.tsx +268 -268
- package/src/input/index.ts +3 -3
- package/src/input/numberFormat.test.ts +79 -79
- package/src/input/numberFormat.ts +94 -94
- package/src/layout/AppMenu.tsx +74 -74
- package/src/layout/AppMenuDrawer.css +71 -71
- package/src/layout/AppMenuDrawer.tsx +122 -122
- package/src/layout/AppMenuItem.css +6 -2
- 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/mdi/Mdi.css +8 -8
- package/src/mdi/Mdi.test.tsx +91 -91
- package/src/mdi/Mdi.tsx +141 -141
- package/src/mdi/MdiConfirmCloseDialog.tsx +47 -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/useDsColorScheme.ts +53 -53
- package/tsconfig.build.json +9 -9
- package/tsconfig.json +8 -8
- package/vitest.config.ts +10 -10
|
@@ -28,8 +28,15 @@ import React, { useMemo } from "react";
|
|
|
28
28
|
import { AfHeaderFilterCell } from "./AfHeaderFilterCell.js";
|
|
29
29
|
import { deleteRow } from "./editing.js";
|
|
30
30
|
import { operators } from "./filter.js";
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
dateTimeValueFormatter,
|
|
33
|
+
dateValueFormatter,
|
|
34
|
+
localDateTimeValueFormatter,
|
|
35
|
+
localDateValueFormatter,
|
|
36
|
+
} from "./formatters.js";
|
|
32
37
|
import { isNewItemRow } from "./useAfNewItemRow.js";
|
|
38
|
+
import { parseUtcDate, parseUtcDateTime } from "./utcDate.js";
|
|
39
|
+
import { renderUtcDateEditCell, renderUtcDateTimeEditCell } from "./utcDateEditing.js";
|
|
33
40
|
import { getLocalizedString } from "../i18n.js";
|
|
34
41
|
|
|
35
42
|
/** `auto-generated-group-footer-`, without spelling it out: every group's total row shares it. */
|
|
@@ -48,6 +55,12 @@ export type AfGridColDef = GridColDef & {
|
|
|
48
55
|
* from server-side sort and filter operations.
|
|
49
56
|
*/
|
|
50
57
|
calculated?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Date and dateTime columns hold wall-clock values by default: stored without a time zone,
|
|
60
|
+
* shown and edited as they are. Set this for columns holding real UTC instants (such as
|
|
61
|
+
* Created/Updated in newer tables) to show and edit them in the user's local time.
|
|
62
|
+
*/
|
|
63
|
+
storedAsUtc?: boolean;
|
|
51
64
|
};
|
|
52
65
|
|
|
53
66
|
/**
|
|
@@ -133,10 +146,19 @@ export function afColumnsToMuiColumn({
|
|
|
133
146
|
|
|
134
147
|
let valueFormatter: (value: any) => string | number | boolean | object | null | undefined = (value) => value;
|
|
135
148
|
|
|
149
|
+
// Wall-clock dates are shown and edited in UTC (see utcDate); real UTC instants
|
|
150
|
+
// use local time and MUI's own editors
|
|
151
|
+
let storedAsUtc = typeof column !== "string" && !!column.storedAsUtc;
|
|
152
|
+
let dateEditing: Pick<GridColDef, "renderEditCell" | "valueParser"> = {};
|
|
153
|
+
|
|
136
154
|
if (type === "date") {
|
|
137
|
-
valueFormatter = dateValueFormatter;
|
|
155
|
+
valueFormatter = storedAsUtc ? localDateValueFormatter : dateValueFormatter;
|
|
156
|
+
if (!storedAsUtc) dateEditing = { renderEditCell: renderUtcDateEditCell, valueParser: parseUtcDate };
|
|
138
157
|
} else if (type === "dateTime") {
|
|
139
|
-
valueFormatter = dateTimeValueFormatter;
|
|
158
|
+
valueFormatter = storedAsUtc ? localDateTimeValueFormatter : dateTimeValueFormatter;
|
|
159
|
+
if (!storedAsUtc) {
|
|
160
|
+
dateEditing = { renderEditCell: renderUtcDateTimeEditCell, valueParser: parseUtcDateTime };
|
|
161
|
+
}
|
|
140
162
|
}
|
|
141
163
|
|
|
142
164
|
let editable = allowUpdate && !fieldDef?.ignored && !fieldDef?.computed && !fieldDef?.joined;
|
|
@@ -155,18 +177,20 @@ export function afColumnsToMuiColumn({
|
|
|
155
177
|
type: type as GridColType,
|
|
156
178
|
headerName: column,
|
|
157
179
|
valueFormatter,
|
|
180
|
+
...dateEditing,
|
|
158
181
|
editable,
|
|
159
182
|
align,
|
|
160
183
|
headerAlign: align,
|
|
161
184
|
width: currentWidth ?? 150,
|
|
162
185
|
};
|
|
163
186
|
} else {
|
|
164
|
-
let { hide, calculated, ...rest } = column;
|
|
187
|
+
let { hide, calculated, storedAsUtc: _storedAsUtc, ...rest } = column;
|
|
165
188
|
col = {
|
|
166
189
|
headerName: fieldDef?.caption ?? fieldName,
|
|
167
190
|
editable,
|
|
168
191
|
type: type as GridColType,
|
|
169
192
|
valueFormatter,
|
|
193
|
+
...dateEditing,
|
|
170
194
|
align,
|
|
171
195
|
headerAlign: align,
|
|
172
196
|
...rest,
|
package/src/grid/useAfGridApi.ts
CHANGED
|
@@ -62,6 +62,9 @@ export function useAfGridApi(props: Partial<AfGridProps>, apiRef: React.RefObjec
|
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
64
|
apiRef.current?.setPinnedColumns(propsRef.current.initialState?.pinnedColumns ?? {});
|
|
65
|
+
// Grouping and aggregation are part of the layout too: back to what the grid declares.
|
|
66
|
+
apiRef.current?.setRowGroupingModel(propsRef.current.initialState?.rowGrouping?.model ?? []);
|
|
67
|
+
apiRef.current?.setAggregationModel(propsRef.current.initialState?.aggregation?.model ?? {});
|
|
65
68
|
}, [apiRef]);
|
|
66
69
|
|
|
67
70
|
if (apiRef.current) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { fromUtcInputValue, parseUtcDate, parseUtcDateTime, toUtcInputValue } from "./utcDate.js";
|
|
4
|
+
|
|
5
|
+
describe("UTC date editing in AfGrid", () => {
|
|
6
|
+
it("stores an entered date as UTC midnight of that date", () => {
|
|
7
|
+
expect(parseUtcDate("2026-10-10")?.toISOString()).toBe("2026-10-10T00:00:00.000Z");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("stores an entered date and time as that wall-clock time in UTC", () => {
|
|
11
|
+
expect(parseUtcDateTime("2026-10-10T07:30")?.toISOString()).toBe("2026-10-10T07:30:00.000Z");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("shows a stored date as its UTC date", () => {
|
|
15
|
+
let stored = new Date("2026-10-10T00:00:00Z");
|
|
16
|
+
expect(toUtcInputValue(stored, false)).toBe("2026-10-10");
|
|
17
|
+
expect(toUtcInputValue(stored, true)).toBe("2026-10-10T00:00");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("treats empty and invalid values as no date", () => {
|
|
21
|
+
expect(fromUtcInputValue("", false)).toBeNull();
|
|
22
|
+
expect(fromUtcInputValue(null, true)).toBeNull();
|
|
23
|
+
expect(toUtcInputValue(new Date("invalid"), false)).toBe("");
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Appframe dates are wall-clock values stored as UTC (the grid formats them with
|
|
2
|
+
// timeZone "utc"). MUI's own date editors read and write local time, which shifts a date
|
|
3
|
+
// entered in Norway to 22:00 the day before, so date columns are edited in UTC throughout.
|
|
4
|
+
|
|
5
|
+
export function toUtcInputValue(value: unknown, withTime: boolean): string {
|
|
6
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
|
|
7
|
+
return "";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return value.toISOString().slice(0, withTime ? 16 : 10);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function fromUtcInputValue(value: unknown, withTime: boolean): Date | null {
|
|
14
|
+
if (typeof value !== "string" || !value) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let date = new Date(withTime ? `${value.slice(0, 16)}:00Z` : `${value.slice(0, 10)}T00:00:00Z`);
|
|
19
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseUtcDate(value: unknown) {
|
|
23
|
+
return fromUtcInputValue(value, false);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseUtcDateTime(value: unknown) {
|
|
27
|
+
return fromUtcInputValue(value, true);
|
|
28
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { GridEditInputCell, type GridRenderEditCellParams } from "@mui/x-data-grid-premium";
|
|
2
|
+
|
|
3
|
+
import { toUtcInputValue } from "./utcDate.js";
|
|
4
|
+
|
|
5
|
+
export function renderUtcDateEditCell(params: GridRenderEditCellParams) {
|
|
6
|
+
return <GridEditInputCell {...params} type="date" value={toUtcInputValue(params.value, false)} />;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function renderUtcDateTimeEditCell(params: GridRenderEditCellParams) {
|
|
10
|
+
return <GridEditInputCell {...params} type="datetime-local" value={toUtcInputValue(params.value, true)} />;
|
|
11
|
+
}
|
package/src/i18n.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* The package's strings live in their own globalization project, `appframe-ds`, so one
|
|
3
|
-
* translation covers every app and the package can word a term its own way. The templates
|
|
4
|
-
* render the dictionary as af.i18n["appframe-ds"]; on a page that does not (a legacy
|
|
5
|
-
* template) the localizer falls back to the article's strings, exactly as before. See
|
|
6
|
-
* `createLocalizer` in @olenbetong/appframe-core.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { createLocalizer } from "@olenbetong/appframe-core";
|
|
10
|
-
|
|
11
|
-
export const PROJECT = "appframe-ds";
|
|
12
|
-
|
|
13
|
-
export const { getLocalizedString, localize } = createLocalizer(PROJECT);
|
|
1
|
+
/*
|
|
2
|
+
* The package's strings live in their own globalization project, `appframe-ds`, so one
|
|
3
|
+
* translation covers every app and the package can word a term its own way. The templates
|
|
4
|
+
* render the dictionary as af.i18n["appframe-ds"]; on a page that does not (a legacy
|
|
5
|
+
* template) the localizer falls back to the article's strings, exactly as before. See
|
|
6
|
+
* `createLocalizer` in @olenbetong/appframe-core.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createLocalizer } from "@olenbetong/appframe-core";
|
|
10
|
+
|
|
11
|
+
export const PROJECT = "appframe-ds";
|
|
12
|
+
|
|
13
|
+
export const { getLocalizedString, localize } = createLocalizer(PROJECT);
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
export * from "./AfLookup/index.js";
|
|
2
|
-
export * from "./autocomplete/index.js";
|
|
3
|
-
export * from "./binding/index.js";
|
|
4
|
-
export * from "./container/index.js";
|
|
5
|
-
export * from "./cutoff/index.js";
|
|
6
|
-
export * from "./filter/index.js";
|
|
7
|
-
export * from "./input/index.js";
|
|
8
|
-
export * from "./layout/index.js";
|
|
9
|
-
export * from "./link/index.js";
|
|
10
|
-
export * from "./list/index.js";
|
|
11
|
-
export * from "./page/index.js";
|
|
12
|
-
export * from "./paper/index.js";
|
|
13
|
-
export * from "./progress/index.js";
|
|
14
|
-
export * from "./sortable/index.js";
|
|
1
|
+
export * from "./AfLookup/index.js";
|
|
2
|
+
export * from "./autocomplete/index.js";
|
|
3
|
+
export * from "./binding/index.js";
|
|
4
|
+
export * from "./container/index.js";
|
|
5
|
+
export * from "./cutoff/index.js";
|
|
6
|
+
export * from "./filter/index.js";
|
|
7
|
+
export * from "./input/index.js";
|
|
8
|
+
export * from "./layout/index.js";
|
|
9
|
+
export * from "./link/index.js";
|
|
10
|
+
export * from "./list/index.js";
|
|
11
|
+
export * from "./page/index.js";
|
|
12
|
+
export * from "./paper/index.js";
|
|
13
|
+
export * from "./progress/index.js";
|
|
14
|
+
export * from "./sortable/index.js";
|
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* The wrapper is the visual input: it owns the border, background, hover and
|
|
3
|
-
* focus styling, while the inner Designsystemet input is rendered bare so the
|
|
4
|
-
* input and the adornments read as a single control.
|
|
5
|
-
*/
|
|
6
|
-
.ObInputAdornments-root {
|
|
7
|
-
--obInputAdornments-padding: 0.25rem;
|
|
8
|
-
--obInputAdornments-gap: 0.25rem;
|
|
9
|
-
--obInputAdornments-fontSize: var(--ds-font-size-3, 1rem);
|
|
10
|
-
|
|
11
|
-
display: flex;
|
|
12
|
-
align-items: center;
|
|
13
|
-
box-sizing: border-box;
|
|
14
|
-
gap: var(--obInputAdornments-gap);
|
|
15
|
-
padding: var(--obInputAdornments-padding);
|
|
16
|
-
inline-size: 100%;
|
|
17
|
-
color: var(--ds-color-neutral-text-default, #1e2b3c);
|
|
18
|
-
background-color: var(--ds-color-neutral-surface-default, #fff);
|
|
19
|
-
border: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-default, #b3b3b3);
|
|
20
|
-
border-radius: var(--ds-border-radius-md, 0.375rem);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/* Designsystemet components come in sm, md and lg; xs and xl are typography-only sizes */
|
|
24
|
-
.ObInputAdornments-root[data-size="sm"] {
|
|
25
|
-
--obInputAdornments-padding: 0.25rem;
|
|
26
|
-
--obInputAdornments-fontSize: var(--ds-font-size-2, 0.875rem);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.ObInputAdornments-root[data-size="md"] {
|
|
30
|
-
--obInputAdornments-padding: 0.25rem;
|
|
31
|
-
--obInputAdornments-fontSize: var(--ds-font-size-3, 1rem);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
.ObInputAdornments-root[data-size="lg"] {
|
|
35
|
-
--obInputAdornments-padding: 0.375rem;
|
|
36
|
-
--obInputAdornments-fontSize: var(--ds-font-size-4, 1.125rem);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/* The Designsystemet input hover (an inner outline doubling the border), drawn on the wrapper */
|
|
40
|
-
.ObInputAdornments-root:hover:not(
|
|
41
|
-
:focus-within,
|
|
42
|
-
:has(.ObInputAdornments-field:disabled, .ObInputAdornments-field[readonly])
|
|
43
|
-
) {
|
|
44
|
-
outline: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-default, #b3b3b3);
|
|
45
|
-
outline-offset: -0.1px;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
.ObInputAdornments-root:has(.ObInputAdornments-field:focus-visible) {
|
|
49
|
-
box-shadow: var(--dsc-focus-boxShadow);
|
|
50
|
-
outline: var(--dsc-focus-outline);
|
|
51
|
-
outline-offset: var(--ds-border-width-focus, 3px);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
.ObInputAdornments-root:has(.ObInputAdornments-field:disabled) {
|
|
55
|
-
cursor: not-allowed;
|
|
56
|
-
opacity: var(--ds-disabled-opacity, 0.3);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/* The bare input: all the visual styling lives on the wrapper instead */
|
|
60
|
-
.ObInputAdornments-root .ObInputAdornments-field {
|
|
61
|
-
flex: 1;
|
|
62
|
-
min-inline-size: 0;
|
|
63
|
-
font-size: var(--obInputAdornments-fontSize);
|
|
64
|
-
background: transparent;
|
|
65
|
-
border: none;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
.ObInputAdornments-root .ObInputAdornments-field:disabled {
|
|
69
|
-
opacity: 1;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.ObInputAdornments-root .ObInputAdornments-field:hover {
|
|
73
|
-
outline: none;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.ObInputAdornments-root .ObInputAdornments-field:focus-visible {
|
|
77
|
-
box-shadow: none;
|
|
78
|
-
outline: none;
|
|
79
|
-
outline-offset: 0;
|
|
80
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
* The wrapper is the visual input: it owns the border, background, hover and
|
|
3
|
+
* focus styling, while the inner Designsystemet input is rendered bare so the
|
|
4
|
+
* input and the adornments read as a single control.
|
|
5
|
+
*/
|
|
6
|
+
.ObInputAdornments-root {
|
|
7
|
+
--obInputAdornments-padding: 0.25rem;
|
|
8
|
+
--obInputAdornments-gap: 0.25rem;
|
|
9
|
+
--obInputAdornments-fontSize: var(--ds-font-size-3, 1rem);
|
|
10
|
+
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
gap: var(--obInputAdornments-gap);
|
|
15
|
+
padding: var(--obInputAdornments-padding);
|
|
16
|
+
inline-size: 100%;
|
|
17
|
+
color: var(--ds-color-neutral-text-default, #1e2b3c);
|
|
18
|
+
background-color: var(--ds-color-neutral-surface-default, #fff);
|
|
19
|
+
border: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-default, #b3b3b3);
|
|
20
|
+
border-radius: var(--ds-border-radius-md, 0.375rem);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Designsystemet components come in sm, md and lg; xs and xl are typography-only sizes */
|
|
24
|
+
.ObInputAdornments-root[data-size="sm"] {
|
|
25
|
+
--obInputAdornments-padding: 0.25rem;
|
|
26
|
+
--obInputAdornments-fontSize: var(--ds-font-size-2, 0.875rem);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.ObInputAdornments-root[data-size="md"] {
|
|
30
|
+
--obInputAdornments-padding: 0.25rem;
|
|
31
|
+
--obInputAdornments-fontSize: var(--ds-font-size-3, 1rem);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.ObInputAdornments-root[data-size="lg"] {
|
|
35
|
+
--obInputAdornments-padding: 0.375rem;
|
|
36
|
+
--obInputAdornments-fontSize: var(--ds-font-size-4, 1.125rem);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* The Designsystemet input hover (an inner outline doubling the border), drawn on the wrapper */
|
|
40
|
+
.ObInputAdornments-root:hover:not(
|
|
41
|
+
:focus-within,
|
|
42
|
+
:has(.ObInputAdornments-field:disabled, .ObInputAdornments-field[readonly])
|
|
43
|
+
) {
|
|
44
|
+
outline: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-default, #b3b3b3);
|
|
45
|
+
outline-offset: -0.1px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.ObInputAdornments-root:has(.ObInputAdornments-field:focus-visible) {
|
|
49
|
+
box-shadow: var(--dsc-focus-boxShadow);
|
|
50
|
+
outline: var(--dsc-focus-outline);
|
|
51
|
+
outline-offset: var(--ds-border-width-focus, 3px);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.ObInputAdornments-root:has(.ObInputAdornments-field:disabled) {
|
|
55
|
+
cursor: not-allowed;
|
|
56
|
+
opacity: var(--ds-disabled-opacity, 0.3);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* The bare input: all the visual styling lives on the wrapper instead */
|
|
60
|
+
.ObInputAdornments-root .ObInputAdornments-field {
|
|
61
|
+
flex: 1;
|
|
62
|
+
min-inline-size: 0;
|
|
63
|
+
font-size: var(--obInputAdornments-fontSize);
|
|
64
|
+
background: transparent;
|
|
65
|
+
border: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.ObInputAdornments-root .ObInputAdornments-field:disabled {
|
|
69
|
+
opacity: 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.ObInputAdornments-root .ObInputAdornments-field:hover {
|
|
73
|
+
outline: none;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.ObInputAdornments-root .ObInputAdornments-field:focus-visible {
|
|
77
|
+
box-shadow: none;
|
|
78
|
+
outline: none;
|
|
79
|
+
outline-offset: 0;
|
|
80
|
+
}
|
|
@@ -1,102 +1,102 @@
|
|
|
1
|
-
import "./InputAdornments.css";
|
|
2
|
-
|
|
3
|
-
import { Input, type InputProps } from "@digdir/designsystemet-react";
|
|
4
|
-
import clsx from "clsx";
|
|
5
|
-
import { Children, isValidElement, type ReactElement, type ReactNode, type Ref } from "react";
|
|
6
|
-
|
|
7
|
-
export type InputSlotProps = {
|
|
8
|
-
/**
|
|
9
|
-
* Extra classes for the input rendered in this slot
|
|
10
|
-
*/
|
|
11
|
-
className?: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Marks where the input should be rendered among the children of
|
|
16
|
-
* `InputAdornments`. Renders nothing on its own; `InputAdornments` swaps it
|
|
17
|
-
* for the actual input. When it is left out, the input is rendered before the
|
|
18
|
-
* adornments.
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* ```jsx
|
|
22
|
-
* <InputAdornments value={value} onChange={handleChange}>
|
|
23
|
-
* <SearchIcon aria-hidden />
|
|
24
|
-
* <InputSlot />
|
|
25
|
-
* <Button icon variant="tertiary"><XMarkIcon aria-hidden /></Button>
|
|
26
|
-
* </InputAdornments>
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
export function InputSlot(_props: InputSlotProps) {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export type InputAdornmentsProps = Omit<InputProps, "children"> & {
|
|
34
|
-
/**
|
|
35
|
-
* Adornments rendered next to the input, e.g. icons or buttons. Include an
|
|
36
|
-
* `InputSlot` to control where the input itself ends up.
|
|
37
|
-
*/
|
|
38
|
-
children?: ReactNode;
|
|
39
|
-
/**
|
|
40
|
-
* Classes for the wrapper that carries the input styling
|
|
41
|
-
*/
|
|
42
|
-
className?: string;
|
|
43
|
-
/**
|
|
44
|
-
* Classes for the bare input element itself
|
|
45
|
-
*/
|
|
46
|
-
inputClassName?: string;
|
|
47
|
-
/**
|
|
48
|
-
* Ref to the input element. The wrapper is an implementation detail, so the
|
|
49
|
-
* ref points at the input rather than at the element carrying the styling.
|
|
50
|
-
*/
|
|
51
|
-
ref?: Ref<HTMLInputElement>;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Input with adornments, equivalent to a MUI `TextField` with
|
|
56
|
-
* `InputProps.startAdornment` / `endAdornment`. Designsystemet has no
|
|
57
|
-
* equivalent, so the border, background, hover and focus styling is moved from
|
|
58
|
-
* the input onto a wrapper element, making the input and its adornments read
|
|
59
|
-
* as a single control.
|
|
60
|
-
*
|
|
61
|
-
* All other props are forwarded to the Designsystemet `Input`, so `ref`,
|
|
62
|
-
* `value`, `onChange`, `data-size` and `data-color` work as usual.
|
|
63
|
-
*
|
|
64
|
-
* @example
|
|
65
|
-
* ```jsx
|
|
66
|
-
* // Input first (default), adornments after
|
|
67
|
-
* <InputAdornments aria-label="Search" value={value} onChange={handleChange}>
|
|
68
|
-
* <Button icon variant="tertiary"><MagnifyingGlassIcon aria-hidden /></Button>
|
|
69
|
-
* </InputAdornments>
|
|
70
|
-
*
|
|
71
|
-
* // Explicit placement with InputSlot
|
|
72
|
-
* <InputAdornments aria-label="Amount" data-size="lg">
|
|
73
|
-
* <span aria-hidden>kr</span>
|
|
74
|
-
* <InputSlot />
|
|
75
|
-
* <span aria-hidden>,-</span>
|
|
76
|
-
* </InputAdornments>
|
|
77
|
-
* ```
|
|
78
|
-
*/
|
|
79
|
-
export function InputAdornments({ children, className, inputClassName, ...props }: InputAdornmentsProps) {
|
|
80
|
-
let size = props["data-size"];
|
|
81
|
-
let color = props["data-color"];
|
|
82
|
-
let childArray = Children.toArray(children);
|
|
83
|
-
let slot = childArray.find((child) => isValidElement(child) && child.type === InputSlot) as
|
|
84
|
-
| ReactElement<InputSlotProps>
|
|
85
|
-
| undefined;
|
|
86
|
-
|
|
87
|
-
let input = (
|
|
88
|
-
<Input
|
|
89
|
-
key="ObInputAdornments-input"
|
|
90
|
-
{...props}
|
|
91
|
-
className={clsx("ObInputAdornments-field", inputClassName, slot?.props.className)}
|
|
92
|
-
/>
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
let content = slot ? childArray.map((child) => (child === slot ? input : child)) : [input, ...childArray];
|
|
96
|
-
|
|
97
|
-
return (
|
|
98
|
-
<div className={clsx("ObInputAdornments-root", className)} data-size={size} data-color={color}>
|
|
99
|
-
{content}
|
|
100
|
-
</div>
|
|
101
|
-
);
|
|
102
|
-
}
|
|
1
|
+
import "./InputAdornments.css";
|
|
2
|
+
|
|
3
|
+
import { Input, type InputProps } from "@digdir/designsystemet-react";
|
|
4
|
+
import clsx from "clsx";
|
|
5
|
+
import { Children, isValidElement, type ReactElement, type ReactNode, type Ref } from "react";
|
|
6
|
+
|
|
7
|
+
export type InputSlotProps = {
|
|
8
|
+
/**
|
|
9
|
+
* Extra classes for the input rendered in this slot
|
|
10
|
+
*/
|
|
11
|
+
className?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Marks where the input should be rendered among the children of
|
|
16
|
+
* `InputAdornments`. Renders nothing on its own; `InputAdornments` swaps it
|
|
17
|
+
* for the actual input. When it is left out, the input is rendered before the
|
|
18
|
+
* adornments.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```jsx
|
|
22
|
+
* <InputAdornments value={value} onChange={handleChange}>
|
|
23
|
+
* <SearchIcon aria-hidden />
|
|
24
|
+
* <InputSlot />
|
|
25
|
+
* <Button icon variant="tertiary"><XMarkIcon aria-hidden /></Button>
|
|
26
|
+
* </InputAdornments>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function InputSlot(_props: InputSlotProps) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type InputAdornmentsProps = Omit<InputProps, "children"> & {
|
|
34
|
+
/**
|
|
35
|
+
* Adornments rendered next to the input, e.g. icons or buttons. Include an
|
|
36
|
+
* `InputSlot` to control where the input itself ends up.
|
|
37
|
+
*/
|
|
38
|
+
children?: ReactNode;
|
|
39
|
+
/**
|
|
40
|
+
* Classes for the wrapper that carries the input styling
|
|
41
|
+
*/
|
|
42
|
+
className?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Classes for the bare input element itself
|
|
45
|
+
*/
|
|
46
|
+
inputClassName?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Ref to the input element. The wrapper is an implementation detail, so the
|
|
49
|
+
* ref points at the input rather than at the element carrying the styling.
|
|
50
|
+
*/
|
|
51
|
+
ref?: Ref<HTMLInputElement>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Input with adornments, equivalent to a MUI `TextField` with
|
|
56
|
+
* `InputProps.startAdornment` / `endAdornment`. Designsystemet has no
|
|
57
|
+
* equivalent, so the border, background, hover and focus styling is moved from
|
|
58
|
+
* the input onto a wrapper element, making the input and its adornments read
|
|
59
|
+
* as a single control.
|
|
60
|
+
*
|
|
61
|
+
* All other props are forwarded to the Designsystemet `Input`, so `ref`,
|
|
62
|
+
* `value`, `onChange`, `data-size` and `data-color` work as usual.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```jsx
|
|
66
|
+
* // Input first (default), adornments after
|
|
67
|
+
* <InputAdornments aria-label="Search" value={value} onChange={handleChange}>
|
|
68
|
+
* <Button icon variant="tertiary"><MagnifyingGlassIcon aria-hidden /></Button>
|
|
69
|
+
* </InputAdornments>
|
|
70
|
+
*
|
|
71
|
+
* // Explicit placement with InputSlot
|
|
72
|
+
* <InputAdornments aria-label="Amount" data-size="lg">
|
|
73
|
+
* <span aria-hidden>kr</span>
|
|
74
|
+
* <InputSlot />
|
|
75
|
+
* <span aria-hidden>,-</span>
|
|
76
|
+
* </InputAdornments>
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function InputAdornments({ children, className, inputClassName, ...props }: InputAdornmentsProps) {
|
|
80
|
+
let size = props["data-size"];
|
|
81
|
+
let color = props["data-color"];
|
|
82
|
+
let childArray = Children.toArray(children);
|
|
83
|
+
let slot = childArray.find((child) => isValidElement(child) && child.type === InputSlot) as
|
|
84
|
+
| ReactElement<InputSlotProps>
|
|
85
|
+
| undefined;
|
|
86
|
+
|
|
87
|
+
let input = (
|
|
88
|
+
<Input
|
|
89
|
+
key="ObInputAdornments-input"
|
|
90
|
+
{...props}
|
|
91
|
+
className={clsx("ObInputAdornments-field", inputClassName, slot?.props.className)}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
let content = slot ? childArray.map((child) => (child === slot ? input : child)) : [input, ...childArray];
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className={clsx("ObInputAdornments-root", className)} data-size={size} data-color={color}>
|
|
99
|
+
{content}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
.ObNumberInput-root .ObInputAdornments-field {
|
|
2
|
-
/* Numbers read best right aligned, and the steppers sit at the end anyway */
|
|
3
|
-
text-align: end;
|
|
4
|
-
font-variant-numeric: tabular-nums;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
.ObNumberInput-root .ObNumberInput-stepper {
|
|
8
|
-
flex-shrink: 0;
|
|
9
|
-
}
|
|
1
|
+
.ObNumberInput-root .ObInputAdornments-field {
|
|
2
|
+
/* Numbers read best right aligned, and the steppers sit at the end anyway */
|
|
3
|
+
text-align: end;
|
|
4
|
+
font-variant-numeric: tabular-nums;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.ObNumberInput-root .ObNumberInput-stepper {
|
|
8
|
+
flex-shrink: 0;
|
|
9
|
+
}
|