@olenbetong/appframe-ds 0.5.0 → 0.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 +22 -0
- package/package.json +11 -9
- package/scripts/copyAssets.mjs +10 -1
- package/src/AfLookup/Lookup.tsx +7 -8
- package/src/AfLookup/{Combobox.tsx → LookupCombobox.tsx} +8 -8
- package/src/AfLookup/LookupEdit.tsx +5 -6
- package/src/AfLookup/index.ts +1 -1
- package/src/autocomplete/Autocomplete.css +222 -0
- package/src/autocomplete/Autocomplete.tsx +316 -0
- package/src/autocomplete/AutocompleteFrame.tsx +149 -0
- package/src/autocomplete/Combobox.tsx +260 -0
- package/src/autocomplete/index.ts +4 -0
- package/src/autocomplete/types.ts +200 -0
- package/src/autocomplete/utils.ts +183 -0
- 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 +4 -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/src/sortable/DropIndicator.css +51 -0
- package/src/sortable/DropIndicator.tsx +27 -0
- package/src/sortable/SortableItemList.css +21 -0
- package/src/sortable/SortableItemList.tsx +264 -0
- package/src/sortable/index.ts +8 -0
- package/src/sortable/reorderSortOrder.ts +126 -0
- package/src/sortable/sortOrder.ts +101 -0
- package/src/sortable/types.ts +9 -0
- package/src/sortable/useSortableContainer.ts +53 -0
- package/src/sortable/useSortableItem.ts +95 -0
- package/src/sortable/useSortableList.ts +117 -0
- /package/src/AfLookup/{Combobox.css → LookupCombobox.css} +0 -0
|
@@ -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,10 @@
|
|
|
1
1
|
export * from "./AfLookup/index.js";
|
|
2
|
+
export * from "./autocomplete/index.js";
|
|
2
3
|
export * from "./binding/index.js";
|
|
4
|
+
export * from "./input/index.js";
|
|
3
5
|
export * from "./layout/index.js";
|
|
4
6
|
export * from "./list/index.js";
|
|
5
7
|
export * from "./page/index.js";
|
|
6
8
|
export * from "./paper/index.js";
|
|
9
|
+
export * from "./progress/index.js";
|
|
10
|
+
export * from "./sortable/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";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--ob-drop-indicator-thickness: 0.125rem;
|
|
3
|
+
--ob-drop-indicator-color: #3f3fa3;
|
|
4
|
+
--ob-drop-indicator-terminal-size: 0.75rem;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.ObDropIndicator-root {
|
|
8
|
+
position: absolute;
|
|
9
|
+
z-index: 10;
|
|
10
|
+
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
|
|
14
|
+
pointer-events: none;
|
|
15
|
+
|
|
16
|
+
&::before {
|
|
17
|
+
content: "";
|
|
18
|
+
|
|
19
|
+
border: var(--ob-drop-indicator-thickness) solid var(--ob-drop-indicator-color);
|
|
20
|
+
border-radius: 50%;
|
|
21
|
+
|
|
22
|
+
width: var(--ob-drop-indicator-terminal-size);
|
|
23
|
+
height: var(--ob-drop-indicator-terminal-size);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&::after {
|
|
27
|
+
content: "";
|
|
28
|
+
|
|
29
|
+
height: var(--ob-drop-indicator-thickness);
|
|
30
|
+
flex-grow: 1;
|
|
31
|
+
background-color: var(--ob-drop-indicator-color);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
&.ObDropIndicator-top,
|
|
35
|
+
&.ObDropIndicator-bottom {
|
|
36
|
+
height: var(--ob-drop-indicator-thickness);
|
|
37
|
+
|
|
38
|
+
left: calc(-1 * var(--ob-drop-indicator-terminal-size) / 2);
|
|
39
|
+
width: calc(100% + var(--ob-drop-indicator-terminal-size) / 2);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&.ObDropIndicator-top {
|
|
43
|
+
top: var(--ob-drop-indicator-gap, 0rem);
|
|
44
|
+
transform: translateY(calc(var(--ob-drop-indicator-thickness) / -2));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
&.ObDropIndicator-bottom {
|
|
48
|
+
bottom: var(--ob-drop-indicator-gap, 0rem);
|
|
49
|
+
transform: translateY(calc(var(--ob-drop-indicator-thickness) / 2));
|
|
50
|
+
}
|
|
51
|
+
}
|