@olenbetong/appframe-ds 0.2.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 (117) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/es/AfLookup/BoundLookup.d.ts +10 -0
  3. package/es/AfLookup/BoundLookup.js +16 -0
  4. package/es/AfLookup/Combobox.css +78 -0
  5. package/es/AfLookup/Combobox.d.ts +31 -0
  6. package/es/AfLookup/Combobox.js +71 -0
  7. package/es/AfLookup/Lookup.css +125 -0
  8. package/es/AfLookup/Lookup.d.ts +35 -0
  9. package/es/AfLookup/Lookup.js +42 -0
  10. package/es/AfLookup/LookupEdit.d.ts +21 -0
  11. package/es/AfLookup/LookupEdit.js +49 -0
  12. package/es/AfLookup/LookupGrid.css +57 -0
  13. package/es/AfLookup/LookupGrid.d.ts +13 -0
  14. package/es/AfLookup/LookupGrid.js +27 -0
  15. package/es/AfLookup/index.d.ts +5 -0
  16. package/es/AfLookup/index.js +5 -0
  17. package/es/binding/BoundCheckbox.d.ts +9 -0
  18. package/es/binding/BoundCheckbox.js +13 -0
  19. package/es/binding/BoundDatePicker.d.ts +7 -0
  20. package/es/binding/BoundDatePicker.js +12 -0
  21. package/es/binding/BoundDateTimePicker.d.ts +7 -0
  22. package/es/binding/BoundDateTimePicker.js +14 -0
  23. package/es/binding/BoundInput.d.ts +6 -0
  24. package/es/binding/BoundInput.js +12 -0
  25. package/es/binding/BoundNumericTextField.d.ts +20 -0
  26. package/es/binding/BoundNumericTextField.js +58 -0
  27. package/es/binding/BoundSelect.d.ts +9 -0
  28. package/es/binding/BoundSelect.js +13 -0
  29. package/es/binding/BoundSwitch.d.ts +9 -0
  30. package/es/binding/BoundSwitch.js +13 -0
  31. package/es/binding/BoundTextField.d.ts +7 -0
  32. package/es/binding/BoundTextField.js +22 -0
  33. package/es/binding/CancelButton.d.ts +3 -0
  34. package/es/binding/CancelButton.js +14 -0
  35. package/es/binding/CancelIconButton.d.ts +2 -0
  36. package/es/binding/CancelIconButton.js +15 -0
  37. package/es/binding/DataEditToolbar.d.ts +13 -0
  38. package/es/binding/DataEditToolbar.js +28 -0
  39. package/es/binding/DataObjectSelect.d.ts +50 -0
  40. package/es/binding/DataObjectSelect.js +26 -0
  41. package/es/binding/DeleteButton.d.ts +8 -0
  42. package/es/binding/DeleteButton.js +14 -0
  43. package/es/binding/DeleteIconButton.d.ts +10 -0
  44. package/es/binding/DeleteIconButton.js +15 -0
  45. package/es/binding/RefreshButton.d.ts +3 -0
  46. package/es/binding/RefreshButton.js +14 -0
  47. package/es/binding/RefreshIconButton.d.ts +2 -0
  48. package/es/binding/RefreshIconButton.js +16 -0
  49. package/es/binding/RefreshRowIconButton.d.ts +7 -0
  50. package/es/binding/RefreshRowIconButton.js +15 -0
  51. package/es/binding/SaveButton.d.ts +3 -0
  52. package/es/binding/SaveButton.js +14 -0
  53. package/es/binding/SaveIconButton.d.ts +2 -0
  54. package/es/binding/SaveIconButton.js +15 -0
  55. package/es/binding/index.d.ts +19 -0
  56. package/es/binding/index.js +19 -0
  57. package/es/index.d.ts +3 -0
  58. package/es/index.js +3 -0
  59. package/es/layout/AppLayoutDesktop.css +65 -0
  60. package/es/layout/AppLayoutDesktop.d.ts +3 -0
  61. package/es/layout/AppLayoutDesktop.js +19 -0
  62. package/es/layout/AppLayoutMobile.css +75 -0
  63. package/es/layout/AppLayoutMobile.d.ts +3 -0
  64. package/es/layout/AppLayoutMobile.js +18 -0
  65. package/es/layout/AppMenu.css +67 -0
  66. package/es/layout/AppMenu.d.ts +8 -0
  67. package/es/layout/AppMenu.js +36 -0
  68. package/es/layout/AppMenuItem.css +97 -0
  69. package/es/layout/AppMenuItem.d.ts +50 -0
  70. package/es/layout/AppMenuItem.js +14 -0
  71. package/es/layout/AppMenuSubheader.css +18 -0
  72. package/es/layout/AppMenuSubheader.d.ts +4 -0
  73. package/es/layout/AppMenuSubheader.js +6 -0
  74. package/es/layout/ErrorBoundary.css +101 -0
  75. package/es/layout/ErrorBoundary.d.ts +30 -0
  76. package/es/layout/ErrorBoundary.js +81 -0
  77. package/es/layout/PageContainer.css +29 -0
  78. package/es/layout/PageContainer.d.ts +7 -0
  79. package/es/layout/PageContainer.js +6 -0
  80. package/es/layout/PartnerToggle.d.ts +5 -0
  81. package/es/layout/PartnerToggle.js +9 -0
  82. package/es/layout/constants.d.ts +2 -0
  83. package/es/layout/constants.js +2 -0
  84. package/es/layout/index.css +65 -0
  85. package/es/layout/index.d.ts +33 -0
  86. package/es/layout/index.js +31 -0
  87. package/package.json +17 -12
  88. package/src/AfLookup/BoundLookup.tsx +4 -4
  89. package/src/AfLookup/Combobox.css +1 -1
  90. package/src/AfLookup/Combobox.tsx +79 -108
  91. package/src/AfLookup/Lookup.css +21 -0
  92. package/src/AfLookup/Lookup.tsx +25 -157
  93. package/src/AfLookup/LookupEdit.tsx +131 -0
  94. package/src/AfLookup/LookupGrid.css +57 -0
  95. package/src/AfLookup/LookupGrid.tsx +98 -0
  96. package/src/AfLookup/index.ts +2 -0
  97. package/src/binding/DataObjectSelect.tsx +1 -1
  98. package/src/index.ts +1 -0
  99. package/src/layout/AppLayoutDesktop.css +65 -0
  100. package/src/layout/AppLayoutDesktop.tsx +45 -0
  101. package/src/layout/AppLayoutMobile.css +75 -0
  102. package/src/layout/AppLayoutMobile.tsx +50 -0
  103. package/src/layout/AppMenu.css +67 -0
  104. package/src/layout/AppMenu.tsx +75 -0
  105. package/src/layout/AppMenuItem.css +97 -0
  106. package/src/layout/AppMenuItem.tsx +80 -0
  107. package/src/layout/AppMenuSubheader.css +18 -0
  108. package/src/layout/AppMenuSubheader.tsx +14 -0
  109. package/src/layout/ErrorBoundary.css +101 -0
  110. package/src/layout/ErrorBoundary.tsx +162 -0
  111. package/src/layout/PageContainer.css +29 -0
  112. package/src/layout/PageContainer.tsx +19 -0
  113. package/src/layout/PartnerToggle.tsx +23 -0
  114. package/src/layout/constants.ts +2 -0
  115. package/src/layout/index.css +65 -0
  116. package/src/layout/index.tsx +62 -0
  117. package/tsconfig.build.tsbuildinfo +1 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @olenbetong/appframe-ds
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 46177d7: Add grid variant support to AfLookup and extract shared lookup logic.
8
+ - `@olenbetong/appframe-react`: new `useLookupState` and `useComboboxData` hooks containing the shared non-UI lookup logic (dialog state, anchor positioning, search filtering, keyboard navigation), plus the `LookupColumn` and `DistributiveOmit` types.
9
+ - `@olenbetong/appframe-mui` / `@olenbetong/appframe-ds`: the lookup popup now supports `variant="grid"` with a `columns` definition (`field`, `headerName`, `width`, `renderCell`). Grid keyboard navigation is cell-by-cell (tracks current row and column) and works while focus is in the search field.
10
+ - `@olenbetong/appframe-ds`: new `AfLookupEdit` component — an editable lookup input where the popup selects a single value (the item's `lookupField`) instead of an entire row, so typing and selecting behave the same. The existing `AfLookupEdit` in `@olenbetong/appframe-mui` now shares the same lookup state logic and supports the grid variant.
11
+
12
+ Fixes:
13
+ - The lookup dialog no longer scrolls its own content (`overflow: hidden` on `.ObLookup-dialog`), and the popup list/grid now grows and shrinks vertically when the dialog is resized (the list wrapper previously had a fixed height).
14
+ - The search field in the `appframe-ds` lookup no longer renders a label above the input; it uses an accessible label and placeholder instead.
15
+
16
+ ## 0.3.0
17
+
18
+ ### Minor Changes
19
+
20
+ - 8487660: Add `AppLayout`, `AppMenu`, `AppMenuItem`, `AppMenuItemExternalLink`, `AppMenuItemButton`, `AppMenuSubheader` layout components — ported from `appframe-mui` with all MUI dependencies removed and replaced with plain HTML + Designsystemet CSS tokens.
21
+
3
22
  ## 0.2.0
4
23
 
5
24
  ### Minor Changes
@@ -0,0 +1,10 @@
1
+ import type { CurrentRow, DataObject } from "@olenbetong/appframe-data";
2
+ import { type DistributiveOmit } from "@olenbetong/appframe-react";
3
+ import { type AfLookupProps } from "./Lookup.js";
4
+ export type BoundLookupProps<TBoundData extends Record<string, unknown>, TLookupData extends Record<string, unknown>> = DistributiveOmit<AfLookupProps<TLookupData>, "onChange"> & {
5
+ dataObject: DataObject<TLookupData>;
6
+ displayField?: keyof TBoundData;
7
+ getChanges: (selectedItem: TLookupData | null) => Partial<TBoundData>;
8
+ getDisplayValue?: (currentRow: CurrentRow<TBoundData>) => string;
9
+ };
10
+ export declare function BoundLookup<TBoundData extends Record<string, unknown>, TLookupData extends Record<string, unknown> = any>({ dataObject, displayField, getChanges, getDisplayValue, ...props }: BoundLookupProps<TBoundData, TLookupData>): import("react").JSX.Element;
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCurrentRow, useDataObject } from "@olenbetong/appframe-react";
3
+ import { AfLookup } from "./Lookup.js";
4
+ export function BoundLookup({ dataObject, displayField, getChanges, getDisplayValue, ...props }) {
5
+ let boundDataObject = useDataObject();
6
+ let row = useCurrentRow(boundDataObject);
7
+ function handleChange(item) {
8
+ let changes = getChanges(item);
9
+ for (let key in changes) {
10
+ let newValue = changes[key];
11
+ boundDataObject.currentRow(key, newValue);
12
+ }
13
+ }
14
+ let displayValue = getDisplayValue ? getDisplayValue(row) : displayField ? String(row[displayField] ?? "") : "";
15
+ return _jsx(AfLookup, { dataObject: dataObject, value: displayValue, onChange: handleChange, ...props });
16
+ }
@@ -0,0 +1,78 @@
1
+ .ObCombobox-root {
2
+ height: 100%;
3
+ display: grid;
4
+ grid-template-rows: min-content auto 1fr;
5
+ gap: 0.5rem;
6
+
7
+ overscroll-behavior: contain;
8
+ }
9
+
10
+ .ObCombobox-actions {
11
+ display: flex;
12
+ justify-content: space-between;
13
+ }
14
+
15
+ .ObCombobox-listWrap {
16
+ overflow: hidden;
17
+ min-height: 0;
18
+ border: 1px solid rgb(0 0 0 / 0.12);
19
+ border-radius: 0.25rem;
20
+ background-color: white;
21
+ }
22
+
23
+ .ObCombobox-list {
24
+ border: 1px solid rgb(0 0 0 / 0.06);
25
+ height: 100%;
26
+ overflow-y: hidden;
27
+ overscroll-behavior: contain;
28
+ list-style: none;
29
+ margin: 0;
30
+ padding: 0;
31
+ }
32
+
33
+ .ObCombobox-input {
34
+ background-color: white;
35
+ }
36
+
37
+ .ObCombobox-listItem {
38
+ --active-color: oklch(from var(--brand-secondary, #999) l c h / 50%);
39
+ cursor: pointer;
40
+
41
+ &.active {
42
+ background-color: var(--active-color);
43
+ }
44
+
45
+ &:hover {
46
+ background-color: rgb(0 0 0 / 0.06);
47
+ }
48
+
49
+ &.active:hover {
50
+ background-color: oklch(from var(--active-color) calc(l + 0.1) calc(c - 0.04) h);
51
+ }
52
+ }
53
+
54
+ .ObCombobox-progress {
55
+ position: relative;
56
+ height: 4px;
57
+ overflow: hidden;
58
+ background-color: rgb(0 0 0 / 0.08);
59
+ }
60
+
61
+ .ObCombobox-progress-bar {
62
+ position: absolute;
63
+ top: 0;
64
+ left: -100%;
65
+ height: 100%;
66
+ width: 50%;
67
+ background-color: var(--ds-color-accent-base-default, #0062ba);
68
+ animation: ObCombobox-progress-anim 1.5s ease-in-out infinite;
69
+ }
70
+
71
+ @keyframes ObCombobox-progress-anim {
72
+ 0% {
73
+ left: -100%;
74
+ }
75
+ 100% {
76
+ left: 200%;
77
+ }
78
+ }
@@ -0,0 +1,31 @@
1
+ import "./Combobox.css";
2
+ import type { DataObject } from "@olenbetong/appframe-data";
3
+ import { type LookupColumn } from "@olenbetong/appframe-react";
4
+ export interface ComboboxBaseProps<T extends Record<string, unknown>> {
5
+ nullable?: boolean;
6
+ dataObject: DataObject<T>;
7
+ uniqueIdField?: string;
8
+ isItemSelected?: (item: T) => boolean;
9
+ onChange?: (item: T | null) => void;
10
+ value?: T;
11
+ onClose?: () => void;
12
+ }
13
+ export interface ComboboxListProps<T extends Record<string, unknown>> extends ComboboxBaseProps<T> {
14
+ /**
15
+ * Renders the popup content as a list (default).
16
+ */
17
+ variant?: "list";
18
+ renderItem: (props: React.ComponentPropsWithRef<"li"> & {
19
+ item: T;
20
+ }) => React.ReactNode;
21
+ }
22
+ export interface ComboboxGridProps<T extends Record<string, unknown>> extends ComboboxBaseProps<T> {
23
+ /**
24
+ * Renders the popup content as a grid with a header row and one column per
25
+ * entry in `columns`.
26
+ */
27
+ variant: "grid";
28
+ columns: LookupColumn<T>[];
29
+ }
30
+ export type ComboboxProps<T extends Record<string, unknown>> = ComboboxListProps<T> | ComboboxGridProps<T>;
31
+ export declare function Combobox<T extends Record<string, unknown>>(props: ComboboxProps<T>): import("react").JSX.Element;
@@ -0,0 +1,71 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import "./Combobox.css";
3
+ import { Button, Textfield } from "@digdir/designsystemet-react";
4
+ import { getLocalizedString } from "@olenbetong/appframe-core";
5
+ import { useComboboxData } from "@olenbetong/appframe-react";
6
+ import clsx from "clsx";
7
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
8
+ import { VariableSizeList as VirtualList } from "react-window";
9
+ import { LookupGrid } from "./LookupGrid.js";
10
+ export function Combobox(props) {
11
+ let { nullable, dataObject, isItemSelected, uniqueIdField = "PrimKey", onChange, onClose } = props;
12
+ let isGrid = props.variant === "grid";
13
+ let columns = isGrid ? props.columns : undefined;
14
+ let listRef = useRef(null);
15
+ let [isDesktop, setIsDesktop] = useState(() => typeof window !== "undefined" && window.matchMedia("(width >= 768px) and (pointer: fine)").matches);
16
+ useEffect(() => {
17
+ let mq = window.matchMedia("(width >= 768px) and (pointer: fine)");
18
+ let handler = (e) => setIsDesktop(e.matches);
19
+ mq.addEventListener("change", handler);
20
+ return () => mq.removeEventListener("change", handler);
21
+ }, []);
22
+ let { id, data, loading, inputValue, setInputValue, currentIndex, currentColumn, getItemId, handleSelectItem, handleKeyDown, } = useComboboxData({
23
+ dataObject,
24
+ uniqueIdField,
25
+ columnCount: columns?.length ?? 1,
26
+ onChange,
27
+ });
28
+ let wrapperRef = useRef(null);
29
+ let [height, setHeight] = useState(0);
30
+ useLayoutEffect(() => {
31
+ if (!wrapperRef.current)
32
+ return;
33
+ let resizeObserver = new ResizeObserver(() => {
34
+ let newHeight = wrapperRef.current.offsetHeight;
35
+ if (newHeight > 0)
36
+ setHeight(newHeight);
37
+ });
38
+ resizeObserver.observe(wrapperRef.current);
39
+ return () => resizeObserver.disconnect();
40
+ }, []);
41
+ let sizeMap = useRef(new Map());
42
+ let getItemSize = (index) => sizeMap.current.get(index) ?? 56;
43
+ let setSize = useCallback((index, size) => {
44
+ if (sizeMap.current.get(index) !== size) {
45
+ sizeMap.current.set(index, size);
46
+ // listRef will not be set on the initial render, so delay resetting
47
+ setTimeout(() => listRef.current?.resetAfterIndex(index, true));
48
+ }
49
+ }, []);
50
+ let Row = ({ index, style: { height, ...style } }) => {
51
+ let rowRef = useRef(null);
52
+ useLayoutEffect(() => {
53
+ if (!rowRef.current)
54
+ return;
55
+ setSize(index, rowRef.current.offsetHeight);
56
+ }, [index]);
57
+ let item = data[index];
58
+ let itemId = getItemId(item);
59
+ let active = index === currentIndex;
60
+ let ItemComponent = props.renderItem;
61
+ return (_jsx(ItemComponent, { className: clsx("ObCombobox-listItem", active && "active", isItemSelected?.(item) && "selected"), "aria-selected": active, onClick: () => handleSelectItem(item), item: item, style: {
62
+ ...style,
63
+ padding: isDesktop ? "0 8px" : "8px",
64
+ }, ref: rowRef, id: itemId }, itemId));
65
+ };
66
+ useEffect(() => listRef.current?.scrollToItem(currentIndex, "smart"), [currentIndex]);
67
+ return (_jsxs("div", { className: "ObCombobox-root", id: id, onClick: (evt) => evt.stopPropagation(), children: [_jsxs("div", { className: "ObCombobox-actions", children: [nullable && (_jsx(Button, { className: "ObCombobox-clear", "data-size": isDesktop ? "sm" : "md", variant: "tertiary", onClick: () => {
68
+ onChange?.(null);
69
+ }, children: getLocalizedString("Clear") })), onClose && (_jsx(Button, { "data-size": isDesktop ? "sm" : "md", variant: "tertiary", className: "ObCombobox-close", onClick: onClose, children: getLocalizedString("Close") }))] }), _jsx(Textfield, { type: "search", className: "ObCombobox-input", "aria-label": getLocalizedString("Search"), placeholder: getLocalizedString("Search"), onKeyDown: handleKeyDown, "aria-activedescendant": getItemId(data[currentIndex]) ?? "", value: inputValue, onChange: (event) => setInputValue(event.target.value) }), _jsxs("div", { className: "ObCombobox-listWrap", ref: wrapperRef, children: [loading && (_jsx("div", { className: "ObCombobox-progress", children: _jsx("div", { className: "ObCombobox-progress-bar" }) })), height > 0 &&
70
+ (columns ? (_jsx(LookupGrid, { columns: columns, data: data, height: height, currentIndex: currentIndex, currentColumn: currentColumn, getItemId: getItemId, isItemSelected: isItemSelected, onSelectItem: handleSelectItem })) : (_jsx("div", { className: "ObCombobox-list", role: "listbox", children: _jsx(VirtualList, { ref: listRef, height: height, width: "100%", itemCount: data.length, itemSize: getItemSize, overscanCount: 5, children: Row }) })))] })] }));
71
+ }
@@ -0,0 +1,125 @@
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
+
21
+ .ObLookup-dialog {
22
+ border: 0;
23
+ margin-block-start: 0;
24
+ padding: 1rem;
25
+ overflow: hidden;
26
+
27
+ background-color: whitesmoke;
28
+
29
+ transition:
30
+ display 0.2s allow-discrete,
31
+ transform 0.2s ease-out,
32
+ scale 0.2s ease-out,
33
+ opacity 0.2s ease-out;
34
+
35
+ &::backdrop {
36
+ opacity: 0;
37
+ background: rgb(0 0 0 / 0.2);
38
+ transition:
39
+ display 0.2s allow-discrete,
40
+ opacity 0.2s ease-out;
41
+ }
42
+
43
+ &[open]::backdrop {
44
+ opacity: 1;
45
+
46
+ @starting-style {
47
+ opacity: 0;
48
+ }
49
+ }
50
+
51
+ @media (width < 768px) {
52
+ inset: 10vh 0.5rem 0;
53
+
54
+ border-radius: 1rem 1rem 0 0;
55
+ height: 90vh;
56
+ width: calc(100vw - 1rem);
57
+ max-width: 100vw;
58
+ transform: translateY(100%);
59
+ z-index: 1500;
60
+
61
+ opacity: 0;
62
+
63
+ &[open] {
64
+ transform: translateY(0);
65
+ opacity: 1;
66
+
67
+ @starting-style {
68
+ transform: translateY(100%);
69
+ opacity: 0;
70
+ }
71
+ }
72
+ }
73
+
74
+ @media (width >= 768px) {
75
+ inset-block-start: 50%;
76
+ inset-inline-start: 50%;
77
+ translate: -50% -50%;
78
+ scale: 1 0;
79
+ opacity: 0;
80
+
81
+ border-radius: 0.25rem;
82
+
83
+ height: clamp(300px, 80vh, 500px);
84
+ min-width: 320px;
85
+ width: var(--default-width, 400px);
86
+
87
+ &[open] {
88
+ scale: 1;
89
+ opacity: 1;
90
+
91
+ @starting-style {
92
+ scale: 1 0;
93
+ opacity: 0;
94
+ }
95
+ }
96
+ }
97
+
98
+ @supports (inset-block-start: anchor(bottom)) {
99
+ @media (width < 768px) or (pointer: coarse) {
100
+ anchor-name: unset !important;
101
+ }
102
+
103
+ @media screen and (width >= 768px) and (pointer: fine) {
104
+ inset-block-start: anchor(bottom);
105
+ inset-block-end: unset;
106
+
107
+ inset-inline-start: anchor(left);
108
+ inset-inline-end: unset;
109
+
110
+ translate: unset;
111
+ transform-origin: top left;
112
+
113
+ resize: both;
114
+ }
115
+ }
116
+
117
+ &[data-anchor="manual"] {
118
+ inset: unset;
119
+ position: fixed;
120
+ inset-inline-start: var(--lookup-anchor-inline-start, 50%);
121
+ inset-block-start: var(--lookup-anchor-block-start, 50%);
122
+ translate: unset;
123
+ transform: none;
124
+ }
125
+ }
@@ -0,0 +1,35 @@
1
+ import "./Lookup.css";
2
+ import { type TextfieldProps } from "@digdir/designsystemet-react";
3
+ import { type DistributiveOmit } from "@olenbetong/appframe-react";
4
+ import { type ComboboxProps } from "./Combobox.js";
5
+ export type AfLookupProps<T extends Record<string, unknown>> = DistributiveOmit<ComboboxProps<T>, "value"> & {
6
+ /**
7
+ * Allows the user to enter a value manually that might not be in the list.
8
+ */
9
+ editable?: boolean;
10
+ fullWidth?: boolean;
11
+ label?: string;
12
+ value?: string | null;
13
+ onChange?: (item: T | null) => void;
14
+ slotProps?: {
15
+ dialog?: Partial<React.HTMLProps<HTMLDialogElement>>;
16
+ textField?: Partial<TextfieldProps>;
17
+ };
18
+ };
19
+ export type LookupProps<T extends Record<string, unknown>> = DistributiveOmit<ComboboxProps<T>, "value"> & {
20
+ slotProps?: {
21
+ dialog?: Partial<React.HTMLProps<HTMLDialogElement>>;
22
+ };
23
+ getAnchorElement?: () => Element | null;
24
+ };
25
+ export declare function useLookup<T extends Record<string, unknown>>({ getAnchorElement, slotProps, ...props }: LookupProps<T>): {
26
+ open: boolean;
27
+ openDialog: () => void;
28
+ onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => void;
29
+ onChange: (item: T | null) => void;
30
+ dialogRef: import("react").RefObject<HTMLDialogElement | null>;
31
+ anchorName: string;
32
+ inputId: string;
33
+ dialog: import("react").ReactPortal;
34
+ };
35
+ export declare function AfLookup<T extends Record<string, unknown>>({ fullWidth, label, slotProps, value, ...props }: AfLookupProps<T>): import("react").JSX.Element;
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import "./Lookup.css";
3
+ import { Button, Textfield } from "@digdir/designsystemet-react";
4
+ import ClearIcon from "@mui/icons-material/Close";
5
+ import SearchIcon from "@mui/icons-material/Search";
6
+ import { getLocalizedString } from "@olenbetong/appframe-core";
7
+ import { useLookupState } from "@olenbetong/appframe-react";
8
+ import clsx from "clsx";
9
+ import { createPortal } from "react-dom";
10
+ import { Combobox } from "./Combobox.js";
11
+ export function useLookup({ getAnchorElement, slotProps, ...props }) {
12
+ let { open, openDialog, onKeyDown, dialogRef, anchorName, inputId } = useLookupState({
13
+ getAnchorElement,
14
+ scrollParentSelector: "[data-virtualized-scroller]",
15
+ });
16
+ function handleChange(item) {
17
+ props.onChange?.(item);
18
+ dialogRef.current?.close();
19
+ }
20
+ return {
21
+ open,
22
+ openDialog,
23
+ onKeyDown,
24
+ onChange: handleChange,
25
+ dialogRef,
26
+ anchorName,
27
+ inputId,
28
+ dialog: createPortal(_jsx("dialog", { ref: dialogRef, className: clsx("ObLookup-dialog", slotProps?.dialog?.className), onKeyDown: (evt) => evt.stopPropagation(), style: { positionAnchor: anchorName, ...slotProps?.dialog?.style }, children: open && _jsx(Combobox, { ...props, onClose: () => dialogRef.current?.close(), onChange: handleChange }) }), document.body, inputId),
29
+ };
30
+ }
31
+ export function AfLookup({ fullWidth = false, label, slotProps, value, ...props }) {
32
+ let { inputId, open: _open, dialog, anchorName, openDialog, onKeyDown } = useLookup(props);
33
+ let hasClear = !!(props.nullable && value && props.onChange);
34
+ return (_jsxs("div", { style: { anchorName }, children: [_jsxs("div", { className: "ObLookup-inputWrap", style: {
35
+ display: fullWidth ? "block" : "inline-block",
36
+ "--ObLookup-buttons-width": hasClear ? "5.5rem" : "3rem",
37
+ }, children: [_jsx(Textfield, { id: inputId, value: value ?? "", label: label, readOnly: true, onClick: openDialog, onKeyDown: onKeyDown, className: clsx(slotProps?.textField?.className, "ObLookup-input"), ...slotProps?.textField, style: { width: fullWidth ? "100%" : undefined, ...slotProps?.textField?.style } }), _jsxs("div", { className: "ObLookup-buttons", children: [hasClear && (_jsx(Button, { icon: true, variant: "tertiary", "data-size": "sm", "aria-label": getLocalizedString("Clear"), onKeyDown: (evt) => evt.stopPropagation(), onClick: (evt) => {
38
+ evt.stopPropagation();
39
+ props.onChange?.(null);
40
+ document.getElementById(inputId)?.focus();
41
+ }, children: _jsx(ClearIcon, {}) })), _jsx(Button, { icon: true, variant: "tertiary", "data-size": "sm", "aria-label": getLocalizedString("Search"), onClick: openDialog, children: _jsx(SearchIcon, {}) })] })] }), dialog] }));
42
+ }
@@ -0,0 +1,21 @@
1
+ import "./Lookup.css";
2
+ import { type TextfieldProps } from "@digdir/designsystemet-react";
3
+ import type { DistributiveOmit } from "@olenbetong/appframe-react";
4
+ import type React from "react";
5
+ import type { ComboboxProps } from "./Combobox.js";
6
+ export type AfLookupEditProps<T extends Record<string, unknown>> = DistributiveOmit<ComboboxProps<T>, "value" | "onChange"> & {
7
+ /**
8
+ * Allows the user to enter a value manually that might not be in the list.
9
+ */
10
+ editable?: boolean;
11
+ fullWidth?: boolean;
12
+ label?: string;
13
+ value?: string | null;
14
+ lookupField: keyof T;
15
+ onChange?: (evt: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
16
+ slotProps?: {
17
+ dialog?: Partial<React.HTMLProps<HTMLDialogElement>>;
18
+ textField?: Partial<TextfieldProps>;
19
+ };
20
+ };
21
+ export declare function AfLookupEdit<T extends Record<string, unknown>>({ fullWidth, label, onChange, slotProps, nullable, lookupField, value, ...props }: AfLookupEditProps<T>): React.JSX.Element;
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import "./Lookup.css";
3
+ import { Button, Textfield } from "@digdir/designsystemet-react";
4
+ import ClearIcon from "@mui/icons-material/Close";
5
+ import SearchIcon from "@mui/icons-material/Search";
6
+ import { getLocalizedString } from "@olenbetong/appframe-core";
7
+ import clsx from "clsx";
8
+ import { useLookup } from "./Lookup.js";
9
+ export function AfLookupEdit({ fullWidth = false, label, onChange, slotProps, nullable, lookupField, value, ...props }) {
10
+ let { inputId, dialog, anchorName, openDialog, onKeyDown } = useLookup({
11
+ ...props,
12
+ nullable,
13
+ slotProps: { dialog: slotProps?.dialog },
14
+ onChange: (item) => applyValue(item),
15
+ });
16
+ // The popup selects a single value from the dataset: the item's lookupField
17
+ // is written to the input through the native value setter so the same
18
+ // change event fires whether the user types or picks from the list/grid.
19
+ function applyValue(item) {
20
+ let input = document.getElementById(inputId);
21
+ let nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
22
+ if (input && nativeInputValueSetter) {
23
+ nativeInputValueSetter.call(input, item ? String(item[lookupField]) : "");
24
+ input.dispatchEvent(new Event("input", { bubbles: true }));
25
+ }
26
+ }
27
+ function handleKeyDown(event) {
28
+ if (event.key === "Enter") {
29
+ event.preventDefault();
30
+ event.stopPropagation();
31
+ openDialog();
32
+ }
33
+ else {
34
+ onKeyDown(event);
35
+ }
36
+ }
37
+ let hasClear = !!(nullable && value && onChange);
38
+ return (_jsxs("div", { style: { anchorName }, children: [_jsxs("div", { className: "ObLookup-inputWrap", style: {
39
+ display: fullWidth ? "block" : "inline-block",
40
+ "--ObLookup-buttons-width": hasClear ? "5.5rem" : "3rem",
41
+ }, children: [_jsx(Textfield, { id: inputId, value: value ?? "", label: label, onChange: onChange, onKeyDown: handleKeyDown, className: clsx(slotProps?.textField?.className, "ObLookup-input"), ...slotProps?.textField, style: { width: fullWidth ? "100%" : undefined, ...slotProps?.textField?.style } }), _jsxs("div", { className: "ObLookup-buttons", children: [hasClear && (_jsx(Button, { icon: true, variant: "tertiary", "data-size": "sm", "aria-label": getLocalizedString("Clear"), onKeyDown: (evt) => evt.stopPropagation(), onClick: (evt) => {
42
+ evt.stopPropagation();
43
+ applyValue(null);
44
+ document.getElementById(inputId)?.focus();
45
+ }, children: _jsx(ClearIcon, {}) })), _jsx(Button, { icon: true, variant: "tertiary", "data-size": "sm", "aria-label": getLocalizedString("Search"), onClick: (evt) => {
46
+ evt.stopPropagation();
47
+ openDialog();
48
+ }, children: _jsx(SearchIcon, {}) })] })] }), dialog] }));
49
+ }
@@ -0,0 +1,57 @@
1
+ .ObLookupGrid-root {
2
+ height: 100%;
3
+ overflow: hidden;
4
+ overscroll-behavior: contain;
5
+ border: 1px solid rgb(0 0 0 / 0.06);
6
+ }
7
+
8
+ .ObLookupGrid-header {
9
+ display: grid;
10
+ height: 40px;
11
+ align-items: center;
12
+ border-bottom: 1px solid rgb(0 0 0 / 0.12);
13
+ background-color: rgb(0 0 0 / 0.03);
14
+ font-weight: 600;
15
+ }
16
+
17
+ .ObLookupGrid-headerCell {
18
+ padding: 0 8px;
19
+ overflow: hidden;
20
+ text-overflow: ellipsis;
21
+ white-space: nowrap;
22
+ }
23
+
24
+ .ObLookupGrid-row {
25
+ --active-color: oklch(from var(--brand-secondary, #999) l c h / 50%);
26
+ display: grid;
27
+ align-items: center;
28
+ cursor: pointer;
29
+ border-bottom: 1px solid rgb(0 0 0 / 0.06);
30
+
31
+ &.active {
32
+ background-color: var(--active-color);
33
+ }
34
+
35
+ &:hover {
36
+ background-color: rgb(0 0 0 / 0.06);
37
+ }
38
+
39
+ &.active:hover {
40
+ background-color: oklch(from var(--active-color) calc(l + 0.1) calc(c - 0.04) h);
41
+ }
42
+ }
43
+
44
+ .ObLookupGrid-cell {
45
+ padding: 0 8px;
46
+ height: 100%;
47
+ display: flex;
48
+ align-items: center;
49
+ overflow: hidden;
50
+ text-overflow: ellipsis;
51
+ white-space: nowrap;
52
+
53
+ &.active {
54
+ outline: 2px solid oklch(from var(--brand-secondary, #666) calc(l - 0.2) c h);
55
+ outline-offset: -2px;
56
+ }
57
+ }
@@ -0,0 +1,13 @@
1
+ import "./LookupGrid.css";
2
+ import type { LookupColumn } from "@olenbetong/appframe-react";
3
+ export interface LookupGridProps<T extends Record<string, unknown>> {
4
+ columns: LookupColumn<T>[];
5
+ data: T[];
6
+ height: number;
7
+ currentIndex: number;
8
+ currentColumn: number;
9
+ getItemId: (item: T | undefined) => string | undefined;
10
+ isItemSelected?: (item: T) => boolean;
11
+ onSelectItem: (item: T) => void;
12
+ }
13
+ export declare function LookupGrid<T extends Record<string, unknown>>({ columns, data, height, currentIndex, currentColumn, getItemId, isItemSelected, onSelectItem, }: LookupGridProps<T>): import("react").JSX.Element;
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./BoundLookup.js";
2
+ export * from "./Combobox.js";
3
+ export * from "./Lookup.js";
4
+ export * from "./LookupEdit.js";
5
+ export * from "./LookupGrid.js";
@@ -0,0 +1,5 @@
1
+ export * from "./BoundLookup.js";
2
+ export * from "./Combobox.js";
3
+ export * from "./Lookup.js";
4
+ export * from "./LookupEdit.js";
5
+ export * from "./LookupGrid.js";
@@ -0,0 +1,9 @@
1
+ import { type CheckboxProps } from "@digdir/designsystemet-react";
2
+ export type BoundCheckboxProps = Omit<CheckboxProps, "onChange" | "onKeyDown"> & {
3
+ field: string;
4
+ onChange?: (evt: React.ChangeEvent<HTMLInputElement>) => void;
5
+ };
6
+ export declare const BoundCheckbox: import("react").ForwardRefExoticComponent<Omit<CheckboxProps, "onChange" | "onKeyDown"> & {
7
+ field: string;
8
+ onChange?: (evt: React.ChangeEvent<HTMLInputElement>) => void;
9
+ } & import("react").RefAttributes<HTMLInputElement>>;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Checkbox } from "@digdir/designsystemet-react";
3
+ import { useField } from "@olenbetong/appframe-react";
4
+ import { forwardRef } from "react";
5
+ export const BoundCheckbox = forwardRef(function BoundCheckbox({ field, onChange: onChangeProp, checked: _checked, ...props }, ref) {
6
+ let { value, setValue, onKeyDown } = useField(field);
7
+ return (_jsx(Checkbox, { checked: value ?? false, onChange: (evt) => {
8
+ onChangeProp?.(evt);
9
+ if (!evt.defaultPrevented) {
10
+ setValue(evt.target.checked);
11
+ }
12
+ }, onKeyDown: (evt) => onKeyDown(evt), ref: ref, name: field, ...props }));
13
+ });
@@ -0,0 +1,7 @@
1
+ import { type TextfieldProps } from "@digdir/designsystemet-react";
2
+ export type BoundDatePickerProps = Omit<TextfieldProps, "value" | "onChange" | "onKeyDown" | "type"> & {
3
+ field: string;
4
+ };
5
+ export declare const BoundDatePicker: import("react").ForwardRefExoticComponent<Omit<TextfieldProps, "onChange" | "onKeyDown" | "type" | "value"> & {
6
+ field: string;
7
+ } & import("react").RefAttributes<HTMLInputElement>>;