@olenbetong/appframe-ds 0.4.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 +6 -0
- package/es/AfLookup/Combobox.d.ts +1 -0
- package/es/AfLookup/Combobox.js +6 -1
- package/es/AfLookup/Lookup.d.ts +1 -1
- package/es/AfLookup/Lookup.js +21 -4
- package/package.json +10 -10
- package/scripts/copyAssets.mjs +22 -0
- package/src/AfLookup/Combobox.tsx +16 -1
- package/src/AfLookup/Lookup.tsx +37 -6
- package/tsconfig.build.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @olenbetong/appframe-ds
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f7f6cea: AfLookup now supports typeahead from the input field: typing opens the lookup popup and forwards the typed character to the combobox search field.
|
|
8
|
+
|
|
3
9
|
## 0.4.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
|
@@ -5,6 +5,7 @@ export interface ComboboxBaseProps<T extends Record<string, unknown>> {
|
|
|
5
5
|
nullable?: boolean;
|
|
6
6
|
dataObject: DataObject<T>;
|
|
7
7
|
uniqueIdField?: string;
|
|
8
|
+
initialSearchValue?: string;
|
|
8
9
|
isItemSelected?: (item: T) => boolean;
|
|
9
10
|
onChange?: (item: T | null) => void;
|
|
10
11
|
value?: T;
|
package/es/AfLookup/Combobox.js
CHANGED
|
@@ -8,7 +8,7 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react
|
|
|
8
8
|
import { VariableSizeList as VirtualList } from "react-window";
|
|
9
9
|
import { LookupGrid } from "./LookupGrid.js";
|
|
10
10
|
export function Combobox(props) {
|
|
11
|
-
let { nullable, dataObject, isItemSelected, uniqueIdField = "PrimKey", onChange, onClose } = props;
|
|
11
|
+
let { nullable, dataObject, isItemSelected, uniqueIdField = "PrimKey", initialSearchValue, onChange, onClose, } = props;
|
|
12
12
|
let isGrid = props.variant === "grid";
|
|
13
13
|
let columns = isGrid ? props.columns : undefined;
|
|
14
14
|
let listRef = useRef(null);
|
|
@@ -64,6 +64,11 @@ export function Combobox(props) {
|
|
|
64
64
|
}, ref: rowRef, id: itemId }, itemId));
|
|
65
65
|
};
|
|
66
66
|
useEffect(() => listRef.current?.scrollToItem(currentIndex, "smart"), [currentIndex]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (initialSearchValue !== undefined) {
|
|
69
|
+
setInputValue(initialSearchValue);
|
|
70
|
+
}
|
|
71
|
+
}, [initialSearchValue, setInputValue]);
|
|
67
72
|
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
73
|
onChange?.(null);
|
|
69
74
|
}, 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 &&
|
package/es/AfLookup/Lookup.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export type LookupProps<T extends Record<string, unknown>> = DistributiveOmit<Co
|
|
|
24
24
|
};
|
|
25
25
|
export declare function useLookup<T extends Record<string, unknown>>({ getAnchorElement, slotProps, ...props }: LookupProps<T>): {
|
|
26
26
|
open: boolean;
|
|
27
|
-
openDialog: () => void;
|
|
27
|
+
openDialog: (searchValue?: string) => void;
|
|
28
28
|
onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => void;
|
|
29
29
|
onChange: (item: T | null) => void;
|
|
30
30
|
dialogRef: import("react").RefObject<HTMLDialogElement | null>;
|
package/es/AfLookup/Lookup.js
CHANGED
|
@@ -6,6 +6,7 @@ import SearchIcon from "@mui/icons-material/Search";
|
|
|
6
6
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
7
7
|
import { useLookupState } from "@olenbetong/appframe-react";
|
|
8
8
|
import clsx from "clsx";
|
|
9
|
+
import { useRef } from "react";
|
|
9
10
|
import { createPortal } from "react-dom";
|
|
10
11
|
import { Combobox } from "./Combobox.js";
|
|
11
12
|
export function useLookup({ getAnchorElement, slotProps, ...props }) {
|
|
@@ -13,30 +14,46 @@ export function useLookup({ getAnchorElement, slotProps, ...props }) {
|
|
|
13
14
|
getAnchorElement,
|
|
14
15
|
scrollParentSelector: "[data-virtualized-scroller]",
|
|
15
16
|
});
|
|
17
|
+
let initialSearchValueRef = useRef(undefined);
|
|
16
18
|
function handleChange(item) {
|
|
17
19
|
props.onChange?.(item);
|
|
18
20
|
dialogRef.current?.close();
|
|
19
21
|
}
|
|
22
|
+
function handleOpenDialog(searchValue) {
|
|
23
|
+
initialSearchValueRef.current = searchValue;
|
|
24
|
+
openDialog();
|
|
25
|
+
}
|
|
20
26
|
return {
|
|
21
27
|
open,
|
|
22
|
-
openDialog,
|
|
28
|
+
openDialog: handleOpenDialog,
|
|
23
29
|
onKeyDown,
|
|
24
30
|
onChange: handleChange,
|
|
25
31
|
dialogRef,
|
|
26
32
|
anchorName,
|
|
27
33
|
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),
|
|
34
|
+
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, initialSearchValue: initialSearchValueRef.current, onClose: () => dialogRef.current?.close(), onChange: handleChange })) }), document.body, inputId),
|
|
29
35
|
};
|
|
30
36
|
}
|
|
31
37
|
export function AfLookup({ fullWidth = false, label, slotProps, value, ...props }) {
|
|
32
38
|
let { inputId, open: _open, dialog, anchorName, openDialog, onKeyDown } = useLookup(props);
|
|
33
39
|
let hasClear = !!(props.nullable && value && props.onChange);
|
|
40
|
+
function handleInputKeyDown(event) {
|
|
41
|
+
let hasModifier = event.ctrlKey || event.altKey || event.metaKey;
|
|
42
|
+
let isTypeaheadKey = event.key.length === 1 && !hasModifier && !event.nativeEvent.isComposing;
|
|
43
|
+
if (isTypeaheadKey) {
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
event.stopPropagation();
|
|
46
|
+
openDialog(event.key);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
onKeyDown(event);
|
|
50
|
+
}
|
|
34
51
|
return (_jsxs("div", { style: { anchorName }, children: [_jsxs("div", { className: "ObLookup-inputWrap", style: {
|
|
35
52
|
display: fullWidth ? "block" : "inline-block",
|
|
36
53
|
"--ObLookup-buttons-width": hasClear ? "5.5rem" : "3rem",
|
|
37
|
-
}, children: [_jsx(Textfield, { id: inputId, value: value ?? "", label: label,
|
|
54
|
+
}, children: [_jsx(Textfield, { id: inputId, value: value ?? "", label: label, onClick: () => openDialog(), onKeyDown: handleInputKeyDown, 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
55
|
evt.stopPropagation();
|
|
39
56
|
props.onChange?.(null);
|
|
40
57
|
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] }));
|
|
58
|
+
}, children: _jsx(ClearIcon, {}) })), _jsx(Button, { icon: true, variant: "tertiary", "data-size": "sm", "aria-label": getLocalizedString("Search"), onClick: () => openDialog(), children: _jsx(SearchIcon, {}) })] })] }), dialog] }));
|
|
42
59
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olenbetong/appframe-ds",
|
|
3
|
-
"version": "0.4.
|
|
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",
|
|
@@ -17,15 +17,15 @@
|
|
|
17
17
|
"peerDependencies": {
|
|
18
18
|
"@digdir/designsystemet-react": ">=1.18.0",
|
|
19
19
|
"@mui/icons-material": ">=7.0.0",
|
|
20
|
-
"react": ">=19.2.
|
|
21
|
-
"react-dom": ">=19.2.
|
|
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"
|
|
@@ -36,16 +36,16 @@
|
|
|
36
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": "
|
|
42
|
+
"react-router": "8.3.0",
|
|
43
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
|
+
}
|
|
@@ -15,6 +15,7 @@ export interface ComboboxBaseProps<T extends Record<string, unknown>> {
|
|
|
15
15
|
nullable?: boolean;
|
|
16
16
|
dataObject: DataObject<T>;
|
|
17
17
|
uniqueIdField?: string;
|
|
18
|
+
initialSearchValue?: string;
|
|
18
19
|
isItemSelected?: (item: T) => boolean;
|
|
19
20
|
onChange?: (item: T | null) => void;
|
|
20
21
|
value?: T;
|
|
@@ -41,7 +42,15 @@ export interface ComboboxGridProps<T extends Record<string, unknown>> extends Co
|
|
|
41
42
|
export type ComboboxProps<T extends Record<string, unknown>> = ComboboxListProps<T> | ComboboxGridProps<T>;
|
|
42
43
|
|
|
43
44
|
export function Combobox<T extends Record<string, unknown>>(props: ComboboxProps<T>) {
|
|
44
|
-
let {
|
|
45
|
+
let {
|
|
46
|
+
nullable,
|
|
47
|
+
dataObject,
|
|
48
|
+
isItemSelected,
|
|
49
|
+
uniqueIdField = "PrimKey",
|
|
50
|
+
initialSearchValue,
|
|
51
|
+
onChange,
|
|
52
|
+
onClose,
|
|
53
|
+
} = props;
|
|
45
54
|
let isGrid = props.variant === "grid";
|
|
46
55
|
let columns = isGrid ? (props as ComboboxGridProps<T>).columns : undefined;
|
|
47
56
|
let listRef = useRef<VirtualList>(null);
|
|
@@ -133,6 +142,12 @@ export function Combobox<T extends Record<string, unknown>>(props: ComboboxProps
|
|
|
133
142
|
|
|
134
143
|
useEffect(() => listRef.current?.scrollToItem(currentIndex, "smart"), [currentIndex]);
|
|
135
144
|
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (initialSearchValue !== undefined) {
|
|
147
|
+
setInputValue(initialSearchValue);
|
|
148
|
+
}
|
|
149
|
+
}, [initialSearchValue, setInputValue]);
|
|
150
|
+
|
|
136
151
|
return (
|
|
137
152
|
// oxlint-disable-next-line jsx-a11y/click-events-have-key-events -- element is not interactive, click is used to prevent dialog close
|
|
138
153
|
// oxlint-disable-next-line jsx-a11y/no-static-element-interactions -- element is not interactive, click is used to prevent dialog close
|
package/src/AfLookup/Lookup.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
|
7
7
|
import { type DistributiveOmit, useLookupState } from "@olenbetong/appframe-react";
|
|
8
8
|
|
|
9
9
|
import clsx from "clsx";
|
|
10
|
+
import { useRef } from "react";
|
|
10
11
|
import { createPortal } from "react-dom";
|
|
11
12
|
|
|
12
13
|
import { Combobox, type ComboboxProps } from "./Combobox.js";
|
|
@@ -42,15 +43,21 @@ export function useLookup<T extends Record<string, unknown>>({
|
|
|
42
43
|
getAnchorElement,
|
|
43
44
|
scrollParentSelector: "[data-virtualized-scroller]",
|
|
44
45
|
});
|
|
46
|
+
let initialSearchValueRef = useRef<string | undefined>(undefined);
|
|
45
47
|
|
|
46
48
|
function handleChange(item: T | null) {
|
|
47
49
|
props.onChange?.(item);
|
|
48
50
|
dialogRef.current?.close();
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
function handleOpenDialog(searchValue?: string) {
|
|
54
|
+
initialSearchValueRef.current = searchValue;
|
|
55
|
+
openDialog();
|
|
56
|
+
}
|
|
57
|
+
|
|
51
58
|
return {
|
|
52
59
|
open,
|
|
53
|
-
openDialog,
|
|
60
|
+
openDialog: handleOpenDialog,
|
|
54
61
|
onKeyDown,
|
|
55
62
|
onChange: handleChange,
|
|
56
63
|
dialogRef,
|
|
@@ -63,7 +70,14 @@ export function useLookup<T extends Record<string, unknown>>({
|
|
|
63
70
|
onKeyDown={(evt) => evt.stopPropagation()}
|
|
64
71
|
style={{ positionAnchor: anchorName, ...slotProps?.dialog?.style } as React.CSSProperties}
|
|
65
72
|
>
|
|
66
|
-
{open &&
|
|
73
|
+
{open && (
|
|
74
|
+
<Combobox
|
|
75
|
+
{...props}
|
|
76
|
+
initialSearchValue={initialSearchValueRef.current}
|
|
77
|
+
onClose={() => dialogRef.current?.close()}
|
|
78
|
+
onChange={handleChange}
|
|
79
|
+
/>
|
|
80
|
+
)}
|
|
67
81
|
</dialog>,
|
|
68
82
|
document.body,
|
|
69
83
|
inputId,
|
|
@@ -82,6 +96,18 @@ export function AfLookup<T extends Record<string, unknown>>({
|
|
|
82
96
|
|
|
83
97
|
let hasClear = !!(props.nullable && value && props.onChange);
|
|
84
98
|
|
|
99
|
+
function handleInputKeyDown(event: React.KeyboardEvent<HTMLElement>) {
|
|
100
|
+
let hasModifier = event.ctrlKey || event.altKey || event.metaKey;
|
|
101
|
+
let isTypeaheadKey = event.key.length === 1 && !hasModifier && !event.nativeEvent.isComposing;
|
|
102
|
+
if (isTypeaheadKey) {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
event.stopPropagation();
|
|
105
|
+
openDialog(event.key);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
onKeyDown(event);
|
|
109
|
+
}
|
|
110
|
+
|
|
85
111
|
return (
|
|
86
112
|
<div style={{ anchorName } as React.CSSProperties}>
|
|
87
113
|
<div
|
|
@@ -97,9 +123,8 @@ export function AfLookup<T extends Record<string, unknown>>({
|
|
|
97
123
|
id={inputId}
|
|
98
124
|
value={value ?? ""}
|
|
99
125
|
label={label}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
onKeyDown={onKeyDown}
|
|
126
|
+
onClick={() => openDialog()}
|
|
127
|
+
onKeyDown={handleInputKeyDown}
|
|
103
128
|
className={clsx(slotProps?.textField?.className, "ObLookup-input")}
|
|
104
129
|
{...(slotProps?.textField as any)}
|
|
105
130
|
style={{ width: fullWidth ? "100%" : undefined, ...slotProps?.textField?.style }}
|
|
@@ -121,7 +146,13 @@ export function AfLookup<T extends Record<string, unknown>>({
|
|
|
121
146
|
<ClearIcon />
|
|
122
147
|
</Button>
|
|
123
148
|
)}
|
|
124
|
-
<Button
|
|
149
|
+
<Button
|
|
150
|
+
icon
|
|
151
|
+
variant="tertiary"
|
|
152
|
+
data-size="sm"
|
|
153
|
+
aria-label={getLocalizedString("Search")}
|
|
154
|
+
onClick={() => openDialog()}
|
|
155
|
+
>
|
|
125
156
|
<SearchIcon />
|
|
126
157
|
</Button>
|
|
127
158
|
</div>
|