@olenbetong/appframe-ds 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/package.json +46 -0
- package/src/AfLookup/BoundLookup.tsx +35 -0
- package/src/AfLookup/Combobox.css +78 -0
- package/src/AfLookup/Combobox.tsx +233 -0
- package/src/AfLookup/Lookup.css +104 -0
- package/src/AfLookup/Lookup.tsx +264 -0
- package/src/AfLookup/index.ts +3 -0
- package/src/binding/BoundCheckbox.tsx +31 -0
- package/src/binding/BoundDatePicker.tsx +32 -0
- package/src/binding/BoundDateTimePicker.tsx +37 -0
- package/src/binding/BoundInput.tsx +26 -0
- package/src/binding/BoundNumericTextField.tsx +102 -0
- package/src/binding/BoundSelect.tsx +31 -0
- package/src/binding/BoundSwitch.tsx +30 -0
- package/src/binding/BoundTextField.tsx +41 -0
- package/src/binding/CancelButton.tsx +27 -0
- package/src/binding/CancelIconButton.tsx +30 -0
- package/src/binding/DataEditToolbar.tsx +93 -0
- package/src/binding/DataObjectSelect.tsx +121 -0
- package/src/binding/DeleteButton.tsx +31 -0
- package/src/binding/DeleteIconButton.tsx +34 -0
- package/src/binding/RefreshButton.tsx +27 -0
- package/src/binding/RefreshIconButton.tsx +32 -0
- package/src/binding/RefreshRowIconButton.tsx +32 -0
- package/src/binding/SaveButton.tsx +27 -0
- package/src/binding/SaveIconButton.tsx +31 -0
- package/src/binding/index.ts +19 -0
- package/src/index.ts +2 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.json +8 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @olenbetong/appframe-ds
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6b58d71: Initial release of `@olenbetong/appframe-ds`
|
|
8
|
+
|
|
9
|
+
New package providing [Designsystemet](https://github.com/digdir/designsystemet) equivalents of the components in `@olenbetong/appframe-mui`. Enables gradual per-app migration from MUI to `@digdir/designsystemet-react`.
|
|
10
|
+
|
|
11
|
+
**Binding components** (`import { … } from "@olenbetong/appframe-ds"`):
|
|
12
|
+
- `BoundTextField` — `<Textfield>` bound to an Appframe field via `useField`; handles date/datetime-local formatting
|
|
13
|
+
- `BoundInput` — bare `<Input>` binding, use inside a DS `<Field>` wrapper
|
|
14
|
+
- `BoundNumericTextField` — `<Textfield>` with optional `precision`/`scale` numeric validation and blur-normalisation
|
|
15
|
+
- `BoundCheckbox` — `<Checkbox>` bound to a boolean field
|
|
16
|
+
- `BoundSelect` — `<Select>` (native) bound to a field
|
|
17
|
+
- `BoundSwitch` — `<Switch>` bound to a boolean field
|
|
18
|
+
- `BoundDatePicker` — `<Textfield type="date">` bound to a date field
|
|
19
|
+
- `BoundDateTimePicker` — `<Textfield type="datetime-local">` bound to a datetime field
|
|
20
|
+
- `SaveButton` / `SaveIconButton` — wraps `useSaveButton`
|
|
21
|
+
- `CancelButton` / `CancelIconButton` — wraps `useCancelButton`
|
|
22
|
+
- `DeleteButton` / `DeleteIconButton` — wraps `useDeleteButton`, renders with `data-color="danger"`
|
|
23
|
+
- `RefreshButton` / `RefreshIconButton` — wraps `useRefreshButton` / `dataObject.refreshDataSource()`
|
|
24
|
+
- `RefreshRowIconButton` — wraps `useRefreshRowButton`
|
|
25
|
+
- `DataEditToolbar` — flex toolbar with save/cancel/refresh-row icon buttons and optional index navigator
|
|
26
|
+
- `DataObjectSelect` / `BoundDataObjectSelect` — DS `<Select>` populated from a data object's data handler
|
|
27
|
+
|
|
28
|
+
**Lookup components** (`import { … } from "@olenbetong/appframe-ds"`):
|
|
29
|
+
- `Combobox` — virtualised search-and-select list using `react-window`; no MUI dependency
|
|
30
|
+
- `AfLookup` / `useLookup` — full lookup field with DS `<Textfield>` + native `<dialog>` popup
|
|
31
|
+
- `BoundLookup` — `AfLookup` bound to a data object via `useCurrentRow`
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@olenbetong/appframe-ds",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Components that bind Designsystemet components to Appframe data objects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "./es/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./es/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"sideEffects": [
|
|
13
|
+
"**/*.css"
|
|
14
|
+
],
|
|
15
|
+
"author": "Bjørnar Vister Hansen <bvh@olenbetong.no>",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@digdir/designsystemet-react": ">=1.0.0",
|
|
19
|
+
"@mui/icons-material": ">=7.0.0",
|
|
20
|
+
"react": ">=18.2.0",
|
|
21
|
+
"react-dom": ">=18.2.0"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@olenbetong/appframe-core": "2.11.10",
|
|
25
|
+
"@olenbetong/appframe-data": "1.5.0",
|
|
26
|
+
"@olenbetong/appframe-react": "1.21.25",
|
|
27
|
+
"react-window": "^1.8.11",
|
|
28
|
+
"react-virtualized-auto-sizer": "^1.0.26",
|
|
29
|
+
"clsx": "^2.1.1"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@digdir/designsystemet-react": "1.13.3",
|
|
33
|
+
"@mui/icons-material": "9.0.0",
|
|
34
|
+
"@types/react": "19.2.14",
|
|
35
|
+
"@types/react-dom": "19.2.3",
|
|
36
|
+
"@types/react-window": "^1.8.8",
|
|
37
|
+
"react": "19.2.5",
|
|
38
|
+
"react-dom": "19.2.5",
|
|
39
|
+
"typescript": "6.0.3"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "pnpm run build:tsc && pnpm run build:css",
|
|
43
|
+
"build:tsc": "node --no-warnings ../../scripts/clean.ts ./es tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
|
|
44
|
+
"build:css": "cp src/AfLookup/Lookup.css es/AfLookup/Lookup.css && cp src/AfLookup/Combobox.css es/AfLookup/Combobox.css"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CurrentRow, DataObject } from "@olenbetong/appframe-data";
|
|
2
|
+
import { useCurrentRow, useDataObject } from "@olenbetong/appframe-react";
|
|
3
|
+
|
|
4
|
+
import { AfLookup, type AfLookupProps } from "./Lookup.js";
|
|
5
|
+
|
|
6
|
+
export interface BoundLookupProps<
|
|
7
|
+
TBoundData extends Record<string, unknown>,
|
|
8
|
+
TLookupData extends Record<string, unknown>,
|
|
9
|
+
> extends Omit<AfLookupProps<TLookupData>, "onChange"> {
|
|
10
|
+
dataObject: DataObject<TLookupData>;
|
|
11
|
+
displayField?: keyof TBoundData;
|
|
12
|
+
getChanges: (selectedItem: TLookupData | null) => Partial<TBoundData>;
|
|
13
|
+
getDisplayValue?: (currentRow: CurrentRow<TBoundData>) => string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function BoundLookup<
|
|
17
|
+
TBoundData extends Record<string, unknown>,
|
|
18
|
+
TLookupData extends Record<string, unknown> = any,
|
|
19
|
+
>({ dataObject, displayField, getChanges, getDisplayValue, ...props }: BoundLookupProps<TBoundData, TLookupData>) {
|
|
20
|
+
let boundDataObject = useDataObject<TBoundData>();
|
|
21
|
+
let row = useCurrentRow(boundDataObject);
|
|
22
|
+
|
|
23
|
+
function handleChange(item: TLookupData | null) {
|
|
24
|
+
let changes = getChanges(item);
|
|
25
|
+
|
|
26
|
+
for (let key in changes) {
|
|
27
|
+
let newValue = changes[key] as TBoundData[keyof TBoundData];
|
|
28
|
+
boundDataObject.currentRow(key, newValue);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let displayValue = getDisplayValue ? getDisplayValue(row) : displayField ? String(row[displayField] ?? "") : "";
|
|
33
|
+
|
|
34
|
+
return <AfLookup<TLookupData> dataObject={dataObject} value={displayValue} onChange={handleChange} {...props} />;
|
|
35
|
+
}
|
|
@@ -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
|
+
height: 372px;
|
|
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,233 @@
|
|
|
1
|
+
import "./Combobox.css";
|
|
2
|
+
|
|
3
|
+
import { Button, Textfield } from "@digdir/designsystemet-react";
|
|
4
|
+
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
5
|
+
import type { DataObject } from "@olenbetong/appframe-data";
|
|
6
|
+
import { useDataWithFilter, useDebounce, useLoading } from "@olenbetong/appframe-react";
|
|
7
|
+
|
|
8
|
+
import clsx from "clsx";
|
|
9
|
+
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
10
|
+
import { VariableSizeList as VirtualList } from "react-window";
|
|
11
|
+
|
|
12
|
+
export interface ComboboxProps<T extends Record<string, unknown>> {
|
|
13
|
+
nullable?: boolean;
|
|
14
|
+
dataObject: DataObject<T>;
|
|
15
|
+
uniqueIdField?: string;
|
|
16
|
+
isItemSelected?: (item: T) => boolean;
|
|
17
|
+
renderItem: (props: React.ComponentPropsWithRef<"li"> & { item: T }) => React.ReactNode;
|
|
18
|
+
onChange?: (item: T | null) => void;
|
|
19
|
+
value?: T;
|
|
20
|
+
onClose?: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
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();
|
|
33
|
+
let [isDesktop, setIsDesktop] = useState(
|
|
34
|
+
() => typeof window !== "undefined" && window.matchMedia("(width >= 768px) and (pointer: fine)").matches,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
let mq = window.matchMedia("(width >= 768px) and (pointer: fine)");
|
|
39
|
+
let handler = (e: MediaQueryListEvent) => setIsDesktop(e.matches);
|
|
40
|
+
mq.addEventListener("change", handler);
|
|
41
|
+
return () => mq.removeEventListener("change", handler);
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
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(
|
|
59
|
+
dataObject,
|
|
60
|
+
query
|
|
61
|
+
.split(/\s+/g)
|
|
62
|
+
.map((word) => `[SearchColumn] LIKE '%${word}%'`)
|
|
63
|
+
.join(" AND "),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
let wrapperRef = useRef<HTMLDivElement>(null);
|
|
67
|
+
let [height, setHeight] = useState<number>(0);
|
|
68
|
+
|
|
69
|
+
useLayoutEffect(() => {
|
|
70
|
+
if (!wrapperRef.current) return;
|
|
71
|
+
|
|
72
|
+
let resizeObserver = new ResizeObserver(() => {
|
|
73
|
+
let newHeight = wrapperRef.current!.offsetHeight;
|
|
74
|
+
if (newHeight > 0) setHeight(newHeight);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
resizeObserver.observe(wrapperRef.current);
|
|
78
|
+
|
|
79
|
+
return () => resizeObserver.disconnect();
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
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
|
+
let sizeMap = useRef(new Map<number, number>());
|
|
137
|
+
let getItemSize = (index: number) => sizeMap.current.get(index) ?? 56;
|
|
138
|
+
let setSize = (index: number, size: number) => {
|
|
139
|
+
if (sizeMap.current.get(index) !== size) {
|
|
140
|
+
sizeMap.current.set(index, size);
|
|
141
|
+
// listRef will not be set on the initial render, so delay resetting
|
|
142
|
+
setTimeout(() => listRef.current?.resetAfterIndex(index, true));
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// biome-ignore lint: nested component defined for performance reasons inside render
|
|
147
|
+
let Row = ({ index, style: { height, ...style } }: { index: number; style: React.CSSProperties }) => {
|
|
148
|
+
let rowRef = useRef<HTMLLIElement>(null);
|
|
149
|
+
|
|
150
|
+
useLayoutEffect(() => {
|
|
151
|
+
if (!rowRef.current) return;
|
|
152
|
+
setSize(index, rowRef.current.offsetHeight);
|
|
153
|
+
}, [index]);
|
|
154
|
+
|
|
155
|
+
let item = data[index];
|
|
156
|
+
let itemId = getItemId(item);
|
|
157
|
+
let active = index === currentIndex;
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<Component
|
|
161
|
+
className={clsx("ObCombobox-listItem", active && "active", isItemSelected?.(item) && "selected")}
|
|
162
|
+
aria-selected={active}
|
|
163
|
+
onClick={() => handleSelectItem(item)}
|
|
164
|
+
item={item}
|
|
165
|
+
style={{
|
|
166
|
+
...style,
|
|
167
|
+
padding: isDesktop ? "0 8px" : "8px",
|
|
168
|
+
}}
|
|
169
|
+
ref={rowRef}
|
|
170
|
+
key={itemId}
|
|
171
|
+
id={itemId}
|
|
172
|
+
/>
|
|
173
|
+
);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
useEffect(() => listRef.current?.scrollToItem(currentIndex, "smart"), [currentIndex]);
|
|
177
|
+
|
|
178
|
+
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
|
|
181
|
+
<div className="ObCombobox-root" id={id} onClick={(evt) => evt.stopPropagation()}>
|
|
182
|
+
<div className="ObCombobox-actions">
|
|
183
|
+
{nullable && (
|
|
184
|
+
<Button
|
|
185
|
+
className="ObCombobox-clear"
|
|
186
|
+
data-size={isDesktop ? "sm" : "md"}
|
|
187
|
+
variant="tertiary"
|
|
188
|
+
onClick={() => {
|
|
189
|
+
onChange?.(null);
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
{getLocalizedString("Clear")}
|
|
193
|
+
</Button>
|
|
194
|
+
)}
|
|
195
|
+
{onClose && (
|
|
196
|
+
<Button data-size={isDesktop ? "sm" : "md"} variant="tertiary" className="ObCombobox-close" onClick={onClose}>
|
|
197
|
+
{getLocalizedString("Close")}
|
|
198
|
+
</Button>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
<Textfield
|
|
202
|
+
type="search"
|
|
203
|
+
className="ObCombobox-input"
|
|
204
|
+
label={getLocalizedString("Search")}
|
|
205
|
+
onKeyDown={handleKeyDown}
|
|
206
|
+
aria-activedescendant={getItemId(data[currentIndex]) ?? ""}
|
|
207
|
+
value={inputValue}
|
|
208
|
+
onChange={(event) => setInputValue((event.target as HTMLInputElement).value)}
|
|
209
|
+
/>
|
|
210
|
+
<div className="ObCombobox-listWrap" ref={wrapperRef}>
|
|
211
|
+
{loading && (
|
|
212
|
+
<div className="ObCombobox-progress">
|
|
213
|
+
<div className="ObCombobox-progress-bar" />
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
{height > 0 && (
|
|
217
|
+
<div className="ObCombobox-list" role="listbox">
|
|
218
|
+
<VirtualList
|
|
219
|
+
ref={listRef}
|
|
220
|
+
height={height}
|
|
221
|
+
width="100%"
|
|
222
|
+
itemCount={data.length}
|
|
223
|
+
itemSize={getItemSize}
|
|
224
|
+
overscanCount={5}
|
|
225
|
+
>
|
|
226
|
+
{Row}
|
|
227
|
+
</VirtualList>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
.ObLookup-dialog {
|
|
2
|
+
border: 0;
|
|
3
|
+
margin-block-start: 0;
|
|
4
|
+
padding: 1rem;
|
|
5
|
+
|
|
6
|
+
background-color: whitesmoke;
|
|
7
|
+
|
|
8
|
+
transition:
|
|
9
|
+
display 0.2s allow-discrete,
|
|
10
|
+
transform 0.2s ease-out,
|
|
11
|
+
scale 0.2s ease-out,
|
|
12
|
+
opacity 0.2s ease-out;
|
|
13
|
+
|
|
14
|
+
&::backdrop {
|
|
15
|
+
opacity: 0;
|
|
16
|
+
background: rgb(0 0 0 / 0.2);
|
|
17
|
+
transition:
|
|
18
|
+
display 0.2s allow-discrete,
|
|
19
|
+
opacity 0.2s ease-out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
&[open]::backdrop {
|
|
23
|
+
opacity: 1;
|
|
24
|
+
|
|
25
|
+
@starting-style {
|
|
26
|
+
opacity: 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@media (width < 768px) {
|
|
31
|
+
inset: 10vh 0.5rem 0;
|
|
32
|
+
|
|
33
|
+
border-radius: 1rem 1rem 0 0;
|
|
34
|
+
height: 90vh;
|
|
35
|
+
width: calc(100vw - 1rem);
|
|
36
|
+
max-width: 100vw;
|
|
37
|
+
transform: translateY(100%);
|
|
38
|
+
z-index: 1500;
|
|
39
|
+
|
|
40
|
+
opacity: 0;
|
|
41
|
+
|
|
42
|
+
&[open] {
|
|
43
|
+
transform: translateY(0);
|
|
44
|
+
opacity: 1;
|
|
45
|
+
|
|
46
|
+
@starting-style {
|
|
47
|
+
transform: translateY(100%);
|
|
48
|
+
opacity: 0;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@media (width >= 768px) {
|
|
54
|
+
inset-block-start: 50%;
|
|
55
|
+
inset-inline-start: 50%;
|
|
56
|
+
translate: -50% -50%;
|
|
57
|
+
scale: 1 0;
|
|
58
|
+
opacity: 0;
|
|
59
|
+
|
|
60
|
+
border-radius: 0.25rem;
|
|
61
|
+
|
|
62
|
+
height: clamp(300px, 80vh, 500px);
|
|
63
|
+
min-width: 320px;
|
|
64
|
+
width: var(--default-width, 400px);
|
|
65
|
+
|
|
66
|
+
&[open] {
|
|
67
|
+
scale: 1;
|
|
68
|
+
opacity: 1;
|
|
69
|
+
|
|
70
|
+
@starting-style {
|
|
71
|
+
scale: 1 0;
|
|
72
|
+
opacity: 0;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@supports (inset-block-start: anchor(bottom)) {
|
|
78
|
+
@media (width < 768px) or (pointer: coarse) {
|
|
79
|
+
anchor-name: unset !important;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@media screen and (width >= 768px) and (pointer: fine) {
|
|
83
|
+
inset-block-start: anchor(bottom);
|
|
84
|
+
inset-block-end: unset;
|
|
85
|
+
|
|
86
|
+
inset-inline-start: anchor(left);
|
|
87
|
+
inset-inline-end: unset;
|
|
88
|
+
|
|
89
|
+
translate: unset;
|
|
90
|
+
transform-origin: top left;
|
|
91
|
+
|
|
92
|
+
resize: both;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
&[data-anchor="manual"] {
|
|
97
|
+
inset: unset;
|
|
98
|
+
position: fixed;
|
|
99
|
+
inset-inline-start: var(--lookup-anchor-inline-start, 50%);
|
|
100
|
+
inset-block-start: var(--lookup-anchor-block-start, 50%);
|
|
101
|
+
translate: unset;
|
|
102
|
+
transform: none;
|
|
103
|
+
}
|
|
104
|
+
}
|