@olenbetong/appframe-ds 0.5.0 → 0.6.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 +12 -0
- package/package.json +4 -4
- package/scripts/copyAssets.mjs +4 -0
- package/src/AfLookup/Lookup.tsx +3 -4
- package/src/AfLookup/LookupEdit.tsx +3 -4
- package/src/binding/CancelButton.tsx +4 -1
- package/src/binding/CancelIconButton.tsx +2 -2
- package/src/binding/DataEditToolbar.tsx +1 -2
- package/src/binding/DeleteButton.tsx +6 -2
- package/src/binding/DeleteIconButton.tsx +5 -4
- package/src/binding/RefreshButton.tsx +7 -3
- package/src/binding/RefreshIconButton.tsx +6 -5
- package/src/binding/RefreshRowIconButton.tsx +6 -5
- package/src/binding/SaveButton.tsx +14 -4
- package/src/binding/SaveIconButton.tsx +13 -6
- package/src/index.ts +2 -0
- package/src/input/InputAdornments.css +75 -0
- package/src/input/InputAdornments.tsx +97 -0
- package/src/input/index.ts +1 -0
- package/src/progress/LinearProgress.css +93 -0
- package/src/progress/LinearProgress.tsx +104 -0
- package/src/progress/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @olenbetong/appframe-ds
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0147e80: Add `LinearProgress` component: a designsystemet-styled linear progress bar equivalent to MUI `LinearProgress`, with indeterminate and determinate modes and an `overlay` prop for an absolutely positioned bar that does not affect the layout.
|
|
8
|
+
- 56080ed: Add `InputAdornments` and `InputSlot`: an input with adornments (MUI `TextField` start/end adornment equivalent) where the border, background, hover and focus styling lives on a wrapper around the input and its adornments. Adornments are passed as children, and `InputSlot` controls where the input is placed (defaults to the start).
|
|
9
|
+
- bc89c1e: Migrated all icons in appframe-ds from `@mui/icons-material` to `@navikt/aksel-icons`. Bound action buttons (`SaveButton`, `DeleteButton`, `CancelButton`, `RefreshButton` and their icon-only variants) now show a leading icon that is hidden while loading, so button width no longer changes when toggling the loading state. `SaveButton` and `SaveIconButton` also display a small warning badge on the save icon when the current record has unsaved changes, matching the existing appframe-mui behavior.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 0147e80: Make the indeterminate `LinearProgress` animation more visible by using a single travelling bar that stretches and shrinks, instead of the two subtle MUI-style bars.
|
|
14
|
+
|
|
3
15
|
## 0.5.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olenbetong/appframe-ds",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Components that bind Designsystemet components to Appframe data objects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./es/index.d.ts",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"peerDependencies": {
|
|
18
18
|
"@digdir/designsystemet-react": ">=1.18.0",
|
|
19
|
-
"@
|
|
19
|
+
"@navikt/aksel-icons": ">=8.16.1",
|
|
20
20
|
"react": ">=19.2.8",
|
|
21
21
|
"react-dom": ">=19.2.8",
|
|
22
22
|
"react-error-boundary": ">=6.1.2",
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@olenbetong/appframe-core": "2.11.10",
|
|
27
27
|
"@olenbetong/appframe-data": "1.5.0",
|
|
28
|
-
"@olenbetong/appframe-react": "
|
|
28
|
+
"@olenbetong/appframe-react": "2.0.0",
|
|
29
29
|
"react-window": "^1.8.11",
|
|
30
30
|
"react-virtualized-auto-sizer": "^1.0.26",
|
|
31
31
|
"clsx": "^2.1.1"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@digdir/designsystemet-react": "1.18.0",
|
|
35
|
-
"@
|
|
35
|
+
"@navikt/aksel-icons": "8.16.1",
|
|
36
36
|
"@types/react": "19.2.17",
|
|
37
37
|
"@types/react-dom": "19.2.3",
|
|
38
38
|
"@types/react-window": "^1.8.8",
|
package/scripts/copyAssets.mjs
CHANGED
|
@@ -16,13 +16,17 @@ const files = [
|
|
|
16
16
|
["src/page/PageBlock.css", "es/page/PageBlock.css"],
|
|
17
17
|
["src/page/PageHeader.css", "es/page/PageHeader.css"],
|
|
18
18
|
["src/list/ItemList.css", "es/list/ItemList.css"],
|
|
19
|
+
["src/input/InputAdornments.css", "es/input/InputAdornments.css"],
|
|
19
20
|
["src/paper/Paper.css", "es/paper/Paper.css"],
|
|
21
|
+
["src/progress/LinearProgress.css", "es/progress/LinearProgress.css"],
|
|
20
22
|
];
|
|
21
23
|
|
|
22
24
|
mkdirSync("es/layout", { recursive: true });
|
|
25
|
+
mkdirSync("es/input", { recursive: true });
|
|
23
26
|
mkdirSync("es/list", { recursive: true });
|
|
24
27
|
mkdirSync("es/page", { recursive: true });
|
|
25
28
|
mkdirSync("es/paper", { recursive: true });
|
|
29
|
+
mkdirSync("es/progress", { recursive: true });
|
|
26
30
|
|
|
27
31
|
for (let [src, dest] of files) {
|
|
28
32
|
cpSync(src, dest);
|
package/src/AfLookup/Lookup.tsx
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import "./Lookup.css";
|
|
2
2
|
|
|
3
3
|
import { Button, Textfield, type TextfieldProps } from "@digdir/designsystemet-react";
|
|
4
|
-
import
|
|
5
|
-
import SearchIcon from "@mui/icons-material/Search";
|
|
4
|
+
import { MagnifyingGlassIcon, XMarkIcon } from "@navikt/aksel-icons";
|
|
6
5
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
7
6
|
import { type DistributiveOmit, useLookupState } from "@olenbetong/appframe-react";
|
|
8
7
|
|
|
@@ -143,7 +142,7 @@ export function AfLookup<T extends Record<string, unknown>>({
|
|
|
143
142
|
document.getElementById(inputId)?.focus();
|
|
144
143
|
}}
|
|
145
144
|
>
|
|
146
|
-
<
|
|
145
|
+
<XMarkIcon />
|
|
147
146
|
</Button>
|
|
148
147
|
)}
|
|
149
148
|
<Button
|
|
@@ -153,7 +152,7 @@ export function AfLookup<T extends Record<string, unknown>>({
|
|
|
153
152
|
aria-label={getLocalizedString("Search")}
|
|
154
153
|
onClick={() => openDialog()}
|
|
155
154
|
>
|
|
156
|
-
<
|
|
155
|
+
<MagnifyingGlassIcon />
|
|
157
156
|
</Button>
|
|
158
157
|
</div>
|
|
159
158
|
</div>
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import "./Lookup.css";
|
|
2
2
|
|
|
3
3
|
import { Button, Textfield, type TextfieldProps } from "@digdir/designsystemet-react";
|
|
4
|
-
import
|
|
5
|
-
import SearchIcon from "@mui/icons-material/Search";
|
|
4
|
+
import { MagnifyingGlassIcon, XMarkIcon } from "@navikt/aksel-icons";
|
|
6
5
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
7
6
|
import type { DistributiveOmit } from "@olenbetong/appframe-react";
|
|
8
7
|
|
|
@@ -108,7 +107,7 @@ export function AfLookupEdit<T extends Record<string, unknown>>({
|
|
|
108
107
|
document.getElementById(inputId)?.focus();
|
|
109
108
|
}}
|
|
110
109
|
>
|
|
111
|
-
<
|
|
110
|
+
<XMarkIcon />
|
|
112
111
|
</Button>
|
|
113
112
|
)}
|
|
114
113
|
<Button
|
|
@@ -121,7 +120,7 @@ export function AfLookupEdit<T extends Record<string, unknown>>({
|
|
|
121
120
|
openDialog();
|
|
122
121
|
}}
|
|
123
122
|
>
|
|
124
|
-
<
|
|
123
|
+
<MagnifyingGlassIcon />
|
|
125
124
|
</Button>
|
|
126
125
|
</div>
|
|
127
126
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Button, type ButtonProps } from "@digdir/designsystemet-react";
|
|
2
|
+
import { ArrowUndoIcon } from "@navikt/aksel-icons";
|
|
2
3
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
3
4
|
import { useCancelButton } from "@olenbetong/appframe-react";
|
|
4
5
|
import { type ForwardRefExoticComponent, forwardRef, type RefAttributes } from "react";
|
|
@@ -6,11 +7,12 @@ import { type ForwardRefExoticComponent, forwardRef, type RefAttributes } from "
|
|
|
6
7
|
export const CancelButton: ForwardRefExoticComponent<ButtonProps & RefAttributes<HTMLButtonElement>> = forwardRef<
|
|
7
8
|
HTMLButtonElement,
|
|
8
9
|
ButtonProps
|
|
9
|
-
>(function CancelButton({ onClick, children, ...props }, ref) {
|
|
10
|
+
>(function CancelButton({ onClick, children, loading, ...props }, ref) {
|
|
10
11
|
let { cancelEdit } = useCancelButton();
|
|
11
12
|
|
|
12
13
|
return (
|
|
13
14
|
<Button
|
|
15
|
+
loading={loading}
|
|
14
16
|
onClick={(evt) => {
|
|
15
17
|
onClick?.(evt);
|
|
16
18
|
if (!evt.defaultPrevented) {
|
|
@@ -21,6 +23,7 @@ export const CancelButton: ForwardRefExoticComponent<ButtonProps & RefAttributes
|
|
|
21
23
|
{...props}
|
|
22
24
|
ref={ref}
|
|
23
25
|
>
|
|
26
|
+
{!loading && <ArrowUndoIcon aria-hidden="true" />}
|
|
24
27
|
{children ?? getLocalizedString("Cancel")}
|
|
25
28
|
</Button>
|
|
26
29
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Button, type ButtonProps } from "@digdir/designsystemet-react";
|
|
2
|
-
import
|
|
2
|
+
import { ArrowUndoIcon } from "@navikt/aksel-icons";
|
|
3
3
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
4
4
|
import { useCancelButton } from "@olenbetong/appframe-react";
|
|
5
5
|
import { forwardRef } from "react";
|
|
@@ -24,7 +24,7 @@ export const CancelIconButton = forwardRef<HTMLButtonElement, Omit<ButtonProps,
|
|
|
24
24
|
{...props}
|
|
25
25
|
ref={ref}
|
|
26
26
|
>
|
|
27
|
-
{children ?? <
|
|
27
|
+
{children ?? <ArrowUndoIcon />}
|
|
28
28
|
</Button>
|
|
29
29
|
);
|
|
30
30
|
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Button, Tooltip } from "@digdir/designsystemet-react";
|
|
2
|
-
import ChevronLeftIcon from "@
|
|
3
|
-
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
|
2
|
+
import { ChevronLeftIcon, ChevronRightIcon } from "@navikt/aksel-icons";
|
|
4
3
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
5
4
|
import type { DataObject } from "@olenbetong/appframe-data";
|
|
6
5
|
import { useCurrentIndex, useDataLength, useDataObject } from "@olenbetong/appframe-react";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Button, type ButtonProps } from "@digdir/designsystemet-react";
|
|
2
|
+
import { TrashIcon } from "@navikt/aksel-icons";
|
|
2
3
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
3
4
|
import { type DeletePrompt, useDeleteButton } from "@olenbetong/appframe-react";
|
|
4
5
|
import { type ForwardRefExoticComponent, forwardRef, type RefAttributes } from "react";
|
|
@@ -8,12 +9,13 @@ export type DeleteButtonProps = ButtonProps & { index?: number; prompt?: DeleteP
|
|
|
8
9
|
export const DeleteButton: ForwardRefExoticComponent<DeleteButtonProps & RefAttributes<HTMLButtonElement>> = forwardRef<
|
|
9
10
|
HTMLButtonElement,
|
|
10
11
|
DeleteButtonProps
|
|
11
|
-
>(function DeleteButton({ prompt, onClick, index, children, ...props }, ref) {
|
|
12
|
+
>(function DeleteButton({ prompt, onClick, index, children, loading, ...props }, ref) {
|
|
12
13
|
let { deleting, deleteRow } = useDeleteButton(prompt);
|
|
14
|
+
let isLoading = deleting || loading;
|
|
13
15
|
|
|
14
16
|
return (
|
|
15
17
|
<Button
|
|
16
|
-
loading={
|
|
18
|
+
loading={isLoading}
|
|
17
19
|
data-color="danger"
|
|
18
20
|
variant="secondary"
|
|
19
21
|
onClick={(evt) => {
|
|
@@ -25,6 +27,8 @@ export const DeleteButton: ForwardRefExoticComponent<DeleteButtonProps & RefAttr
|
|
|
25
27
|
{...props}
|
|
26
28
|
ref={ref}
|
|
27
29
|
>
|
|
30
|
+
{/* Icon is hidden while loading so the spinner takes its place instead of adding width */}
|
|
31
|
+
{!isLoading && <TrashIcon aria-hidden="true" />}
|
|
28
32
|
{children ?? getLocalizedString("Delete")}
|
|
29
33
|
</Button>
|
|
30
34
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Button, type ButtonProps } from "@digdir/designsystemet-react";
|
|
2
|
-
import
|
|
2
|
+
import { TrashIcon } from "@navikt/aksel-icons";
|
|
3
3
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
4
4
|
import { type DeletePrompt, useDeleteButton } from "@olenbetong/appframe-react";
|
|
5
5
|
import { forwardRef } from "react";
|
|
@@ -7,15 +7,16 @@ import { forwardRef } from "react";
|
|
|
7
7
|
export type DeleteIconButtonProps = Omit<ButtonProps, "icon"> & { index?: number; prompt?: DeletePrompt };
|
|
8
8
|
|
|
9
9
|
export const DeleteIconButton = forwardRef<HTMLButtonElement, DeleteIconButtonProps>(function DeleteIconButton(
|
|
10
|
-
{ prompt, onClick, index, children, ...props },
|
|
10
|
+
{ prompt, onClick, index, children, loading, ...props },
|
|
11
11
|
ref,
|
|
12
12
|
) {
|
|
13
13
|
let { isDeleting, deleteRow } = useDeleteButton(prompt);
|
|
14
|
+
let isLoading = isDeleting || loading;
|
|
14
15
|
|
|
15
16
|
return (
|
|
16
17
|
<Button
|
|
17
18
|
icon
|
|
18
|
-
loading={
|
|
19
|
+
loading={isLoading}
|
|
19
20
|
data-color="danger"
|
|
20
21
|
aria-label={getLocalizedString("Delete")}
|
|
21
22
|
variant="tertiary"
|
|
@@ -28,7 +29,7 @@ export const DeleteIconButton = forwardRef<HTMLButtonElement, DeleteIconButtonPr
|
|
|
28
29
|
{...props}
|
|
29
30
|
ref={ref}
|
|
30
31
|
>
|
|
31
|
-
{children ?? <
|
|
32
|
+
{children ?? <TrashIcon />}
|
|
32
33
|
</Button>
|
|
33
34
|
);
|
|
34
35
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Button, type ButtonProps } from "@digdir/designsystemet-react";
|
|
2
|
+
import { ArrowsCirclepathIcon } from "@navikt/aksel-icons";
|
|
2
3
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
3
4
|
import { useRefreshButton } from "@olenbetong/appframe-react";
|
|
4
5
|
import { type ForwardRefExoticComponent, forwardRef, type RefAttributes } from "react";
|
|
@@ -6,12 +7,13 @@ import { type ForwardRefExoticComponent, forwardRef, type RefAttributes } from "
|
|
|
6
7
|
export const RefreshButton: ForwardRefExoticComponent<ButtonProps & RefAttributes<HTMLButtonElement>> = forwardRef<
|
|
7
8
|
HTMLButtonElement,
|
|
8
9
|
ButtonProps
|
|
9
|
-
>(function RefreshButton({ onClick, children, ...props }, ref) {
|
|
10
|
-
let { loading, refresh } = useRefreshButton();
|
|
10
|
+
>(function RefreshButton({ onClick, children, loading, ...props }, ref) {
|
|
11
|
+
let { loading: refreshing, refresh } = useRefreshButton();
|
|
12
|
+
let isLoading = refreshing || loading;
|
|
11
13
|
|
|
12
14
|
return (
|
|
13
15
|
<Button
|
|
14
|
-
loading={
|
|
16
|
+
loading={isLoading}
|
|
15
17
|
onClick={(evt) => {
|
|
16
18
|
onClick?.(evt);
|
|
17
19
|
if (!evt.defaultPrevented) {
|
|
@@ -21,6 +23,8 @@ export const RefreshButton: ForwardRefExoticComponent<ButtonProps & RefAttribute
|
|
|
21
23
|
{...props}
|
|
22
24
|
ref={ref}
|
|
23
25
|
>
|
|
26
|
+
{/* Icon is hidden while loading so the spinner takes its place instead of adding width */}
|
|
27
|
+
{!isLoading && <ArrowsCirclepathIcon aria-hidden="true" />}
|
|
24
28
|
{children ?? getLocalizedString("Refresh")}
|
|
25
29
|
</Button>
|
|
26
30
|
);
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { Button, type ButtonProps } from "@digdir/designsystemet-react";
|
|
2
|
-
import
|
|
2
|
+
import { ArrowsCirclepathIcon } from "@navikt/aksel-icons";
|
|
3
3
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
4
4
|
import { useDataObject, useLoading } from "@olenbetong/appframe-react";
|
|
5
5
|
import { forwardRef } from "react";
|
|
6
6
|
|
|
7
7
|
export const RefreshIconButton = forwardRef<HTMLButtonElement, Omit<ButtonProps, "icon">>(function RefreshIconButton(
|
|
8
|
-
{ onClick, children, ...props },
|
|
8
|
+
{ onClick, children, loading, ...props },
|
|
9
9
|
ref,
|
|
10
10
|
) {
|
|
11
11
|
let dataObject = useDataObject();
|
|
12
|
-
let
|
|
12
|
+
let refreshing = useLoading(dataObject);
|
|
13
|
+
let isLoading = refreshing || loading;
|
|
13
14
|
|
|
14
15
|
return (
|
|
15
16
|
<Button
|
|
16
17
|
icon
|
|
17
|
-
loading={
|
|
18
|
+
loading={isLoading}
|
|
18
19
|
aria-label={getLocalizedString("Refresh data")}
|
|
19
20
|
variant="tertiary"
|
|
20
21
|
onClick={(evt) => {
|
|
@@ -26,7 +27,7 @@ export const RefreshIconButton = forwardRef<HTMLButtonElement, Omit<ButtonProps,
|
|
|
26
27
|
{...props}
|
|
27
28
|
ref={ref}
|
|
28
29
|
>
|
|
29
|
-
{children ?? <
|
|
30
|
+
{children ?? <ArrowsCirclepathIcon />}
|
|
30
31
|
</Button>
|
|
31
32
|
);
|
|
32
33
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Button, type ButtonProps } from "@digdir/designsystemet-react";
|
|
2
|
-
import
|
|
2
|
+
import { ArrowsCirclepathIcon } from "@navikt/aksel-icons";
|
|
3
3
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
4
4
|
import { useRefreshRowButton } from "@olenbetong/appframe-react";
|
|
5
5
|
import { forwardRef } from "react";
|
|
@@ -7,13 +7,14 @@ import { forwardRef } from "react";
|
|
|
7
7
|
export type RefreshRowIconButtonProps = Omit<ButtonProps, "icon"> & { index?: number };
|
|
8
8
|
|
|
9
9
|
export const RefreshRowIconButton = forwardRef<HTMLButtonElement, RefreshRowIconButtonProps>(
|
|
10
|
-
function RefreshRowIconButton({ onClick, index, children, ...props }, ref) {
|
|
11
|
-
let { loading, refreshRow } = useRefreshRowButton();
|
|
10
|
+
function RefreshRowIconButton({ onClick, index, children, loading, ...props }, ref) {
|
|
11
|
+
let { loading: refreshing, refreshRow } = useRefreshRowButton();
|
|
12
|
+
let isLoading = refreshing || loading;
|
|
12
13
|
|
|
13
14
|
return (
|
|
14
15
|
<Button
|
|
15
16
|
icon
|
|
16
|
-
loading={
|
|
17
|
+
loading={isLoading}
|
|
17
18
|
aria-label={getLocalizedString("Refresh current row")}
|
|
18
19
|
variant="tertiary"
|
|
19
20
|
onClick={(evt) => {
|
|
@@ -25,7 +26,7 @@ export const RefreshRowIconButton = forwardRef<HTMLButtonElement, RefreshRowIcon
|
|
|
25
26
|
{...props}
|
|
26
27
|
ref={ref}
|
|
27
28
|
>
|
|
28
|
-
{children ?? <
|
|
29
|
+
{children ?? <ArrowsCirclepathIcon />}
|
|
29
30
|
</Button>
|
|
30
31
|
);
|
|
31
32
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Button, type ButtonProps } from "@digdir/designsystemet-react";
|
|
1
|
+
import { Badge, BadgePosition, Button, type ButtonProps } from "@digdir/designsystemet-react";
|
|
2
|
+
import { FloppydiskIcon } from "@navikt/aksel-icons";
|
|
2
3
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
3
4
|
import { useSaveButton } from "@olenbetong/appframe-react";
|
|
4
5
|
import { type ForwardRefExoticComponent, forwardRef, type RefAttributes } from "react";
|
|
@@ -6,12 +7,13 @@ import { type ForwardRefExoticComponent, forwardRef, type RefAttributes } from "
|
|
|
6
7
|
export const SaveButton: ForwardRefExoticComponent<ButtonProps & RefAttributes<HTMLButtonElement>> = forwardRef<
|
|
7
8
|
HTMLButtonElement,
|
|
8
9
|
ButtonProps
|
|
9
|
-
>(function SaveButton({ onClick, children, ...props }, ref) {
|
|
10
|
-
let { saving, save } = useSaveButton();
|
|
10
|
+
>(function SaveButton({ onClick, children, loading, ...props }, ref) {
|
|
11
|
+
let { dirty, saving, save } = useSaveButton();
|
|
12
|
+
let isLoading = saving || loading;
|
|
11
13
|
|
|
12
14
|
return (
|
|
13
15
|
<Button
|
|
14
|
-
loading={
|
|
16
|
+
loading={isLoading}
|
|
15
17
|
onClick={(evt) => {
|
|
16
18
|
onClick?.(evt);
|
|
17
19
|
if (!evt.defaultPrevented) {
|
|
@@ -21,6 +23,14 @@ export const SaveButton: ForwardRefExoticComponent<ButtonProps & RefAttributes<H
|
|
|
21
23
|
{...props}
|
|
22
24
|
ref={ref}
|
|
23
25
|
>
|
|
26
|
+
{/* Icon is hidden while loading so the spinner takes its place instead of adding width */}
|
|
27
|
+
{!isLoading && (
|
|
28
|
+
<BadgePosition placement="top-right" overlap="circle">
|
|
29
|
+
<FloppydiskIcon aria-hidden="true" />
|
|
30
|
+
{/* Indicates unsaved changes on the current record */}
|
|
31
|
+
{dirty && <Badge data-color="warning" aria-hidden="true" />}
|
|
32
|
+
</BadgePosition>
|
|
33
|
+
)}
|
|
24
34
|
{children ?? getLocalizedString("Save")}
|
|
25
35
|
</Button>
|
|
26
36
|
);
|
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import { Button, type ButtonProps } from "@digdir/designsystemet-react";
|
|
2
|
-
import
|
|
1
|
+
import { Badge, BadgePosition, Button, type ButtonProps } from "@digdir/designsystemet-react";
|
|
2
|
+
import { FloppydiskIcon } from "@navikt/aksel-icons";
|
|
3
3
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
4
4
|
import { useSaveButton } from "@olenbetong/appframe-react";
|
|
5
5
|
import { forwardRef } from "react";
|
|
6
6
|
|
|
7
7
|
export const SaveIconButton = forwardRef<HTMLButtonElement, Omit<ButtonProps, "icon">>(function SaveIconButton(
|
|
8
|
-
{ onClick, children, ...props },
|
|
8
|
+
{ onClick, children, loading, ...props },
|
|
9
9
|
ref,
|
|
10
10
|
) {
|
|
11
|
-
let { saving, save } = useSaveButton();
|
|
11
|
+
let { dirty, saving, save } = useSaveButton();
|
|
12
|
+
let isLoading = saving || loading;
|
|
12
13
|
|
|
13
14
|
return (
|
|
14
15
|
<Button
|
|
15
16
|
icon
|
|
16
|
-
loading={
|
|
17
|
+
loading={isLoading}
|
|
17
18
|
aria-label={getLocalizedString("Save")}
|
|
18
19
|
variant="tertiary"
|
|
19
20
|
onClick={(evt) => {
|
|
@@ -25,7 +26,13 @@ export const SaveIconButton = forwardRef<HTMLButtonElement, Omit<ButtonProps, "i
|
|
|
25
26
|
{...props}
|
|
26
27
|
ref={ref}
|
|
27
28
|
>
|
|
28
|
-
{children ??
|
|
29
|
+
{children ?? (
|
|
30
|
+
<BadgePosition placement="top-right" overlap="circle">
|
|
31
|
+
<FloppydiskIcon />
|
|
32
|
+
{/* Indicates unsaved changes on the current record */}
|
|
33
|
+
{dirty && <Badge data-color="warning" aria-hidden="true" />}
|
|
34
|
+
</BadgePosition>
|
|
35
|
+
)}
|
|
29
36
|
</Button>
|
|
30
37
|
);
|
|
31
38
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from "./AfLookup/index.js";
|
|
2
2
|
export * from "./binding/index.js";
|
|
3
|
+
export * from "./input/index.js";
|
|
3
4
|
export * from "./layout/index.js";
|
|
4
5
|
export * from "./list/index.js";
|
|
5
6
|
export * from "./page/index.js";
|
|
6
7
|
export * from "./paper/index.js";
|
|
8
|
+
export * from "./progress/index.js";
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
.ObInputAdornments-root[data-size="xs"] {
|
|
24
|
+
--obInputAdornments-padding: 0.125rem;
|
|
25
|
+
--obInputAdornments-fontSize: var(--ds-font-size-1, 0.75rem);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.ObInputAdornments-root[data-size="sm"] {
|
|
29
|
+
--obInputAdornments-padding: 0.25rem;
|
|
30
|
+
--obInputAdornments-fontSize: var(--ds-font-size-2, 0.875rem);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.ObInputAdornments-root[data-size="md"] {
|
|
34
|
+
--obInputAdornments-padding: 0.25rem;
|
|
35
|
+
--obInputAdornments-fontSize: var(--ds-font-size-3, 1rem);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ObInputAdornments-root[data-size="lg"] {
|
|
39
|
+
--obInputAdornments-padding: 0.375rem;
|
|
40
|
+
--obInputAdornments-fontSize: var(--ds-font-size-4, 1.125rem);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.ObInputAdornments-root:hover:not(:focus-within) {
|
|
44
|
+
border-color: var(--ds-color-neutral-border-strong, #767676);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.ObInputAdornments-root:has(.ObInputAdornments-field:focus-visible) {
|
|
48
|
+
box-shadow: var(--dsc-focus-boxShadow);
|
|
49
|
+
outline: var(--dsc-focus-outline);
|
|
50
|
+
outline-offset: var(--ds-border-width-focus, 3px);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.ObInputAdornments-root:has(.ObInputAdornments-field:disabled) {
|
|
54
|
+
cursor: not-allowed;
|
|
55
|
+
opacity: var(--ds-disabled-opacity, 0.3);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* The bare input: all the visual styling lives on the wrapper instead */
|
|
59
|
+
.ObInputAdornments-root .ObInputAdornments-field {
|
|
60
|
+
flex: 1;
|
|
61
|
+
min-inline-size: 0;
|
|
62
|
+
font-size: var(--obInputAdornments-fontSize);
|
|
63
|
+
background: transparent;
|
|
64
|
+
border: none;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.ObInputAdornments-root .ObInputAdornments-field:disabled {
|
|
68
|
+
opacity: 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.ObInputAdornments-root .ObInputAdornments-field:focus-visible {
|
|
72
|
+
box-shadow: none;
|
|
73
|
+
outline: none;
|
|
74
|
+
outline-offset: 0;
|
|
75
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
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 } 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
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Input with adornments, equivalent to a MUI `TextField` with
|
|
51
|
+
* `InputProps.startAdornment` / `endAdornment`. Designsystemet has no
|
|
52
|
+
* equivalent, so the border, background, hover and focus styling is moved from
|
|
53
|
+
* the input onto a wrapper element, making the input and its adornments read
|
|
54
|
+
* as a single control.
|
|
55
|
+
*
|
|
56
|
+
* All other props are forwarded to the Designsystemet `Input`, so `ref`,
|
|
57
|
+
* `value`, `onChange`, `data-size` and `data-color` work as usual.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```jsx
|
|
61
|
+
* // Input first (default), adornments after
|
|
62
|
+
* <InputAdornments aria-label="Search" value={value} onChange={handleChange}>
|
|
63
|
+
* <Button icon variant="tertiary"><MagnifyingGlassIcon aria-hidden /></Button>
|
|
64
|
+
* </InputAdornments>
|
|
65
|
+
*
|
|
66
|
+
* // Explicit placement with InputSlot
|
|
67
|
+
* <InputAdornments aria-label="Amount" data-size="lg">
|
|
68
|
+
* <span aria-hidden>kr</span>
|
|
69
|
+
* <InputSlot />
|
|
70
|
+
* <span aria-hidden>,-</span>
|
|
71
|
+
* </InputAdornments>
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export function InputAdornments({ children, className, inputClassName, ...props }: InputAdornmentsProps) {
|
|
75
|
+
let size = props["data-size"];
|
|
76
|
+
let color = props["data-color"];
|
|
77
|
+
let childArray = Children.toArray(children);
|
|
78
|
+
let slot = childArray.find((child) => isValidElement(child) && child.type === InputSlot) as
|
|
79
|
+
| ReactElement<InputSlotProps>
|
|
80
|
+
| undefined;
|
|
81
|
+
|
|
82
|
+
let input = (
|
|
83
|
+
<Input
|
|
84
|
+
key="ObInputAdornments-input"
|
|
85
|
+
{...props}
|
|
86
|
+
className={clsx("ObInputAdornments-field", inputClassName, slot?.props.className)}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
let content = slot ? childArray.map((child) => (child === slot ? input : child)) : [input, ...childArray];
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className={clsx("ObInputAdornments-root", className)} data-size={size} data-color={color}>
|
|
94
|
+
{content}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./InputAdornments.js";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
.ObLinearProgress-root {
|
|
2
|
+
--obLinearProgress-thickness: 0.25rem;
|
|
3
|
+
--obLinearProgress-duration: 2.1s;
|
|
4
|
+
|
|
5
|
+
position: relative;
|
|
6
|
+
display: block;
|
|
7
|
+
box-sizing: border-box;
|
|
8
|
+
inline-size: 100%;
|
|
9
|
+
block-size: var(--obLinearProgress-thickness);
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
background-color: var(--ds-color-surface-active, #cddffb);
|
|
12
|
+
border-radius: var(--ds-border-radius-full, 624.9375rem);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.ObLinearProgress-root[data-size="xs"] {
|
|
16
|
+
--obLinearProgress-thickness: 0.125rem;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.ObLinearProgress-root[data-size="sm"] {
|
|
20
|
+
--obLinearProgress-thickness: 0.25rem;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.ObLinearProgress-root[data-size="md"] {
|
|
24
|
+
--obLinearProgress-thickness: 0.5rem;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.ObLinearProgress-root[data-size="lg"] {
|
|
28
|
+
--obLinearProgress-thickness: 0.75rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.ObLinearProgress-bar {
|
|
32
|
+
position: absolute;
|
|
33
|
+
inset-block: 0;
|
|
34
|
+
inset-inline-start: 0;
|
|
35
|
+
inline-size: 100%;
|
|
36
|
+
background-color: var(--ds-color-base-default, #0062ba);
|
|
37
|
+
border-radius: inherit;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.ObLinearProgress-determinate .ObLinearProgress-bar {
|
|
41
|
+
transition: inline-size 0.2s linear;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
* The indeterminate bar is full width and animated with `transform`, where the
|
|
46
|
+
* start and end of the bar move at different speeds (the bar stretches and
|
|
47
|
+
* shrinks as it travels).
|
|
48
|
+
*/
|
|
49
|
+
.ObLinearProgress-indeterminate .ObLinearProgress-bar {
|
|
50
|
+
transform-origin: left center;
|
|
51
|
+
will-change: transform;
|
|
52
|
+
animation: ObLinearProgress-indeterminate var(--obLinearProgress-duration) cubic-bezier(0.65, 0.815, 0.735, 0.395)
|
|
53
|
+
infinite;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Zero-height wrapper so the overlay bar never affects the layout */
|
|
57
|
+
.ObLinearProgress-overlay {
|
|
58
|
+
position: relative;
|
|
59
|
+
z-index: 1;
|
|
60
|
+
block-size: 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.ObLinearProgress-overlay > .ObLinearProgress-root {
|
|
64
|
+
position: absolute;
|
|
65
|
+
inset-block-start: 0;
|
|
66
|
+
inset-inline: 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@keyframes ObLinearProgress-indeterminate {
|
|
70
|
+
0% {
|
|
71
|
+
transform: translateX(-35%) scaleX(0.35);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
60% {
|
|
75
|
+
transform: translateX(60%) scaleX(0.7);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
100% {
|
|
79
|
+
transform: translateX(100%) scaleX(0.2);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@media (prefers-reduced-motion: reduce) {
|
|
84
|
+
.ObLinearProgress-indeterminate .ObLinearProgress-bar {
|
|
85
|
+
animation-duration: 4s;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@media print {
|
|
90
|
+
.ObLinearProgress-root {
|
|
91
|
+
display: none;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import "./LinearProgress.css";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import type React from "react";
|
|
5
|
+
|
|
6
|
+
export const linearProgressSizes = ["xs", "sm", "md", "lg"] as const;
|
|
7
|
+
|
|
8
|
+
export type LinearProgressSize = (typeof linearProgressSizes)[number];
|
|
9
|
+
|
|
10
|
+
export type LinearProgressProps = Omit<React.HTMLAttributes<HTMLDivElement>, "color"> & {
|
|
11
|
+
/**
|
|
12
|
+
* Designsystemet colour palette used for the bar
|
|
13
|
+
* @default "accent"
|
|
14
|
+
*/
|
|
15
|
+
"data-color"?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Thickness of the bar
|
|
18
|
+
* @default "sm"
|
|
19
|
+
*/
|
|
20
|
+
"data-size"?: LinearProgressSize;
|
|
21
|
+
/**
|
|
22
|
+
* Renders the bar absolutely positioned in a zero-height wrapper, so it
|
|
23
|
+
* overlays the top of the following content instead of taking up space.
|
|
24
|
+
* Use it for loading indicators that should not shift the layout.
|
|
25
|
+
* @default false
|
|
26
|
+
*/
|
|
27
|
+
overlay?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Progress value. When omitted the bar is indeterminate.
|
|
30
|
+
*/
|
|
31
|
+
value?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Highest possible value of `value`
|
|
34
|
+
* @default 100
|
|
35
|
+
*/
|
|
36
|
+
valueMax?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Lowest possible value of `value`
|
|
39
|
+
* @default 0
|
|
40
|
+
*/
|
|
41
|
+
valueMin?: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Linear progress indicator, equivalent to MUI `LinearProgress`. Indeterminate
|
|
46
|
+
* by default, determinate when `value` is given. Designsystemet has no linear
|
|
47
|
+
* progress component, so this is built with the designsystemet colour and
|
|
48
|
+
* radius tokens (use `data-color` to change palette).
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```jsx
|
|
52
|
+
* // Indeterminate, in the normal document flow
|
|
53
|
+
* <LinearProgress aria-label="Loading..." />
|
|
54
|
+
*
|
|
55
|
+
* // Determinate
|
|
56
|
+
* <LinearProgress aria-label="Uploading" value={40} />
|
|
57
|
+
*
|
|
58
|
+
* // Absolutely positioned, does not affect the layout
|
|
59
|
+
* <LinearProgress overlay aria-label="Loading..." />
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function LinearProgress({
|
|
63
|
+
className,
|
|
64
|
+
overlay,
|
|
65
|
+
value,
|
|
66
|
+
valueMax = 100,
|
|
67
|
+
valueMin = 0,
|
|
68
|
+
"data-color": color = "accent",
|
|
69
|
+
"data-size": size = "sm",
|
|
70
|
+
...props
|
|
71
|
+
}: LinearProgressProps) {
|
|
72
|
+
let isDeterminate = typeof value === "number" && Number.isFinite(value);
|
|
73
|
+
let clamped = isDeterminate ? Math.min(Math.max(value as number, valueMin), valueMax) : undefined;
|
|
74
|
+
let percentage =
|
|
75
|
+
clamped === undefined || valueMax === valueMin ? undefined : ((clamped - valueMin) / (valueMax - valueMin)) * 100;
|
|
76
|
+
|
|
77
|
+
let bar = (
|
|
78
|
+
<div
|
|
79
|
+
className={clsx(
|
|
80
|
+
"ObLinearProgress-root",
|
|
81
|
+
isDeterminate ? "ObLinearProgress-determinate" : "ObLinearProgress-indeterminate",
|
|
82
|
+
!overlay && className,
|
|
83
|
+
)}
|
|
84
|
+
data-color={color}
|
|
85
|
+
data-size={size}
|
|
86
|
+
role="progressbar"
|
|
87
|
+
aria-valuemin={valueMin}
|
|
88
|
+
aria-valuemax={valueMax}
|
|
89
|
+
aria-valuenow={clamped}
|
|
90
|
+
{...props}
|
|
91
|
+
>
|
|
92
|
+
<div
|
|
93
|
+
className="ObLinearProgress-bar"
|
|
94
|
+
style={percentage === undefined ? undefined : { inlineSize: `${percentage}%` }}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (!overlay) {
|
|
100
|
+
return bar;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return <div className={clsx("ObLinearProgress-overlay", className)}>{bar}</div>;
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./LinearProgress.js";
|