@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/es/AfLookup/BoundLookup.d.ts +4 -3
  3. package/es/AfLookup/Combobox.css +1 -1
  4. package/es/AfLookup/Combobox.d.ts +21 -5
  5. package/es/AfLookup/Combobox.js +19 -72
  6. package/es/AfLookup/Lookup.css +21 -0
  7. package/es/AfLookup/Lookup.d.ts +6 -5
  8. package/es/AfLookup/Lookup.js +13 -132
  9. package/es/AfLookup/LookupEdit.d.ts +21 -0
  10. package/es/AfLookup/LookupEdit.js +49 -0
  11. package/es/AfLookup/LookupGrid.css +57 -0
  12. package/es/AfLookup/LookupGrid.d.ts +13 -0
  13. package/es/AfLookup/LookupGrid.js +27 -0
  14. package/es/AfLookup/index.d.ts +2 -0
  15. package/es/AfLookup/index.js +2 -0
  16. package/es/binding/BoundDatePicker.d.ts +1 -1
  17. package/es/binding/BoundDateTimePicker.d.ts +1 -1
  18. package/es/binding/BoundInput.d.ts +1 -1
  19. package/es/binding/BoundNumericTextField.d.ts +1 -1
  20. package/es/binding/BoundSwitch.d.ts +1 -1
  21. package/es/binding/BoundTextField.d.ts +1 -1
  22. package/es/binding/DataEditToolbar.d.ts +1 -1
  23. package/es/binding/DataEditToolbar.js +1 -1
  24. package/es/binding/DataObjectSelect.d.ts +2 -2
  25. package/es/binding/DataObjectSelect.js +2 -2
  26. package/es/layout/AppLayoutDesktop.d.ts +1 -1
  27. package/es/layout/AppLayoutDesktop.js +1 -1
  28. package/es/layout/AppLayoutMobile.d.ts +1 -1
  29. package/es/layout/AppLayoutMobile.js +3 -3
  30. package/es/layout/AppMenu.d.ts +1 -1
  31. package/es/layout/AppMenu.js +1 -1
  32. package/es/layout/AppMenuSubheader.d.ts +1 -1
  33. package/es/layout/ErrorBoundary.d.ts +4 -4
  34. package/es/layout/PageContainer.d.ts +1 -1
  35. package/es/layout/index.d.ts +1 -1
  36. package/package.json +13 -13
  37. package/src/AfLookup/BoundLookup.tsx +4 -4
  38. package/src/AfLookup/Combobox.css +1 -1
  39. package/src/AfLookup/Combobox.tsx +79 -108
  40. package/src/AfLookup/Lookup.css +21 -0
  41. package/src/AfLookup/Lookup.tsx +25 -157
  42. package/src/AfLookup/LookupEdit.tsx +131 -0
  43. package/src/AfLookup/LookupGrid.css +57 -0
  44. package/src/AfLookup/LookupGrid.tsx +98 -0
  45. package/src/AfLookup/index.ts +2 -0
  46. package/src/binding/DataObjectSelect.tsx +1 -1
  47. package/src/layout/AppLayoutMobile.tsx +1 -1
  48. 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" | "value" | "onKeyDown" | "type"> & {
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/jsx-runtime").JSX.Element;
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/jsx-runtime").JSX.Element;
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, "onChange" | "checked"> & {
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" | "value" | "onKeyDown"> & {
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/jsx-runtime").JSX.Element;
13
+ export declare function DataEditToolbar({ children, navigationDataObject, showNavigation, style, ...props }: DataEditToolbarProps): import("react").JSX.Element;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
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/jsx-runtime").JSX.Element;
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/jsx-runtime").JSX.Element;
50
+ }): import("react").JSX.Element;
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
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
- // biome-ignore lint/correctness/useExhaustiveDependencies: default value should only affect the initial value
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, }: AppLayoutProps): import("react/jsx-runtime").JSX.Element;
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/jsx-runtime").JSX.Element;
3
+ export declare function AppLayoutMobile({ AppMenuProps, children, menu, partner }: AppLayoutProps): import("react").JSX.Element;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
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
- // biome-ignore lint/correctness/useExhaustiveDependencies: close drawer on navigation
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 && (_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] }));
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
  }
