@olenbetong/appframe-ds 0.3.0 → 0.4.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 +13 -0
- package/es/AfLookup/BoundLookup.d.ts +4 -3
- package/es/AfLookup/Combobox.css +1 -1
- package/es/AfLookup/Combobox.d.ts +21 -5
- package/es/AfLookup/Combobox.js +19 -72
- package/es/AfLookup/Lookup.css +21 -0
- package/es/AfLookup/Lookup.d.ts +6 -5
- package/es/AfLookup/Lookup.js +13 -132
- package/es/AfLookup/LookupEdit.d.ts +21 -0
- package/es/AfLookup/LookupEdit.js +49 -0
- package/es/AfLookup/LookupGrid.css +57 -0
- package/es/AfLookup/LookupGrid.d.ts +13 -0
- package/es/AfLookup/LookupGrid.js +27 -0
- package/es/AfLookup/index.d.ts +2 -0
- package/es/AfLookup/index.js +2 -0
- package/es/binding/BoundDatePicker.d.ts +1 -1
- package/es/binding/BoundDateTimePicker.d.ts +1 -1
- package/es/binding/BoundInput.d.ts +1 -1
- package/es/binding/BoundNumericTextField.d.ts +1 -1
- package/es/binding/BoundSwitch.d.ts +1 -1
- package/es/binding/BoundTextField.d.ts +1 -1
- package/es/binding/DataEditToolbar.d.ts +1 -1
- package/es/binding/DataEditToolbar.js +1 -1
- package/es/binding/DataObjectSelect.d.ts +2 -2
- package/es/binding/DataObjectSelect.js +2 -2
- package/es/layout/AppLayoutDesktop.d.ts +1 -1
- package/es/layout/AppLayoutDesktop.js +1 -1
- package/es/layout/AppLayoutMobile.d.ts +1 -1
- package/es/layout/AppLayoutMobile.js +3 -3
- package/es/layout/AppMenu.d.ts +1 -1
- package/es/layout/AppMenu.js +1 -1
- package/es/layout/AppMenuSubheader.d.ts +1 -1
- package/es/layout/ErrorBoundary.d.ts +4 -4
- package/es/layout/PageContainer.d.ts +1 -1
- package/es/layout/index.d.ts +1 -1
- package/package.json +13 -13
- package/src/AfLookup/BoundLookup.tsx +4 -4
- package/src/AfLookup/Combobox.css +1 -1
- package/src/AfLookup/Combobox.tsx +79 -108
- package/src/AfLookup/Lookup.css +21 -0
- package/src/AfLookup/Lookup.tsx +25 -157
- package/src/AfLookup/LookupEdit.tsx +131 -0
- package/src/AfLookup/LookupGrid.css +57 -0
- package/src/AfLookup/LookupGrid.tsx +98 -0
- package/src/AfLookup/index.ts +2 -0
- package/src/binding/DataObjectSelect.tsx +1 -1
- package/src/layout/AppLayoutMobile.tsx +1 -1
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -2,6 +2,6 @@ import { type TextfieldProps } from "@digdir/designsystemet-react";
|
|
|
2
2
|
export type BoundDateTimePickerProps = Omit<TextfieldProps, "value" | "onChange" | "onKeyDown" | "type"> & {
|
|
3
3
|
field: string;
|
|
4
4
|
};
|
|
5
|
-
export declare const BoundDateTimePicker: import("react").ForwardRefExoticComponent<Omit<TextfieldProps, "onChange" | "
|
|
5
|
+
export declare const BoundDateTimePicker: import("react").ForwardRefExoticComponent<Omit<TextfieldProps, "onChange" | "onKeyDown" | "type" | "value"> & {
|
|
6
6
|
field: string;
|
|
7
7
|
} & import("react").RefAttributes<HTMLInputElement>>;
|
|
@@ -3,4 +3,4 @@ export type BoundInputProps = Omit<InputProps, "onChange" | "onKeyDown" | "onBlu
|
|
|
3
3
|
field: string;
|
|
4
4
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
5
5
|
};
|
|
6
|
-
export declare function BoundInput({ field, onChange: onChangeProp, ...props }: BoundInputProps): import("react
|
|
6
|
+
export declare function BoundInput({ field, onChange: onChangeProp, ...props }: BoundInputProps): import("react").JSX.Element;
|
|
@@ -17,4 +17,4 @@ export type BoundNumericTextFieldProps = Omit<TextfieldProps, "onChange" | "onKe
|
|
|
17
17
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
18
18
|
onError?: (error: string | null) => void;
|
|
19
19
|
};
|
|
20
|
-
export declare function BoundNumericTextField({ field, precision, scale, onChange: onChangeProp, onError, ...props }: BoundNumericTextFieldProps): import("react
|
|
20
|
+
export declare function BoundNumericTextField({ field, precision, scale, onChange: onChangeProp, onError, ...props }: BoundNumericTextFieldProps): import("react").JSX.Element;
|
|
@@ -3,7 +3,7 @@ export type BoundSwitchProps = Omit<SwitchProps, "onChange" | "checked"> & {
|
|
|
3
3
|
field: string;
|
|
4
4
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
5
5
|
};
|
|
6
|
-
export declare const BoundSwitch: import("react").ForwardRefExoticComponent<Omit<SwitchProps, "
|
|
6
|
+
export declare const BoundSwitch: import("react").ForwardRefExoticComponent<Omit<SwitchProps, "checked" | "onChange"> & {
|
|
7
7
|
field: string;
|
|
8
8
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
9
9
|
} & import("react").RefAttributes<HTMLInputElement>>;
|
|
@@ -2,6 +2,6 @@ import { type TextfieldProps } from "@digdir/designsystemet-react";
|
|
|
2
2
|
export type BoundTextFieldProps = Omit<TextfieldProps, "value" | "onChange" | "onKeyDown"> & {
|
|
3
3
|
field: string;
|
|
4
4
|
};
|
|
5
|
-
export declare const BoundTextField: import("react").ForwardRefExoticComponent<Omit<TextfieldProps, "onChange" | "
|
|
5
|
+
export declare const BoundTextField: import("react").ForwardRefExoticComponent<Omit<TextfieldProps, "onChange" | "onKeyDown" | "value"> & {
|
|
6
6
|
field: string;
|
|
7
7
|
} & import("react").RefAttributes<HTMLInputElement & HTMLTextAreaElement>>;
|
|
@@ -10,4 +10,4 @@ export type DataEditToolbarProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
|
10
10
|
*/
|
|
11
11
|
showNavigation?: boolean;
|
|
12
12
|
};
|
|
13
|
-
export declare function DataEditToolbar({ children, navigationDataObject, showNavigation, style, ...props }: DataEditToolbarProps): import("react
|
|
13
|
+
export declare function DataEditToolbar({ children, navigationDataObject, showNavigation, style, ...props }: DataEditToolbarProps): import("react").JSX.Element;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Button, Tooltip } from "@digdir/designsystemet-react";
|
|
3
3
|
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
|
4
4
|
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
|
@@ -41,10 +41,10 @@ export type DataObjectSelectProps<T extends Record<string, unknown>> = React.Sel
|
|
|
41
41
|
*/
|
|
42
42
|
getOptionLabel?: (item: T) => string;
|
|
43
43
|
};
|
|
44
|
-
export declare function DataObjectSelect<T extends Record<string, unknown>>({ nullable, blankLabel, dataObject, filter, id: idProp, label, name, getOptionLabel, getOptionValue, valueField, labelField, value, defaultValue, ...props }: DataObjectSelectProps<T>): import("react
|
|
44
|
+
export declare function DataObjectSelect<T extends Record<string, unknown>>({ nullable, blankLabel, dataObject, filter, id: idProp, label, name, getOptionLabel, getOptionValue, valueField, labelField, value, defaultValue, ...props }: DataObjectSelectProps<T>): import("react").JSX.Element;
|
|
45
45
|
export declare function BoundDataObjectSelect<T extends Record<string, unknown>>({ field, ...props }: DataObjectSelectProps<T> & {
|
|
46
46
|
/**
|
|
47
47
|
* Field to bind the value of the select component to.
|
|
48
48
|
*/
|
|
49
49
|
field: string;
|
|
50
|
-
}): import("react
|
|
50
|
+
}): import("react").JSX.Element;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Field, Label, Select } from "@digdir/designsystemet-react";
|
|
3
3
|
import { useFetchData, useField } from "@olenbetong/appframe-react";
|
|
4
4
|
import { useEffect, useId, useRef } from "react";
|
|
@@ -7,7 +7,7 @@ export function DataObjectSelect({ nullable = false, blankLabel = "", dataObject
|
|
|
7
7
|
let fallbackId = useId();
|
|
8
8
|
let id = idProp ?? fallbackId;
|
|
9
9
|
let inputRef = useRef(null);
|
|
10
|
-
//
|
|
10
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps -- default value should only affect the initial value
|
|
11
11
|
useEffect(() => {
|
|
12
12
|
let defaultExists = data.find((r) => String(getOptionValue?.(r) ?? r[valueField]) === defaultValue);
|
|
13
13
|
if (inputRef.current && defaultExists) {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import "./AppLayoutDesktop.css";
|
|
2
2
|
import type { AppLayoutProps } from "./index.js";
|
|
3
|
-
export declare function AppLayoutDesktop({ AppMenuProps, children, menu, partner
|
|
3
|
+
export declare function AppLayoutDesktop({ AppMenuProps, children, menu, partner }: AppLayoutProps): import("react").JSX.Element;
|
|
@@ -13,7 +13,7 @@ function LoadingFallback() {
|
|
|
13
13
|
animation: "progress-indeterminate 1.5s ease-in-out infinite",
|
|
14
14
|
} }));
|
|
15
15
|
}
|
|
16
|
-
export function AppLayoutDesktop({ AppMenuProps, children, menu, partner
|
|
16
|
+
export function AppLayoutDesktop({ AppMenuProps, children, menu, partner }) {
|
|
17
17
|
let [open, setOpen] = useState(true);
|
|
18
18
|
return (_jsxs("div", { className: clsx(partner ? "app-view" : "LayoutLarge-container"), children: [partner && menu && _jsx(PartnerToggle, { open: open, onClick: () => setOpen((o) => !o) }), _jsxs("div", { className: clsx("LayoutLarge-root", menu && "LayoutLarge-withMenu", open && "open"), children: [menu && (_jsx(AppMenu, { className: "LayoutLarge-menu AppMenu-drawer", ...AppMenuProps, children: menu })), _jsx("main", { className: "LayoutLarge-main", children: _jsx(Suspense, { fallback: _jsx(LoadingFallback, {}), children: _jsx(ErrorBoundary, { FallbackComponent: ErrorAlertFallback, children: children }) }) })] })] }));
|
|
19
19
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import "./AppLayoutMobile.css";
|
|
2
2
|
import type { AppLayoutProps } from "./index.js";
|
|
3
|
-
export declare function AppLayoutMobile({ AppMenuProps, children, menu, partner }: AppLayoutProps): import("react
|
|
3
|
+
export declare function AppLayoutMobile({ AppMenuProps, children, menu, partner }: AppLayoutProps): import("react").JSX.Element;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import "./AppLayoutMobile.css";
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import { useLocation } from "react-router";
|
|
@@ -10,9 +10,9 @@ function Toggle({ onToggle }) {
|
|
|
10
10
|
export function AppLayoutMobile({ AppMenuProps, children, menu, partner }) {
|
|
11
11
|
let [drawerOpen, setDrawerOpen] = useState(false);
|
|
12
12
|
let location = useLocation();
|
|
13
|
-
//
|
|
13
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps -- close drawer on navigation
|
|
14
14
|
useEffect(() => {
|
|
15
15
|
setDrawerOpen(false);
|
|
16
16
|
}, [location]);
|
|
17
|
-
return (_jsxs("div", { className: "LayoutSmall-root", children: [partner && menu &&
|
|
17
|
+
return (_jsxs("div", { className: "LayoutSmall-root", children: [partner && menu && _jsx(PartnerToggle, { open: drawerOpen, onClick: () => setDrawerOpen((c) => !c) }), !partner && menu && (_jsx("div", { className: "LayoutSmall-menuBar", children: _jsx(Toggle, { onToggle: () => setDrawerOpen((c) => !c) }) })), menu && (_jsxs(_Fragment, { children: [_jsx("div", { className: `LayoutSmall-drawerOverlay${drawerOpen ? " open" : ""}`, onClick: () => setDrawerOpen(false), "aria-hidden": "true" }), _jsx("div", { className: `LayoutSmall-drawer AppMenu-drawer${drawerOpen ? " open" : ""}`, children: _jsx(AppMenu, { ...AppMenuProps, children: menu }) })] })), children] }));
|
|
18
18
|
}
|
package/es/layout/AppMenu.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ export type AppMenuProps = Pick<React.HTMLProps<HTMLElement>, "aria-label" | "ar
|
|
|
5
5
|
headerPrimary?: string;
|
|
6
6
|
headerSecondary?: string;
|
|
7
7
|
};
|
|
8
|
-
export declare function AppMenu({ children, className, headerPrimary, headerSecondary, ...props }: AppMenuProps):
|
|
8
|
+
export declare function AppMenu({ children, className, headerPrimary, headerSecondary, ...props }: AppMenuProps): React.JSX.Element;
|
package/es/layout/AppMenu.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import "./AppMenu.css";
|
|
3
3
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
4
4
|
import clsx from "clsx";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import "./AppMenuSubheader.css";
|
|
2
2
|
import type React from "react";
|
|
3
3
|
export type AppMenuSubheaderProps = React.HTMLAttributes<HTMLDivElement>;
|
|
4
|
-
export declare function AppMenuSubheader({ children, className, ...props }: AppMenuSubheaderProps):
|
|
4
|
+
export declare function AppMenuSubheader({ children, className, ...props }: AppMenuSubheaderProps): React.JSX.Element;
|
|
@@ -21,10 +21,10 @@ export type ErrorAlertFallbackProps = {
|
|
|
21
21
|
error: unknown;
|
|
22
22
|
className?: string;
|
|
23
23
|
};
|
|
24
|
-
export declare function ErrorAlertFallback({ error, className }: ErrorAlertFallbackProps): import("react
|
|
25
|
-
export declare function RouteErrorPage(): import("react
|
|
26
|
-
export declare function AsyncErrorAlert(): import("react
|
|
24
|
+
export declare function ErrorAlertFallback({ error, className }: ErrorAlertFallbackProps): import("react").JSX.Element;
|
|
25
|
+
export declare function RouteErrorPage(): import("react").JSX.Element;
|
|
26
|
+
export declare function AsyncErrorAlert(): import("react").JSX.Element;
|
|
27
27
|
export declare function DataObjectErrorAlert({ dataObject }: {
|
|
28
28
|
dataObject: DataObject<any>;
|
|
29
|
-
}): import("react
|
|
29
|
+
}): import("react").JSX.Element | null;
|
|
30
30
|
export {};
|
|
@@ -4,4 +4,4 @@ export type PageContainerProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
|
4
4
|
fullHeight?: boolean;
|
|
5
5
|
maxWidth?: "xs" | "sm" | "md" | "lg" | "xl" | false;
|
|
6
6
|
};
|
|
7
|
-
export declare function PageContainer({ children, className, fullHeight, maxWidth, ...props }: PageContainerProps):
|
|
7
|
+
export declare function PageContainer({ children, className, fullHeight, maxWidth, ...props }: PageContainerProps): React.JSX.Element;
|
package/es/layout/index.d.ts
CHANGED
|
@@ -30,4 +30,4 @@ export * from "./constants.js";
|
|
|
30
30
|
export * from "./ErrorBoundary.js";
|
|
31
31
|
export * from "./PageContainer.js";
|
|
32
32
|
export * from "./PartnerToggle.js";
|
|
33
|
-
export declare function AppLayout(props: AppLayoutProps):
|
|
33
|
+
export declare function AppLayout(props: AppLayoutProps): React.JSX.Element;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olenbetong/appframe-ds",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Components that bind Designsystemet components to Appframe data objects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./es/index.d.ts",
|
|
@@ -15,37 +15,37 @@
|
|
|
15
15
|
"author": "Bjørnar Vister Hansen <bvh@olenbetong.no>",
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"@digdir/designsystemet-react": ">=1.
|
|
18
|
+
"@digdir/designsystemet-react": ">=1.18.0",
|
|
19
19
|
"@mui/icons-material": ">=7.0.0",
|
|
20
|
-
"react": ">=
|
|
21
|
-
"react-dom": ">=
|
|
20
|
+
"react": ">=19.2.7",
|
|
21
|
+
"react-dom": ">=19.2.7",
|
|
22
22
|
"react-error-boundary": ">=5.0.0",
|
|
23
23
|
"react-router": ">=7.0.0"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@olenbetong/appframe-core": "2.11.10",
|
|
27
27
|
"@olenbetong/appframe-data": "1.5.0",
|
|
28
|
-
"@olenbetong/appframe-react": "1.
|
|
28
|
+
"@olenbetong/appframe-react": "1.22.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
|
-
"@digdir/designsystemet-react": "1.
|
|
35
|
-
"@mui/icons-material": "9.0
|
|
36
|
-
"@types/react": "19.2.
|
|
34
|
+
"@digdir/designsystemet-react": "1.18.0",
|
|
35
|
+
"@mui/icons-material": "9.2.0",
|
|
36
|
+
"@types/react": "19.2.17",
|
|
37
37
|
"@types/react-dom": "19.2.3",
|
|
38
38
|
"@types/react-window": "^1.8.8",
|
|
39
|
-
"react": "19.2.
|
|
40
|
-
"react-dom": "19.2.
|
|
39
|
+
"react": "19.2.7",
|
|
40
|
+
"react-dom": "19.2.7",
|
|
41
41
|
"react-error-boundary": "6.1.2",
|
|
42
|
-
"react-router": "7.
|
|
43
|
-
"typescript": "
|
|
42
|
+
"react-router": "7.17.0",
|
|
43
|
+
"typescript": "7.0.2"
|
|
44
44
|
},
|
|
45
45
|
"optionalDependencies": {},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "pnpm run build:tsc && pnpm run build:css",
|
|
48
48
|
"build:tsc": "node --no-warnings ../../scripts/clean.ts ./es tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
|
|
49
|
-
"build:css": "cp src/AfLookup/Lookup.css es/AfLookup/Lookup.css && cp src/AfLookup/Combobox.css es/AfLookup/Combobox.css && mkdir -p es/layout && cp src/layout/AppMenu.css es/layout/AppMenu.css && cp src/layout/AppMenuItem.css es/layout/AppMenuItem.css && cp src/layout/AppMenuSubheader.css es/layout/AppMenuSubheader.css && cp src/layout/AppLayoutDesktop.css es/layout/AppLayoutDesktop.css && cp src/layout/AppLayoutMobile.css es/layout/AppLayoutMobile.css && cp src/layout/index.css es/layout/index.css && cp src/layout/ErrorBoundary.css es/layout/ErrorBoundary.css && cp src/layout/PageContainer.css es/layout/PageContainer.css"
|
|
49
|
+
"build:css": "cp src/AfLookup/Lookup.css es/AfLookup/Lookup.css && cp src/AfLookup/Combobox.css es/AfLookup/Combobox.css && cp src/AfLookup/LookupGrid.css es/AfLookup/LookupGrid.css && mkdir -p es/layout && cp src/layout/AppMenu.css es/layout/AppMenu.css && cp src/layout/AppMenuItem.css es/layout/AppMenuItem.css && cp src/layout/AppMenuSubheader.css es/layout/AppMenuSubheader.css && cp src/layout/AppLayoutDesktop.css es/layout/AppLayoutDesktop.css && cp src/layout/AppLayoutMobile.css es/layout/AppLayoutMobile.css && cp src/layout/index.css es/layout/index.css && cp src/layout/ErrorBoundary.css es/layout/ErrorBoundary.css && cp src/layout/PageContainer.css es/layout/PageContainer.css"
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import type { CurrentRow, DataObject } from "@olenbetong/appframe-data";
|
|
2
|
-
import { useCurrentRow, useDataObject } from "@olenbetong/appframe-react";
|
|
2
|
+
import { type DistributiveOmit, useCurrentRow, useDataObject } from "@olenbetong/appframe-react";
|
|
3
3
|
|
|
4
4
|
import { AfLookup, type AfLookupProps } from "./Lookup.js";
|
|
5
5
|
|
|
6
|
-
export
|
|
6
|
+
export type BoundLookupProps<
|
|
7
7
|
TBoundData extends Record<string, unknown>,
|
|
8
8
|
TLookupData extends Record<string, unknown>,
|
|
9
|
-
>
|
|
9
|
+
> = DistributiveOmit<AfLookupProps<TLookupData>, "onChange"> & {
|
|
10
10
|
dataObject: DataObject<TLookupData>;
|
|
11
11
|
displayField?: keyof TBoundData;
|
|
12
12
|
getChanges: (selectedItem: TLookupData | null) => Partial<TBoundData>;
|
|
13
13
|
getDisplayValue?: (currentRow: CurrentRow<TBoundData>) => string;
|
|
14
|
-
}
|
|
14
|
+
};
|
|
15
15
|
|
|
16
16
|
export function BoundLookup<
|
|
17
17
|
TBoundData extends Record<string, unknown>,
|
|
@@ -3,33 +3,49 @@ import "./Combobox.css";
|
|
|
3
3
|
import { Button, Textfield } from "@digdir/designsystemet-react";
|
|
4
4
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
5
5
|
import type { DataObject } from "@olenbetong/appframe-data";
|
|
6
|
-
import {
|
|
6
|
+
import { type LookupColumn, useComboboxData } from "@olenbetong/appframe-react";
|
|
7
7
|
|
|
8
8
|
import clsx from "clsx";
|
|
9
|
-
import { useCallback, useEffect,
|
|
9
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
10
10
|
import { VariableSizeList as VirtualList } from "react-window";
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
import { LookupGrid } from "./LookupGrid.js";
|
|
13
|
+
|
|
14
|
+
export interface ComboboxBaseProps<T extends Record<string, unknown>> {
|
|
13
15
|
nullable?: boolean;
|
|
14
16
|
dataObject: DataObject<T>;
|
|
15
17
|
uniqueIdField?: string;
|
|
16
18
|
isItemSelected?: (item: T) => boolean;
|
|
17
|
-
renderItem: (props: React.ComponentPropsWithRef<"li"> & { item: T }) => React.ReactNode;
|
|
18
19
|
onChange?: (item: T | null) => void;
|
|
19
20
|
value?: T;
|
|
20
21
|
onClose?: () => void;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
renderItem:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
export interface ComboboxListProps<T extends Record<string, unknown>> extends ComboboxBaseProps<T> {
|
|
25
|
+
/**
|
|
26
|
+
* Renders the popup content as a list (default).
|
|
27
|
+
*/
|
|
28
|
+
variant?: "list";
|
|
29
|
+
renderItem: (props: React.ComponentPropsWithRef<"li"> & { item: T }) => React.ReactNode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ComboboxGridProps<T extends Record<string, unknown>> extends ComboboxBaseProps<T> {
|
|
33
|
+
/**
|
|
34
|
+
* Renders the popup content as a grid with a header row and one column per
|
|
35
|
+
* entry in `columns`.
|
|
36
|
+
*/
|
|
37
|
+
variant: "grid";
|
|
38
|
+
columns: LookupColumn<T>[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type ComboboxProps<T extends Record<string, unknown>> = ComboboxListProps<T> | ComboboxGridProps<T>;
|
|
42
|
+
|
|
43
|
+
export function Combobox<T extends Record<string, unknown>>(props: ComboboxProps<T>) {
|
|
44
|
+
let { nullable, dataObject, isItemSelected, uniqueIdField = "PrimKey", onChange, onClose } = props;
|
|
45
|
+
let isGrid = props.variant === "grid";
|
|
46
|
+
let columns = isGrid ? (props as ComboboxGridProps<T>).columns : undefined;
|
|
47
|
+
let listRef = useRef<VirtualList>(null);
|
|
48
|
+
|
|
33
49
|
let [isDesktop, setIsDesktop] = useState(
|
|
34
50
|
() => typeof window !== "undefined" && window.matchMedia("(width >= 768px) and (pointer: fine)").matches,
|
|
35
51
|
);
|
|
@@ -41,27 +57,23 @@ export function Combobox<T extends Record<string, unknown>>({
|
|
|
41
57
|
return () => mq.removeEventListener("change", handler);
|
|
42
58
|
}, []);
|
|
43
59
|
|
|
44
|
-
let
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
let query = useDebounce(inputValue, 300);
|
|
57
|
-
let loading = useLoading(dataObject);
|
|
58
|
-
let data = useDataWithFilter(
|
|
60
|
+
let {
|
|
61
|
+
id,
|
|
62
|
+
data,
|
|
63
|
+
loading,
|
|
64
|
+
inputValue,
|
|
65
|
+
setInputValue,
|
|
66
|
+
currentIndex,
|
|
67
|
+
currentColumn,
|
|
68
|
+
getItemId,
|
|
69
|
+
handleSelectItem,
|
|
70
|
+
handleKeyDown,
|
|
71
|
+
} = useComboboxData<T>({
|
|
59
72
|
dataObject,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
);
|
|
73
|
+
uniqueIdField,
|
|
74
|
+
columnCount: columns?.length ?? 1,
|
|
75
|
+
onChange,
|
|
76
|
+
});
|
|
65
77
|
|
|
66
78
|
let wrapperRef = useRef<HTMLDivElement>(null);
|
|
67
79
|
let [height, setHeight] = useState<number>(0);
|
|
@@ -79,71 +91,16 @@ export function Combobox<T extends Record<string, unknown>>({
|
|
|
79
91
|
return () => resizeObserver.disconnect();
|
|
80
92
|
}, []);
|
|
81
93
|
|
|
82
|
-
// Reset current index when new data is loaded
|
|
83
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: isn't it obvious?
|
|
84
|
-
useEffect(() => {
|
|
85
|
-
setCurrentIndex(0);
|
|
86
|
-
}, [data]);
|
|
87
|
-
|
|
88
|
-
let getItemId = useCallback(
|
|
89
|
-
(item: T | undefined) => {
|
|
90
|
-
if (!item) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
let itemId = item[uniqueIdField];
|
|
95
|
-
|
|
96
|
-
return `${id}-${itemId}`;
|
|
97
|
-
},
|
|
98
|
-
[id, uniqueIdField],
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
function handleSelectItem(item: T) {
|
|
102
|
-
onChange?.(item);
|
|
103
|
-
setInputValue("");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
|
107
|
-
let newIndex: number | null = null;
|
|
108
|
-
|
|
109
|
-
if (event.key === "ArrowDown") {
|
|
110
|
-
newIndex = currentIndex + 1;
|
|
111
|
-
} else if (event.key === "ArrowUp") {
|
|
112
|
-
newIndex = currentIndex - 1;
|
|
113
|
-
} else if (event.key === "Home" && !event.shiftKey && !event.ctrlKey && !event.metaKey) {
|
|
114
|
-
newIndex = 0;
|
|
115
|
-
} else if (event.key === "End" && !event.shiftKey && !event.ctrlKey && !event.metaKey) {
|
|
116
|
-
newIndex = data.length - 1;
|
|
117
|
-
} else if (event.key === "Enter") {
|
|
118
|
-
event.stopPropagation();
|
|
119
|
-
event.preventDefault();
|
|
120
|
-
handleSelectItem(data[currentIndex]);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (newIndex !== null) {
|
|
125
|
-
event.stopPropagation();
|
|
126
|
-
event.preventDefault();
|
|
127
|
-
if (newIndex < 0) {
|
|
128
|
-
newIndex = data.length - 1;
|
|
129
|
-
} else if (newIndex >= data.length) {
|
|
130
|
-
newIndex = 0;
|
|
131
|
-
}
|
|
132
|
-
setCurrentIndex(newIndex);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
94
|
let sizeMap = useRef(new Map<number, number>());
|
|
137
95
|
let getItemSize = (index: number) => sizeMap.current.get(index) ?? 56;
|
|
138
|
-
let setSize = (index: number, size: number) => {
|
|
96
|
+
let setSize = useCallback((index: number, size: number) => {
|
|
139
97
|
if (sizeMap.current.get(index) !== size) {
|
|
140
98
|
sizeMap.current.set(index, size);
|
|
141
99
|
// listRef will not be set on the initial render, so delay resetting
|
|
142
100
|
setTimeout(() => listRef.current?.resetAfterIndex(index, true));
|
|
143
101
|
}
|
|
144
|
-
};
|
|
102
|
+
}, []);
|
|
145
103
|
|
|
146
|
-
// biome-ignore lint: nested component defined for performance reasons inside render
|
|
147
104
|
let Row = ({ index, style: { height, ...style } }: { index: number; style: React.CSSProperties }) => {
|
|
148
105
|
let rowRef = useRef<HTMLLIElement>(null);
|
|
149
106
|
|
|
@@ -155,9 +112,10 @@ export function Combobox<T extends Record<string, unknown>>({
|
|
|
155
112
|
let item = data[index];
|
|
156
113
|
let itemId = getItemId(item);
|
|
157
114
|
let active = index === currentIndex;
|
|
115
|
+
let ItemComponent = (props as ComboboxListProps<T>).renderItem;
|
|
158
116
|
|
|
159
117
|
return (
|
|
160
|
-
<
|
|
118
|
+
<ItemComponent
|
|
161
119
|
className={clsx("ObCombobox-listItem", active && "active", isItemSelected?.(item) && "selected")}
|
|
162
120
|
aria-selected={active}
|
|
163
121
|
onClick={() => handleSelectItem(item)}
|
|
@@ -176,8 +134,8 @@ export function Combobox<T extends Record<string, unknown>>({
|
|
|
176
134
|
useEffect(() => listRef.current?.scrollToItem(currentIndex, "smart"), [currentIndex]);
|
|
177
135
|
|
|
178
136
|
return (
|
|
179
|
-
//
|
|
180
|
-
//
|
|
137
|
+
// oxlint-disable-next-line jsx-a11y/click-events-have-key-events -- element is not interactive, click is used to prevent dialog close
|
|
138
|
+
// oxlint-disable-next-line jsx-a11y/no-static-element-interactions -- element is not interactive, click is used to prevent dialog close
|
|
181
139
|
<div className="ObCombobox-root" id={id} onClick={(evt) => evt.stopPropagation()}>
|
|
182
140
|
<div className="ObCombobox-actions">
|
|
183
141
|
{nullable && (
|
|
@@ -201,7 +159,8 @@ export function Combobox<T extends Record<string, unknown>>({
|
|
|
201
159
|
<Textfield
|
|
202
160
|
type="search"
|
|
203
161
|
className="ObCombobox-input"
|
|
204
|
-
label={getLocalizedString("Search")}
|
|
162
|
+
aria-label={getLocalizedString("Search")}
|
|
163
|
+
placeholder={getLocalizedString("Search")}
|
|
205
164
|
onKeyDown={handleKeyDown}
|
|
206
165
|
aria-activedescendant={getItemId(data[currentIndex]) ?? ""}
|
|
207
166
|
value={inputValue}
|
|
@@ -213,20 +172,32 @@ export function Combobox<T extends Record<string, unknown>>({
|
|
|
213
172
|
<div className="ObCombobox-progress-bar" />
|
|
214
173
|
</div>
|
|
215
174
|
)}
|
|
216
|
-
{height > 0 &&
|
|
217
|
-
|
|
218
|
-
<
|
|
219
|
-
|
|
175
|
+
{height > 0 &&
|
|
176
|
+
(columns ? (
|
|
177
|
+
<LookupGrid<T>
|
|
178
|
+
columns={columns}
|
|
179
|
+
data={data}
|
|
220
180
|
height={height}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
181
|
+
currentIndex={currentIndex}
|
|
182
|
+
currentColumn={currentColumn}
|
|
183
|
+
getItemId={getItemId}
|
|
184
|
+
isItemSelected={isItemSelected}
|
|
185
|
+
onSelectItem={handleSelectItem}
|
|
186
|
+
/>
|
|
187
|
+
) : (
|
|
188
|
+
<div className="ObCombobox-list" role="listbox">
|
|
189
|
+
<VirtualList
|
|
190
|
+
ref={listRef}
|
|
191
|
+
height={height}
|
|
192
|
+
width="100%"
|
|
193
|
+
itemCount={data.length}
|
|
194
|
+
itemSize={getItemSize}
|
|
195
|
+
overscanCount={5}
|
|
196
|
+
>
|
|
197
|
+
{Row}
|
|
198
|
+
</VirtualList>
|
|
199
|
+
</div>
|
|
200
|
+
))}
|
|
230
201
|
</div>
|
|
231
202
|
</div>
|
|
232
203
|
);
|
package/src/AfLookup/Lookup.css
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
|
+
.ObLookup-inputWrap {
|
|
2
|
+
position: relative;
|
|
3
|
+
|
|
4
|
+
.ds-input {
|
|
5
|
+
padding-inline-end: var(--ObLookup-buttons-width, 3rem);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.ObLookup-buttons {
|
|
10
|
+
position: absolute;
|
|
11
|
+
inset-inline-end: 0;
|
|
12
|
+
inset-block-end: 0;
|
|
13
|
+
/* Match the .ds-input height so the buttons center on the input row,
|
|
14
|
+
also when a label or description is rendered above the input */
|
|
15
|
+
height: var(--ds-size-12, 3rem);
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
padding-inline-end: var(--ds-size-1, 0.25rem);
|
|
19
|
+
}
|
|
20
|
+
|
|
1
21
|
.ObLookup-dialog {
|
|
2
22
|
border: 0;
|
|
3
23
|
margin-block-start: 0;
|
|
4
24
|
padding: 1rem;
|
|
25
|
+
overflow: hidden;
|
|
5
26
|
|
|
6
27
|
background-color: whitesmoke;
|
|
7
28
|
|