@olenbetong/appframe-ds 0.3.0 → 0.4.1
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 +19 -0
- package/es/AfLookup/BoundLookup.d.ts +4 -3
- package/es/AfLookup/Combobox.css +1 -1
- package/es/AfLookup/Combobox.d.ts +22 -5
- package/es/AfLookup/Combobox.js +24 -72
- package/es/AfLookup/Lookup.css +21 -0
- package/es/AfLookup/Lookup.d.ts +7 -6
- package/es/AfLookup/Lookup.js +32 -134
- 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 +15 -15
- package/scripts/copyAssets.mjs +22 -0
- package/src/AfLookup/BoundLookup.tsx +4 -4
- package/src/AfLookup/Combobox.css +1 -1
- package/src/AfLookup/Combobox.tsx +94 -108
- package/src/AfLookup/Lookup.css +21 -0
- package/src/AfLookup/Lookup.tsx +60 -161
- 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
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import "./LookupGrid.css";
|
|
3
|
+
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
4
|
+
import clsx from "clsx";
|
|
5
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
6
|
+
import { FixedSizeList as VirtualList } from "react-window";
|
|
7
|
+
const ROW_HEIGHT = 40;
|
|
8
|
+
const HEADER_HEIGHT = 40;
|
|
9
|
+
function getColumnWidth(column) {
|
|
10
|
+
if (typeof column.width === "number") {
|
|
11
|
+
return `${column.width}px`;
|
|
12
|
+
}
|
|
13
|
+
return column.width ?? "minmax(80px, 1fr)";
|
|
14
|
+
}
|
|
15
|
+
export function LookupGrid({ columns, data, height, currentIndex, currentColumn, getItemId, isItemSelected, onSelectItem, }) {
|
|
16
|
+
let listRef = useRef(null);
|
|
17
|
+
let gridTemplateColumns = useMemo(() => columns.map(getColumnWidth).join(" "), [columns]);
|
|
18
|
+
useEffect(() => listRef.current?.scrollToItem(currentIndex, "smart"), [currentIndex]);
|
|
19
|
+
// oxlint-disable-next-line react/no-nested-component-definitions -- react-window row renderer
|
|
20
|
+
let Row = ({ index, style }) => {
|
|
21
|
+
let item = data[index];
|
|
22
|
+
let itemId = getItemId(item);
|
|
23
|
+
let active = index === currentIndex;
|
|
24
|
+
return (_jsx("div", { role: "row", "aria-selected": active, className: clsx("ObLookupGrid-row", active && "active", isItemSelected?.(item) && "selected"), style: { ...style, gridTemplateColumns }, onClick: () => onSelectItem(item), id: itemId, children: columns.map((column, columnIndex) => (_jsx("div", { role: "gridcell", className: clsx("ObLookupGrid-cell", active && columnIndex === currentColumn && "active"), title: String(item[column.field] ?? ""), children: column.renderCell ? column.renderCell(item[column.field], item) : String(item[column.field] ?? "") }, column.field))) }, itemId));
|
|
25
|
+
};
|
|
26
|
+
return (_jsxs("div", { role: "grid", "aria-label": getLocalizedString("Search results"), className: "ObLookupGrid-root", children: [_jsx("div", { role: "row", className: "ObLookupGrid-header", style: { gridTemplateColumns }, children: columns.map((column) => (_jsx("div", { role: "columnheader", className: "ObLookupGrid-headerCell", children: column.headerName ?? column.field }, column.field))) }), _jsx(VirtualList, { ref: listRef, height: Math.max(0, height - HEADER_HEIGHT), width: "100%", itemCount: data.length, itemSize: ROW_HEIGHT, overscanCount: 5, children: Row })] }));
|
|
27
|
+
}
|
package/es/AfLookup/index.d.ts
CHANGED
package/es/AfLookup/index.js
CHANGED
|
@@ -2,6 +2,6 @@ import { type TextfieldProps } from "@digdir/designsystemet-react";
|
|
|
2
2
|
export type BoundDatePickerProps = Omit<TextfieldProps, "value" | "onChange" | "onKeyDown" | "type"> & {
|
|
3
3
|
field: string;
|
|
4
4
|
};
|
|
5
|
-
export declare const BoundDatePicker: import("react").ForwardRefExoticComponent<Omit<TextfieldProps, "onChange" | "
|
|
5
|
+
export declare const BoundDatePicker: import("react").ForwardRefExoticComponent<Omit<TextfieldProps, "onChange" | "onKeyDown" | "type" | "value"> & {
|
|
6
6
|
field: string;
|
|
7
7
|
} & import("react").RefAttributes<HTMLInputElement>>;
|
|
@@ -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.1",
|
|
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": ">=
|
|
22
|
-
"react-error-boundary": ">=
|
|
23
|
-
"react-router": ">=
|
|
20
|
+
"react": ">=19.2.8",
|
|
21
|
+
"react-dom": ">=19.2.8",
|
|
22
|
+
"react-error-boundary": ">=6.1.2",
|
|
23
|
+
"react-router": ">=8.3.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.23.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.8",
|
|
40
|
+
"react-dom": "19.2.8",
|
|
41
41
|
"react-error-boundary": "6.1.2",
|
|
42
|
-
"react-router": "
|
|
43
|
-
"typescript": "
|
|
42
|
+
"react-router": "8.3.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": "
|
|
49
|
+
"build:css": "node scripts/copyAssets.mjs"
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { cpSync, mkdirSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const files = [
|
|
4
|
+
["src/AfLookup/Lookup.css", "es/AfLookup/Lookup.css"],
|
|
5
|
+
["src/AfLookup/LookupGrid.css", "es/AfLookup/LookupGrid.css"],
|
|
6
|
+
["src/AfLookup/Combobox.css", "es/AfLookup/Combobox.css"],
|
|
7
|
+
["src/layout/AppMenu.css", "es/layout/AppMenu.css"],
|
|
8
|
+
["src/layout/AppMenuItem.css", "es/layout/AppMenuItem.css"],
|
|
9
|
+
["src/layout/AppMenuSubheader.css", "es/layout/AppMenuSubheader.css"],
|
|
10
|
+
["src/layout/AppLayoutDesktop.css", "es/layout/AppLayoutDesktop.css"],
|
|
11
|
+
["src/layout/AppLayoutMobile.css", "es/layout/AppLayoutMobile.css"],
|
|
12
|
+
["src/layout/index.css", "es/layout/index.css"],
|
|
13
|
+
["src/layout/ErrorBoundary.css", "es/layout/ErrorBoundary.css"],
|
|
14
|
+
["src/layout/PageContainer.css", "es/layout/PageContainer.css"],
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
mkdirSync("es/layout", { recursive: true });
|
|
18
|
+
|
|
19
|
+
for (let [src, dest] of files) {
|
|
20
|
+
cpSync(src, dest);
|
|
21
|
+
console.log(`Copied ${src} → ${dest}`);
|
|
22
|
+
}
|
|
@@ -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,58 @@ 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;
|
|
18
|
+
initialSearchValue?: string;
|
|
16
19
|
isItemSelected?: (item: T) => boolean;
|
|
17
|
-
renderItem: (props: React.ComponentPropsWithRef<"li"> & { item: T }) => React.ReactNode;
|
|
18
20
|
onChange?: (item: T | null) => void;
|
|
19
21
|
value?: T;
|
|
20
22
|
onClose?: () => void;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
renderItem:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
export interface ComboboxListProps<T extends Record<string, unknown>> extends ComboboxBaseProps<T> {
|
|
26
|
+
/**
|
|
27
|
+
* Renders the popup content as a list (default).
|
|
28
|
+
*/
|
|
29
|
+
variant?: "list";
|
|
30
|
+
renderItem: (props: React.ComponentPropsWithRef<"li"> & { item: T }) => React.ReactNode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ComboboxGridProps<T extends Record<string, unknown>> extends ComboboxBaseProps<T> {
|
|
34
|
+
/**
|
|
35
|
+
* Renders the popup content as a grid with a header row and one column per
|
|
36
|
+
* entry in `columns`.
|
|
37
|
+
*/
|
|
38
|
+
variant: "grid";
|
|
39
|
+
columns: LookupColumn<T>[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type ComboboxProps<T extends Record<string, unknown>> = ComboboxListProps<T> | ComboboxGridProps<T>;
|
|
43
|
+
|
|
44
|
+
export function Combobox<T extends Record<string, unknown>>(props: ComboboxProps<T>) {
|
|
45
|
+
let {
|
|
46
|
+
nullable,
|
|
47
|
+
dataObject,
|
|
48
|
+
isItemSelected,
|
|
49
|
+
uniqueIdField = "PrimKey",
|
|
50
|
+
initialSearchValue,
|
|
51
|
+
onChange,
|
|
52
|
+
onClose,
|
|
53
|
+
} = props;
|
|
54
|
+
let isGrid = props.variant === "grid";
|
|
55
|
+
let columns = isGrid ? (props as ComboboxGridProps<T>).columns : undefined;
|
|
56
|
+
let listRef = useRef<VirtualList>(null);
|
|
57
|
+
|
|
33
58
|
let [isDesktop, setIsDesktop] = useState(
|
|
34
59
|
() => typeof window !== "undefined" && window.matchMedia("(width >= 768px) and (pointer: fine)").matches,
|
|
35
60
|
);
|
|
@@ -41,27 +66,23 @@ export function Combobox<T extends Record<string, unknown>>({
|
|
|
41
66
|
return () => mq.removeEventListener("change", handler);
|
|
42
67
|
}, []);
|
|
43
68
|
|
|
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(
|
|
69
|
+
let {
|
|
70
|
+
id,
|
|
71
|
+
data,
|
|
72
|
+
loading,
|
|
73
|
+
inputValue,
|
|
74
|
+
setInputValue,
|
|
75
|
+
currentIndex,
|
|
76
|
+
currentColumn,
|
|
77
|
+
getItemId,
|
|
78
|
+
handleSelectItem,
|
|
79
|
+
handleKeyDown,
|
|
80
|
+
} = useComboboxData<T>({
|
|
59
81
|
dataObject,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
);
|
|
82
|
+
uniqueIdField,
|
|
83
|
+
columnCount: columns?.length ?? 1,
|
|
84
|
+
onChange,
|
|
85
|
+
});
|
|
65
86
|
|
|
66
87
|
let wrapperRef = useRef<HTMLDivElement>(null);
|
|
67
88
|
let [height, setHeight] = useState<number>(0);
|
|
@@ -79,71 +100,16 @@ export function Combobox<T extends Record<string, unknown>>({
|
|
|
79
100
|
return () => resizeObserver.disconnect();
|
|
80
101
|
}, []);
|
|
81
102
|
|
|
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
103
|
let sizeMap = useRef(new Map<number, number>());
|
|
137
104
|
let getItemSize = (index: number) => sizeMap.current.get(index) ?? 56;
|
|
138
|
-
let setSize = (index: number, size: number) => {
|
|
105
|
+
let setSize = useCallback((index: number, size: number) => {
|
|
139
106
|
if (sizeMap.current.get(index) !== size) {
|
|
140
107
|
sizeMap.current.set(index, size);
|
|
141
108
|
// listRef will not be set on the initial render, so delay resetting
|
|
142
109
|
setTimeout(() => listRef.current?.resetAfterIndex(index, true));
|
|
143
110
|
}
|
|
144
|
-
};
|
|
111
|
+
}, []);
|
|
145
112
|
|
|
146
|
-
// biome-ignore lint: nested component defined for performance reasons inside render
|
|
147
113
|
let Row = ({ index, style: { height, ...style } }: { index: number; style: React.CSSProperties }) => {
|
|
148
114
|
let rowRef = useRef<HTMLLIElement>(null);
|
|
149
115
|
|
|
@@ -155,9 +121,10 @@ export function Combobox<T extends Record<string, unknown>>({
|
|
|
155
121
|
let item = data[index];
|
|
156
122
|
let itemId = getItemId(item);
|
|
157
123
|
let active = index === currentIndex;
|
|
124
|
+
let ItemComponent = (props as ComboboxListProps<T>).renderItem;
|
|
158
125
|
|
|
159
126
|
return (
|
|
160
|
-
<
|
|
127
|
+
<ItemComponent
|
|
161
128
|
className={clsx("ObCombobox-listItem", active && "active", isItemSelected?.(item) && "selected")}
|
|
162
129
|
aria-selected={active}
|
|
163
130
|
onClick={() => handleSelectItem(item)}
|
|
@@ -175,9 +142,15 @@ export function Combobox<T extends Record<string, unknown>>({
|
|
|
175
142
|
|
|
176
143
|
useEffect(() => listRef.current?.scrollToItem(currentIndex, "smart"), [currentIndex]);
|
|
177
144
|
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (initialSearchValue !== undefined) {
|
|
147
|
+
setInputValue(initialSearchValue);
|
|
148
|
+
}
|
|
149
|
+
}, [initialSearchValue, setInputValue]);
|
|
150
|
+
|
|
178
151
|
return (
|
|
179
|
-
//
|
|
180
|
-
//
|
|
152
|
+
// oxlint-disable-next-line jsx-a11y/click-events-have-key-events -- element is not interactive, click is used to prevent dialog close
|
|
153
|
+
// oxlint-disable-next-line jsx-a11y/no-static-element-interactions -- element is not interactive, click is used to prevent dialog close
|
|
181
154
|
<div className="ObCombobox-root" id={id} onClick={(evt) => evt.stopPropagation()}>
|
|
182
155
|
<div className="ObCombobox-actions">
|
|
183
156
|
{nullable && (
|
|
@@ -201,7 +174,8 @@ export function Combobox<T extends Record<string, unknown>>({
|
|
|
201
174
|
<Textfield
|
|
202
175
|
type="search"
|
|
203
176
|
className="ObCombobox-input"
|
|
204
|
-
label={getLocalizedString("Search")}
|
|
177
|
+
aria-label={getLocalizedString("Search")}
|
|
178
|
+
placeholder={getLocalizedString("Search")}
|
|
205
179
|
onKeyDown={handleKeyDown}
|
|
206
180
|
aria-activedescendant={getItemId(data[currentIndex]) ?? ""}
|
|
207
181
|
value={inputValue}
|
|
@@ -213,20 +187,32 @@ export function Combobox<T extends Record<string, unknown>>({
|
|
|
213
187
|
<div className="ObCombobox-progress-bar" />
|
|
214
188
|
</div>
|
|
215
189
|
)}
|
|
216
|
-
{height > 0 &&
|
|
217
|
-
|
|
218
|
-
<
|
|
219
|
-
|
|
190
|
+
{height > 0 &&
|
|
191
|
+
(columns ? (
|
|
192
|
+
<LookupGrid<T>
|
|
193
|
+
columns={columns}
|
|
194
|
+
data={data}
|
|
220
195
|
height={height}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
196
|
+
currentIndex={currentIndex}
|
|
197
|
+
currentColumn={currentColumn}
|
|
198
|
+
getItemId={getItemId}
|
|
199
|
+
isItemSelected={isItemSelected}
|
|
200
|
+
onSelectItem={handleSelectItem}
|
|
201
|
+
/>
|
|
202
|
+
) : (
|
|
203
|
+
<div className="ObCombobox-list" role="listbox">
|
|
204
|
+
<VirtualList
|
|
205
|
+
ref={listRef}
|
|
206
|
+
height={height}
|
|
207
|
+
width="100%"
|
|
208
|
+
itemCount={data.length}
|
|
209
|
+
itemSize={getItemSize}
|
|
210
|
+
overscanCount={5}
|
|
211
|
+
>
|
|
212
|
+
{Row}
|
|
213
|
+
</VirtualList>
|
|
214
|
+
</div>
|
|
215
|
+
))}
|
|
230
216
|
</div>
|
|
231
217
|
</div>
|
|
232
218
|
);
|