@@ -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): import("react/jsx-runtime").JSX.Element;
8
+ export declare function AppMenu({ children, className, headerPrimary, headerSecondary, ...props }: AppMenuProps): React.JSX.Element;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
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): import("react/jsx-runtime").JSX.Element;
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/jsx-runtime").JSX.Element;
25
- export declare function RouteErrorPage(): import("react/jsx-runtime").JSX.Element;
26
- export declare function AsyncErrorAlert(): import("react/jsx-runtime").JSX.Element;
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/jsx-runtime").JSX.Element | null;
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): import("react/jsx-runtime").JSX.Element;
7
+ export declare function PageContainer({ children, className, fullHeight, maxWidth, ...props }: PageContainerProps): React.JSX.Element;
@@ -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): import("react/jsx-runtime").JSX.Element;
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.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.15.0",
18
+ "@digdir/designsystemet-react": ">=1.18.0",
19
19
  "@mui/icons-material": ">=7.0.0",
20
- "react": ">=18.2.0",
21
- "react-dom": ">=18.2.0",
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.21.25",
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.15.0",
35
- "@mui/icons-material": "9.0.1",
36
- "@types/react": "19.2.15",
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.6",
40
- "react-dom": "19.2.6",
39
+ "react": "19.2.7",
40
+ "react-dom": "19.2.7",
41
41
  "react-error-boundary": "6.1.2",
42
- "react-router": "7.16.0",
43
- "typescript": "6.0.3"
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 interface BoundLookupProps<
6
+ export type BoundLookupProps<
7
7
  TBoundData extends Record<string, unknown>,
8
8
  TLookupData extends Record<string, unknown>,
9
- > extends Omit<AfLookupProps<TLookupData>, "onChange"> {
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>,
@@ -14,7 +14,7 @@
14
14
 
15
15
  .ObCombobox-listWrap {
16
16
  overflow: hidden;
17
- height: 372px;
17
+ min-height: 0;
18
18
  border: 1px solid rgb(0 0 0 / 0.12);
19
19
  border-radius: 0.25rem;
20
20
  background-color: white;
@@ -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 { useDataWithFilter, useDebounce, useLoading } from "@olenbetong/appframe-react";
6
+ import { type LookupColumn, useComboboxData } from "@olenbetong/appframe-react";
7
7
 
8
8
  import clsx from "clsx";
9
- import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
9
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
10
10
  import { VariableSizeList as VirtualList } from "react-window";
11
11
 
12
- export interface ComboboxProps<T extends Record<string, unknown>> {
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 function Combobox<T extends Record<string, unknown>>({
24
- nullable,
25
- dataObject,
26
- isItemSelected,
27
- uniqueIdField = "PrimKey",
28
- renderItem: Component,
29
- onChange,
30
- onClose,
31
- }: ComboboxProps<T>) {
32
- let id = useId();
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 [currentIndex, setCurrentIndex] = useState<number>(0);
45
- let listRef = useRef<VirtualList>(null);
46
- let [inputValue, setInputValue] = useState(() => {
47
- // Preserve filter even if the lookup is unmounted
48
- let currentFilter = dataObject.getParameter("filterString") ?? "";
49
- let filters = currentFilter.split(/\s+AND\s+/g);
50
- let wordFilters = filters.filter((filter) => filter.includes("[SearchColumn]"));
51
- let words = wordFilters.map((filter) => filter.match(/'%([^']*)%'/)?.[1] ?? "").filter(Boolean);
52
-
53
- return words.join(" ");
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
- query
61
- .split(/\s+/g)
62
- .map((word) => `[SearchColumn] LIKE '%${word}%'`)
63
- .join(" AND "),
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
- <Component
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
- // biome-ignore lint/a11y/useKeyWithClickEvents: element is not interactive, click is used to prevent dialog close
180
- // biome-ignore lint/a11y/noStaticElementInteractions: element is not interactive, click is used to prevent dialog close
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
- <div className="ObCombobox-list" role="listbox">
218
- <VirtualList
219
- ref={listRef}
175
+ {height > 0 &&
176
+ (columns ? (
177
+ <LookupGrid<T>
178
+ columns={columns}
179
+ data={data}
220
180
  height={height}
221
- width="100%"
222
- itemCount={data.length}
223
- itemSize={getItemSize}
224
- overscanCount={5}
225
- >
226
- {Row}
227
- </VirtualList>
228
- </div>
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
  );
@@ -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