@olenbetong/appframe-ds 0.2.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.
@@ -0,0 +1,264 @@
1
+ import "./Lookup.css";
2
+
3
+ import { Button, Textfield, type TextfieldProps } from "@digdir/designsystemet-react";
4
+ import ClearIcon from "@mui/icons-material/Close";
5
+ import SearchIcon from "@mui/icons-material/Search";
6
+ import { getLocalizedString } from "@olenbetong/appframe-core";
7
+
8
+ import clsx from "clsx";
9
+ import { useCallback, useEffect, useId, useRef, useState } from "react";
10
+ import { createPortal, flushSync } from "react-dom";
11
+
12
+ import { Combobox, type ComboboxProps } from "./Combobox.js";
13
+
14
+ export interface AfLookupProps<T extends Record<string, unknown>> extends Omit<ComboboxProps<T>, "value"> {
15
+ /**
16
+ * Allows the user to enter a value manually that might not be in the list.
17
+ */
18
+ editable?: boolean;
19
+ fullWidth?: boolean;
20
+ label?: string;
21
+ value?: string | null;
22
+ onChange?: (item: T | null) => void;
23
+ slotProps?: {
24
+ dialog?: Partial<React.HTMLProps<HTMLDialogElement>>;
25
+ textField?: Partial<TextfieldProps>;
26
+ };
27
+ }
28
+
29
+ export interface LookupProps<T extends Record<string, unknown>> extends Omit<ComboboxProps<T>, "value"> {
30
+ slotProps?: {
31
+ dialog?: Partial<React.HTMLProps<HTMLDialogElement>>;
32
+ };
33
+ getAnchorElement?: () => Element | null;
34
+ }
35
+
36
+ export function useLookup<T extends Record<string, unknown>>({
37
+ getAnchorElement,
38
+ slotProps,
39
+ ...props
40
+ }: LookupProps<T>) {
41
+ let [open, setOpen] = useState(false);
42
+ let anchorName = `--anchor-${CSS.escape(useId())}`;
43
+ let dialogRef = useRef<HTMLDialogElement>(null);
44
+ let inputId = useId();
45
+ let resetManualAnchor = useCallback(() => {
46
+ let $dialog = dialogRef.current;
47
+ if ($dialog) {
48
+ $dialog.style.removeProperty("--lookup-anchor-inline-start");
49
+ $dialog.style.removeProperty("--lookup-anchor-block-start");
50
+ delete $dialog.dataset.anchor;
51
+ }
52
+ }, []);
53
+ let updateManualAnchor = useCallback(() => {
54
+ if (typeof window === "undefined") {
55
+ return;
56
+ }
57
+ if (!window.matchMedia?.("(pointer: fine)").matches) {
58
+ resetManualAnchor();
59
+ return;
60
+ }
61
+ let anchorElement = getAnchorElement?.() ?? null;
62
+ let $dialog = dialogRef.current;
63
+ if (!$dialog || !anchorElement) {
64
+ resetManualAnchor();
65
+ return;
66
+ }
67
+ let rect = anchorElement.getBoundingClientRect();
68
+ let dialogHeight = $dialog.offsetHeight || 0;
69
+ let dialogWidth = $dialog.offsetWidth || 0;
70
+ let top = rect.bottom + 8;
71
+ let availableBelow = window.innerHeight - rect.bottom - 16;
72
+ if (availableBelow < dialogHeight) {
73
+ top = Math.max(16, rect.top - dialogHeight - 8);
74
+ }
75
+ let left = rect.left;
76
+ let minInline = 16;
77
+ let maxInline = window.innerWidth - dialogWidth - 16;
78
+ if (Number.isFinite(maxInline)) {
79
+ left = Math.min(Math.max(left, minInline), Math.max(minInline, maxInline));
80
+ }
81
+ $dialog.style.setProperty("--lookup-anchor-inline-start", `${Math.round(left)}px`);
82
+ $dialog.style.setProperty("--lookup-anchor-block-start", `${Math.round(top)}px`);
83
+ $dialog.dataset.anchor = "manual";
84
+ }, [getAnchorElement, resetManualAnchor]);
85
+
86
+ function handleOpen() {
87
+ let $dialog = dialogRef.current;
88
+
89
+ if ($dialog) {
90
+ flushSync(() => {
91
+ $dialog.showModal();
92
+ setOpen(true);
93
+ });
94
+
95
+ $dialog.querySelector<HTMLInputElement>('input[type="search"]')?.focus();
96
+ }
97
+ }
98
+
99
+ function handleKeyDown(event: React.KeyboardEvent<HTMLElement>) {
100
+ if (event.key === "ArrowDown" || event.key === "F4") {
101
+ event.preventDefault();
102
+ event.stopPropagation();
103
+ handleOpen();
104
+ }
105
+ }
106
+
107
+ let handleClose = useCallback(() => {
108
+ setOpen(false);
109
+ document.getElementById(inputId)?.focus();
110
+ }, [inputId]);
111
+
112
+ function handleChange(item: T | null) {
113
+ props.onChange?.(item);
114
+ dialogRef.current?.close();
115
+ }
116
+
117
+ useEffect(() => {
118
+ if (!open) {
119
+ resetManualAnchor();
120
+ return;
121
+ }
122
+ let frame = requestAnimationFrame(() => {
123
+ updateManualAnchor();
124
+ });
125
+ updateManualAnchor();
126
+ let cleanupTargets: Array<
127
+ [EventTarget, string, EventListenerOrEventListenerObject, AddEventListenerOptions | boolean | undefined]
128
+ > = [];
129
+ let listener = () => updateManualAnchor();
130
+ cleanupTargets.push([window, "resize", listener, undefined]);
131
+ cleanupTargets.push([window, "scroll", listener, true]);
132
+ let anchorElement = getAnchorElement?.() ?? null;
133
+ let scrollParent = anchorElement?.closest<HTMLElement>("[data-virtualized-scroller]") ?? null;
134
+ if (scrollParent) {
135
+ cleanupTargets.push([scrollParent, "scroll", listener, { passive: true }]);
136
+ }
137
+ for (let [target, type, handler, options] of cleanupTargets) {
138
+ target.addEventListener(type, handler, options);
139
+ }
140
+ return () => {
141
+ cancelAnimationFrame(frame);
142
+ for (let [target, type, handler, options] of cleanupTargets) {
143
+ target.removeEventListener(type, handler, options);
144
+ }
145
+ };
146
+ }, [getAnchorElement, open, resetManualAnchor, updateManualAnchor]);
147
+
148
+ useEffect(() => {
149
+ let $dialog = dialogRef.current;
150
+ if ($dialog) {
151
+ let controller = new AbortController();
152
+ let signal = controller.signal;
153
+ let closeEvent = () => {
154
+ // Wait for transition to finish before closing
155
+ setTimeout(() => {
156
+ handleClose();
157
+ }, 150);
158
+ };
159
+
160
+ $dialog.addEventListener("close", closeEvent, { signal });
161
+ $dialog.addEventListener("cancel", closeEvent, { signal });
162
+ $dialog.addEventListener(
163
+ "click",
164
+ (event) => {
165
+ let rect = $dialog.getBoundingClientRect();
166
+ if (
167
+ rect.left > event.clientX ||
168
+ rect.right < event.clientX ||
169
+ rect.top > event.clientY ||
170
+ rect.bottom < event.clientY
171
+ ) {
172
+ $dialog.close();
173
+ }
174
+ },
175
+ { signal },
176
+ );
177
+
178
+ return () => {
179
+ controller.abort();
180
+ };
181
+ }
182
+ }, [handleClose]);
183
+
184
+ return {
185
+ open,
186
+ openDialog: handleOpen,
187
+ onKeyDown: handleKeyDown,
188
+ onChange: handleChange,
189
+ dialogRef,
190
+ anchorName,
191
+ inputId,
192
+ dialog: createPortal(
193
+ <dialog
194
+ ref={dialogRef}
195
+ className={clsx("ObLookup-dialog", slotProps?.dialog?.className)}
196
+ onKeyDown={(evt) => evt.stopPropagation()}
197
+ style={{ positionAnchor: anchorName, ...slotProps?.dialog?.style } as React.CSSProperties}
198
+ >
199
+ {open && <Combobox {...props} onClose={() => dialogRef.current?.close()} onChange={handleChange} />}
200
+ </dialog>,
201
+ document.body,
202
+ inputId,
203
+ ),
204
+ };
205
+ }
206
+
207
+ export function AfLookup<T extends Record<string, unknown>>({
208
+ fullWidth = false,
209
+ label,
210
+ slotProps,
211
+ value,
212
+ ...props
213
+ }: AfLookupProps<T>) {
214
+ let { inputId, open: _open, dialog, anchorName, openDialog, onKeyDown } = useLookup<T>(props);
215
+
216
+ return (
217
+ <div style={{ anchorName } as React.CSSProperties}>
218
+ <div style={{ position: "relative", display: fullWidth ? "block" : "inline-block" }}>
219
+ <Textfield
220
+ id={inputId}
221
+ value={value ?? ""}
222
+ label={label}
223
+ readOnly
224
+ onClick={openDialog}
225
+ onKeyDown={onKeyDown}
226
+ className={clsx(slotProps?.textField?.className, "ObLookup-input")}
227
+ {...(slotProps?.textField as any)}
228
+ style={{ width: fullWidth ? "100%" : undefined, ...slotProps?.textField?.style }}
229
+ />
230
+ <div
231
+ style={{
232
+ position: "absolute",
233
+ right: 0,
234
+ top: "50%",
235
+ transform: "translateY(-50%)",
236
+ display: "flex",
237
+ alignItems: "center",
238
+ }}
239
+ >
240
+ {props.nullable && value && props.onChange && (
241
+ <Button
242
+ icon
243
+ variant="tertiary"
244
+ data-size="sm"
245
+ aria-label={getLocalizedString("Clear")}
246
+ onKeyDown={(evt) => evt.stopPropagation()}
247
+ onClick={(evt) => {
248
+ evt.stopPropagation();
249
+ props.onChange?.(null);
250
+ document.getElementById(inputId)?.focus();
251
+ }}
252
+ >
253
+ <ClearIcon />
254
+ </Button>
255
+ )}
256
+ <Button icon variant="tertiary" data-size="sm" aria-label={getLocalizedString("Search")} onClick={openDialog}>
257
+ <SearchIcon />
258
+ </Button>
259
+ </div>
260
+ </div>
261
+ {dialog}
262
+ </div>
263
+ );
264
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./BoundLookup.js";
2
+ export * from "./Combobox.js";
3
+ export * from "./Lookup.js";
@@ -0,0 +1,31 @@
1
+ import { Checkbox, type CheckboxProps } from "@digdir/designsystemet-react";
2
+ import { useField } from "@olenbetong/appframe-react";
3
+ import { forwardRef } from "react";
4
+
5
+ export type BoundCheckboxProps = Omit<CheckboxProps, "onChange" | "onKeyDown"> & {
6
+ field: string;
7
+ onChange?: (evt: React.ChangeEvent<HTMLInputElement>) => void;
8
+ };
9
+
10
+ export const BoundCheckbox = forwardRef<HTMLInputElement, BoundCheckboxProps>(function BoundCheckbox(
11
+ { field, onChange: onChangeProp, checked: _checked, ...props },
12
+ ref,
13
+ ) {
14
+ let { value, setValue, onKeyDown } = useField<any, any>(field);
15
+
16
+ return (
17
+ <Checkbox
18
+ checked={value ?? false}
19
+ onChange={(evt) => {
20
+ onChangeProp?.(evt);
21
+ if (!evt.defaultPrevented) {
22
+ setValue(evt.target.checked);
23
+ }
24
+ }}
25
+ onKeyDown={(evt) => onKeyDown(evt as any)}
26
+ ref={ref}
27
+ name={field}
28
+ {...(props as any)}
29
+ />
30
+ );
31
+ });
@@ -0,0 +1,32 @@
1
+ import { Textfield, type TextfieldProps } from "@digdir/designsystemet-react";
2
+ import { useField } from "@olenbetong/appframe-react";
3
+ import { forwardRef } from "react";
4
+
5
+ export type BoundDatePickerProps = Omit<TextfieldProps, "value" | "onChange" | "onKeyDown" | "type"> & {
6
+ field: string;
7
+ };
8
+
9
+ export const BoundDatePicker = forwardRef<HTMLInputElement, BoundDatePickerProps>(function BoundDatePicker(
10
+ { field, ...props },
11
+ ref,
12
+ ) {
13
+ let { error, value, onChange, onKeyDown } = useField<any, any>(field);
14
+
15
+ let inputValue = value;
16
+ if (value instanceof Date) {
17
+ inputValue = value.toISOString().split("T")[0];
18
+ }
19
+
20
+ return (
21
+ <Textfield
22
+ type="date"
23
+ name={field}
24
+ error={error ?? undefined}
25
+ {...(props as any)}
26
+ value={inputValue ?? ""}
27
+ onChange={(evt) => onChange(evt as any)}
28
+ onKeyDown={(evt) => onKeyDown(evt as any)}
29
+ ref={ref}
30
+ />
31
+ );
32
+ });
@@ -0,0 +1,37 @@
1
+ import { Textfield, type TextfieldProps } from "@digdir/designsystemet-react";
2
+ import { useField } from "@olenbetong/appframe-react";
3
+ import { forwardRef } from "react";
4
+
5
+ export type BoundDateTimePickerProps = Omit<TextfieldProps, "value" | "onChange" | "onKeyDown" | "type"> & {
6
+ field: string;
7
+ };
8
+
9
+ export const BoundDateTimePicker = forwardRef<HTMLInputElement, BoundDateTimePickerProps>(function BoundDateTimePicker(
10
+ { field, ...props },
11
+ ref,
12
+ ) {
13
+ let { error, value, onChange, onKeyDown } = useField<any, any>(field);
14
+
15
+ let inputValue = value;
16
+ if (value instanceof Date) {
17
+ inputValue = value.toISOString().replace("Z", "");
18
+ }
19
+
20
+ return (
21
+ <Textfield
22
+ type="datetime-local"
23
+ name={field}
24
+ error={error ?? undefined}
25
+ {...(props as any)}
26
+ value={inputValue ?? ""}
27
+ onChange={(evt) => {
28
+ onChange(
29
+ evt as any,
30
+ (evt.target as HTMLInputElement).value ? `${(evt.target as HTMLInputElement).value}Z` : null,
31
+ );
32
+ }}
33
+ onKeyDown={(evt) => onKeyDown(evt as any)}
34
+ ref={ref}
35
+ />
36
+ );
37
+ });
@@ -0,0 +1,26 @@
1
+ import { Input, type InputProps } from "@digdir/designsystemet-react";
2
+ import { useField } from "@olenbetong/appframe-react";
3
+
4
+ export type BoundInputProps = Omit<InputProps, "onChange" | "onKeyDown" | "onBlur"> & {
5
+ field: string;
6
+ onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
7
+ };
8
+
9
+ export function BoundInput({ field, onChange: onChangeProp, ...props }: BoundInputProps) {
10
+ let { value, onChange, onKeyDown } = useField<any, any>(field);
11
+
12
+ return (
13
+ <Input
14
+ name={field}
15
+ {...props}
16
+ value={value ?? ""}
17
+ onChange={(event) => {
18
+ onChangeProp?.(event as any);
19
+ if (!event.defaultPrevented) {
20
+ onChange(event as any);
21
+ }
22
+ }}
23
+ onKeyDown={(evt) => onKeyDown(evt as any)}
24
+ />
25
+ );
26
+ }
@@ -0,0 +1,102 @@
1
+ import { Textfield, type TextfieldProps } from "@digdir/designsystemet-react";
2
+ import { getLocalizedString, localize } from "@olenbetong/appframe-core";
3
+ import { useField } from "@olenbetong/appframe-react";
4
+ import { useEffect, useState } from "react";
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
+ }
@@ -0,0 +1,31 @@
1
+ import { Select, type SelectProps } from "@digdir/designsystemet-react";
2
+ import { useField } from "@olenbetong/appframe-react";
3
+ import { forwardRef } from "react";
4
+
5
+ export type BoundSelectProps = Omit<SelectProps, "onChange" | "value"> & {
6
+ field: string;
7
+ onChange?: (event: React.ChangeEvent<HTMLSelectElement>) => void;
8
+ };
9
+
10
+ export const BoundSelect = forwardRef<HTMLSelectElement, BoundSelectProps>(function BoundSelect(
11
+ { field, onChange: onChangeProp, ...props },
12
+ ref,
13
+ ) {
14
+ let { value, onChange, onKeyDown } = useField<any, any>(field);
15
+
16
+ return (
17
+ <Select
18
+ name={field}
19
+ value={value ?? ""}
20
+ onChange={(evt) => {
21
+ onChangeProp?.(evt);
22
+ if (!evt.defaultPrevented) {
23
+ onChange(evt as any);
24
+ }
25
+ }}
26
+ onKeyDown={(evt) => onKeyDown(evt as any)}
27
+ {...props}
28
+ ref={ref}
29
+ />
30
+ );
31
+ });
@@ -0,0 +1,30 @@
1
+ import { Switch, type SwitchProps } from "@digdir/designsystemet-react";
2
+ import { useField } from "@olenbetong/appframe-react";
3
+ import { forwardRef } from "react";
4
+
5
+ export type BoundSwitchProps = Omit<SwitchProps, "onChange" | "checked"> & {
6
+ field: string;
7
+ onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
8
+ };
9
+
10
+ export const BoundSwitch = forwardRef<HTMLInputElement, BoundSwitchProps>(function BoundSwitch(
11
+ { field, onChange: onChangeProp, ...props },
12
+ ref,
13
+ ) {
14
+ let { value, setValue } = useField<any, any>(field);
15
+
16
+ return (
17
+ <Switch
18
+ name={field}
19
+ {...(props as any)}
20
+ checked={value ?? false}
21
+ onChange={(evt) => {
22
+ onChangeProp?.(evt);
23
+ if (!evt.defaultPrevented) {
24
+ setValue(evt.target.checked);
25
+ }
26
+ }}
27
+ ref={ref}
28
+ />
29
+ );
30
+ });
@@ -0,0 +1,41 @@
1
+ import { Textfield, type TextfieldProps } from "@digdir/designsystemet-react";
2
+ import { useField } from "@olenbetong/appframe-react";
3
+ import { forwardRef } from "react";
4
+
5
+ export type BoundTextFieldProps = Omit<TextfieldProps, "value" | "onChange" | "onKeyDown"> & {
6
+ field: string;
7
+ };
8
+
9
+ export const BoundTextField = forwardRef<HTMLInputElement & HTMLTextAreaElement, BoundTextFieldProps>(
10
+ function BoundTextField({ field, ...props }, ref) {
11
+ let { error, value, onChange, onKeyDown } = useField<any, any>(field);
12
+
13
+ let inputValue = value;
14
+ if (props.type === "date" && value instanceof Date) {
15
+ inputValue = value.toISOString().split("T")[0];
16
+ } else if (["datetime", "datetime-local"].includes(props.type ?? "") && value instanceof Date) {
17
+ inputValue = value.toISOString().replace("Z", "");
18
+ }
19
+
20
+ return (
21
+ <Textfield
22
+ name={field}
23
+ error={error ?? undefined}
24
+ {...(props as any)}
25
+ value={inputValue ?? ""}
26
+ onChange={(evt) => {
27
+ if (["datetime", "datetime-local"].includes(props.type ?? "")) {
28
+ onChange(
29
+ evt as any,
30
+ (evt.target as HTMLInputElement).value ? `${(evt.target as HTMLInputElement).value}Z` : null,
31
+ );
32
+ } else {
33
+ onChange(evt as any);
34
+ }
35
+ }}
36
+ onKeyDown={(evt) => onKeyDown(evt as any)}
37
+ ref={ref}
38
+ />
39
+ );
40
+ },
41
+ );
@@ -0,0 +1,27 @@
1
+ import { Button, type ButtonProps } from "@digdir/designsystemet-react";
2
+ import { getLocalizedString } from "@olenbetong/appframe-core";
3
+ import { useCancelButton } from "@olenbetong/appframe-react";
4
+ import { type ForwardRefExoticComponent, forwardRef, type RefAttributes } from "react";
5
+
6
+ export const CancelButton: ForwardRefExoticComponent<ButtonProps & RefAttributes<HTMLButtonElement>> = forwardRef<
7
+ HTMLButtonElement,
8
+ ButtonProps
9
+ >(function CancelButton({ onClick, children, ...props }, ref) {
10
+ let { cancelEdit } = useCancelButton();
11
+
12
+ return (
13
+ <Button
14
+ onClick={(evt) => {
15
+ onClick?.(evt);
16
+ if (!evt.defaultPrevented) {
17
+ cancelEdit();
18
+ }
19
+ }}
20
+ variant="secondary"
21
+ {...props}
22
+ ref={ref}
23
+ >
24
+ {children ?? getLocalizedString("Cancel")}
25
+ </Button>
26
+ );
27
+ });
@@ -0,0 +1,30 @@
1
+ import { Button, type ButtonProps } from "@digdir/designsystemet-react";
2
+ import UndoIcon from "@mui/icons-material/Undo";
3
+ import { getLocalizedString } from "@olenbetong/appframe-core";
4
+ import { useCancelButton } from "@olenbetong/appframe-react";
5
+ import { forwardRef } from "react";
6
+
7
+ export const CancelIconButton = forwardRef<HTMLButtonElement, Omit<ButtonProps, "icon">>(function CancelIconButton(
8
+ { onClick, children, ...props },
9
+ ref,
10
+ ) {
11
+ let { cancelEdit } = useCancelButton();
12
+
13
+ return (
14
+ <Button
15
+ icon
16
+ aria-label={getLocalizedString("Revert unsaved changes")}
17
+ variant="tertiary"
18
+ onClick={(evt) => {
19
+ onClick?.(evt);
20
+ if (!evt.defaultPrevented) {
21
+ cancelEdit();
22
+ }
23
+ }}
24
+ {...props}
25
+ ref={ref}
26
+ >
27
+ {children ?? <UndoIcon />}
28
+ </Button>
29
+ );
30
+ });