@olenbetong/appframe-ds 0.6.0 → 0.7.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 +10 -0
- package/package.json +8 -6
- package/scripts/copyAssets.mjs +6 -1
- package/src/AfLookup/Lookup.tsx +4 -4
- package/src/AfLookup/{Combobox.tsx → LookupCombobox.tsx} +8 -8
- package/src/AfLookup/LookupEdit.tsx +2 -2
- package/src/AfLookup/index.ts +1 -1
- package/src/autocomplete/Autocomplete.css +222 -0
- package/src/autocomplete/Autocomplete.tsx +316 -0
- package/src/autocomplete/AutocompleteFrame.tsx +149 -0
- package/src/autocomplete/Combobox.tsx +260 -0
- package/src/autocomplete/index.ts +4 -0
- package/src/autocomplete/types.ts +200 -0
- package/src/autocomplete/utils.ts +183 -0
- package/src/index.ts +2 -0
- package/src/sortable/DropIndicator.css +51 -0
- package/src/sortable/DropIndicator.tsx +27 -0
- package/src/sortable/SortableItemList.css +21 -0
- package/src/sortable/SortableItemList.tsx +264 -0
- package/src/sortable/index.ts +8 -0
- package/src/sortable/reorderSortOrder.ts +126 -0
- package/src/sortable/sortOrder.ts +101 -0
- package/src/sortable/types.ts +9 -0
- package/src/sortable/useSortableContainer.ts +53 -0
- package/src/sortable/useSortableItem.ts +95 -0
- package/src/sortable/useSortableList.ts +117 -0
- /package/src/AfLookup/{Combobox.css → LookupCombobox.css} +0 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import "./Autocomplete.css";
|
|
2
|
+
|
|
3
|
+
import { Autocomplete as BaseAutocomplete } from "@base-ui/react/autocomplete";
|
|
4
|
+
import { ChipRemovable } from "@digdir/designsystemet-react";
|
|
5
|
+
import { ChevronDownIcon } from "@navikt/aksel-icons";
|
|
6
|
+
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
7
|
+
import { useId, useMemo, useRef } from "react";
|
|
8
|
+
|
|
9
|
+
import { AutocompleteFrame } from "./AutocompleteFrame.js";
|
|
10
|
+
import type { AutocompleteBaseProps, AutocompleteChangeReason, AutocompleteValue } from "./types.js";
|
|
11
|
+
import {
|
|
12
|
+
areOptionsEqual,
|
|
13
|
+
defaultFilterOptions,
|
|
14
|
+
defaultGetOptionLabel,
|
|
15
|
+
groupOptions,
|
|
16
|
+
handleInputValueChange,
|
|
17
|
+
useControlled,
|
|
18
|
+
} from "./utils.js";
|
|
19
|
+
|
|
20
|
+
export interface AutocompleteProps<
|
|
21
|
+
Option,
|
|
22
|
+
Multiple extends boolean | undefined = false,
|
|
23
|
+
FreeSolo extends boolean | undefined = false,
|
|
24
|
+
> extends AutocompleteBaseProps<Option, Multiple> {
|
|
25
|
+
/**
|
|
26
|
+
* The selected option(s). Use together with `onChange` for a controlled
|
|
27
|
+
* component.
|
|
28
|
+
*/
|
|
29
|
+
value?: AutocompleteValue<Option, Multiple, FreeSolo>;
|
|
30
|
+
/**
|
|
31
|
+
* The initially selected option(s) when uncontrolled.
|
|
32
|
+
*/
|
|
33
|
+
defaultValue?: AutocompleteValue<Option, Multiple, FreeSolo>;
|
|
34
|
+
/**
|
|
35
|
+
* Called when the selection changes.
|
|
36
|
+
*/
|
|
37
|
+
onChange?: (
|
|
38
|
+
event: Event | undefined,
|
|
39
|
+
value: AutocompleteValue<Option, Multiple, FreeSolo>,
|
|
40
|
+
reason: AutocompleteChangeReason,
|
|
41
|
+
) => void;
|
|
42
|
+
/**
|
|
43
|
+
* Allows the value to be arbitrary text that is not part of `options`.
|
|
44
|
+
* Pressing <kbd>Enter</kbd> without a highlighted option commits the text.
|
|
45
|
+
* @default false
|
|
46
|
+
*/
|
|
47
|
+
freeSolo?: FreeSolo;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A text input with suggestions, built on Base UI's `Autocomplete` and styled
|
|
52
|
+
* with Designsystemet tokens. The options are suggestions: with `freeSolo` the
|
|
53
|
+
* user may commit any text.
|
|
54
|
+
*
|
|
55
|
+
* The API follows MUI's `Autocomplete`: `options`, `value`/`onChange`,
|
|
56
|
+
* `inputValue`/`onInputChange`, `getOptionLabel`, `filterOptions`,
|
|
57
|
+
* `renderOption`, `groupBy`, `multiple` and `freeSolo` all behave the same way.
|
|
58
|
+
* Designsystemet field props (`label`, `description`, `error`, `data-size`,
|
|
59
|
+
* `data-color`) are supported on top.
|
|
60
|
+
*
|
|
61
|
+
* Use `Combobox` instead when the user must pick one of the options.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```jsx
|
|
65
|
+
* <Autocomplete
|
|
66
|
+
* freeSolo
|
|
67
|
+
* label="Search"
|
|
68
|
+
* options={suggestions}
|
|
69
|
+
* inputValue={query}
|
|
70
|
+
* onInputChange={(_event, value) => setQuery(value)}
|
|
71
|
+
* onChange={(_event, value) => runSearch(value)}
|
|
72
|
+
* />
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export function Autocomplete<
|
|
76
|
+
Option,
|
|
77
|
+
Multiple extends boolean | undefined = false,
|
|
78
|
+
FreeSolo extends boolean | undefined = false,
|
|
79
|
+
>(props: AutocompleteProps<Option, Multiple, FreeSolo>) {
|
|
80
|
+
let {
|
|
81
|
+
options,
|
|
82
|
+
multiple,
|
|
83
|
+
freeSolo,
|
|
84
|
+
value: valueProp,
|
|
85
|
+
defaultValue,
|
|
86
|
+
onChange,
|
|
87
|
+
inputValue: inputValueProp,
|
|
88
|
+
onInputChange,
|
|
89
|
+
getOptionLabel = defaultGetOptionLabel,
|
|
90
|
+
getOptionKey,
|
|
91
|
+
getOptionDisabled,
|
|
92
|
+
isOptionEqualToValue,
|
|
93
|
+
groupBy,
|
|
94
|
+
filterOptions,
|
|
95
|
+
renderOption,
|
|
96
|
+
renderChipLabel,
|
|
97
|
+
noOptionsText,
|
|
98
|
+
loadingText,
|
|
99
|
+
loading,
|
|
100
|
+
limit = -1,
|
|
101
|
+
autoHighlight,
|
|
102
|
+
openOnFocus = false,
|
|
103
|
+
open,
|
|
104
|
+
defaultOpen,
|
|
105
|
+
onOpenChange,
|
|
106
|
+
modal,
|
|
107
|
+
disabled,
|
|
108
|
+
readOnly,
|
|
109
|
+
required,
|
|
110
|
+
name,
|
|
111
|
+
id: idProp,
|
|
112
|
+
className,
|
|
113
|
+
style,
|
|
114
|
+
controlClassName,
|
|
115
|
+
popupClassName,
|
|
116
|
+
fullWidth,
|
|
117
|
+
disableClearable,
|
|
118
|
+
placeholder,
|
|
119
|
+
inputRef,
|
|
120
|
+
"data-size": size,
|
|
121
|
+
"data-color": color,
|
|
122
|
+
} = props;
|
|
123
|
+
|
|
124
|
+
type Value = AutocompleteValue<Option, Multiple, FreeSolo>;
|
|
125
|
+
|
|
126
|
+
let generatedId = useId();
|
|
127
|
+
let id = idProp ?? generatedId;
|
|
128
|
+
|
|
129
|
+
let emptyValue = (multiple ? [] : null) as Value;
|
|
130
|
+
let [value, setValue] = useControlled(valueProp, defaultValue ?? emptyValue);
|
|
131
|
+
let initialInputValue = multiple || value == null ? "" : getOptionLabel(value as Option);
|
|
132
|
+
let [inputValue, setInputValue] = useControlled(inputValueProp, initialInputValue);
|
|
133
|
+
let highlightedRef = useRef<Option | undefined>(undefined);
|
|
134
|
+
|
|
135
|
+
let filteredOptions = useMemo(() => {
|
|
136
|
+
let filter = (filterOptions ?? defaultFilterOptions) as (
|
|
137
|
+
options: readonly Option[],
|
|
138
|
+
state: { inputValue: string; getOptionLabel: (option: Option) => string },
|
|
139
|
+
) => Option[];
|
|
140
|
+
|
|
141
|
+
return filter(options, { inputValue, getOptionLabel });
|
|
142
|
+
}, [options, inputValue, filterOptions, getOptionLabel]);
|
|
143
|
+
|
|
144
|
+
let filteredItems = useMemo(
|
|
145
|
+
() => (groupBy ? groupOptions(filteredOptions, groupBy) : filteredOptions),
|
|
146
|
+
[filteredOptions, groupBy],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
let selectedOptions = useMemo(() => (multiple ? ((value ?? []) as Option[]) : []), [multiple, value]);
|
|
150
|
+
|
|
151
|
+
function commit(next: Value, reason: AutocompleteChangeReason, event?: Event) {
|
|
152
|
+
setValue(next);
|
|
153
|
+
onChange?.(event, next, reason);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function handleSelectOption(option: Option, event?: Event) {
|
|
157
|
+
if (multiple) {
|
|
158
|
+
let alreadySelected = selectedOptions.some((item) => areOptionsEqual(item, option, isOptionEqualToValue));
|
|
159
|
+
if (alreadySelected) return;
|
|
160
|
+
|
|
161
|
+
commit([...selectedOptions, option] as Value, "selectOption", event);
|
|
162
|
+
setInputValue("");
|
|
163
|
+
onInputChange?.(event, "", "reset");
|
|
164
|
+
} else {
|
|
165
|
+
commit(option as unknown as Value, "selectOption", event);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function handleRemoveChip(option: Option) {
|
|
170
|
+
let next = selectedOptions.filter((item) => !areOptionsEqual(item, option, isOptionEqualToValue));
|
|
171
|
+
commit(next as Value, "removeOption");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function handleInputKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
|
|
175
|
+
if (event.key !== "Enter" || !freeSolo || highlightedRef.current !== undefined) return;
|
|
176
|
+
|
|
177
|
+
let text = inputValue.trim();
|
|
178
|
+
if (!text) return;
|
|
179
|
+
|
|
180
|
+
if (multiple) {
|
|
181
|
+
commit([...selectedOptions, text] as Value, "createOption", event.nativeEvent);
|
|
182
|
+
setInputValue("");
|
|
183
|
+
onInputChange?.(event.nativeEvent, "", "reset");
|
|
184
|
+
} else {
|
|
185
|
+
commit(text as unknown as Value, "createOption", event.nativeEvent);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function handleInputBlur(event: React.FocusEvent<HTMLInputElement>) {
|
|
190
|
+
if (freeSolo) return;
|
|
191
|
+
|
|
192
|
+
// Without free text, the input only makes sense as a reflection of the
|
|
193
|
+
// current selection, so anything the user typed is discarded.
|
|
194
|
+
let text = multiple || value == null ? "" : getOptionLabel(value as Option);
|
|
195
|
+
if (text !== inputValue) {
|
|
196
|
+
setInputValue(text);
|
|
197
|
+
onInputChange?.(event.nativeEvent, text, "reset");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let chips =
|
|
202
|
+
multiple && selectedOptions.length > 0
|
|
203
|
+
? selectedOptions.map((option) => (
|
|
204
|
+
<ChipRemovable
|
|
205
|
+
key={getOptionKey?.(option) ?? getOptionLabel(option)}
|
|
206
|
+
data-size={size}
|
|
207
|
+
disabled={disabled || readOnly}
|
|
208
|
+
onClick={() => handleRemoveChip(option)}
|
|
209
|
+
>
|
|
210
|
+
{renderChipLabel?.(option) ?? getOptionLabel(option)}
|
|
211
|
+
</ChipRemovable>
|
|
212
|
+
))
|
|
213
|
+
: undefined;
|
|
214
|
+
|
|
215
|
+
function renderItem(option: Option, index: number) {
|
|
216
|
+
let selected = multiple
|
|
217
|
+
? selectedOptions.some((item) => areOptionsEqual(item, option, isOptionEqualToValue))
|
|
218
|
+
: value != null && areOptionsEqual(value as Option, option, isOptionEqualToValue);
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<BaseAutocomplete.Item
|
|
222
|
+
key={getOptionKey?.(option) ?? getOptionLabel(option)}
|
|
223
|
+
className="ObAutocomplete-item"
|
|
224
|
+
value={option}
|
|
225
|
+
index={index}
|
|
226
|
+
disabled={getOptionDisabled?.(option)}
|
|
227
|
+
data-selected={selected || undefined}
|
|
228
|
+
onClick={(event) => handleSelectOption(option, event.nativeEvent)}
|
|
229
|
+
>
|
|
230
|
+
<span>{renderOption?.(option, { index, inputValue, selected }) ?? getOptionLabel(option)}</span>
|
|
231
|
+
</BaseAutocomplete.Item>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function renderListItem(item: any, index: number) {
|
|
236
|
+
if (!groupBy) return renderItem(item as Option, index);
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<BaseAutocomplete.Group key={item.value} items={item.items}>
|
|
240
|
+
<BaseAutocomplete.GroupLabel className="ObAutocomplete-groupLabel">{item.value}</BaseAutocomplete.GroupLabel>
|
|
241
|
+
<BaseAutocomplete.Collection>
|
|
242
|
+
{(option: Option, optionIndex: number) => renderItem(option, optionIndex)}
|
|
243
|
+
</BaseAutocomplete.Collection>
|
|
244
|
+
</BaseAutocomplete.Group>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<BaseAutocomplete.Root
|
|
250
|
+
items={options as any}
|
|
251
|
+
filteredItems={filteredItems as any}
|
|
252
|
+
filter={null}
|
|
253
|
+
value={inputValue}
|
|
254
|
+
onValueChange={(next, details) => {
|
|
255
|
+
handleInputValueChange(next, details, setInputValue, onInputChange);
|
|
256
|
+
|
|
257
|
+
if (details.reason === "clear-press" || details.reason === "input-clear") {
|
|
258
|
+
commit(emptyValue, "clear", details.event);
|
|
259
|
+
}
|
|
260
|
+
}}
|
|
261
|
+
itemToStringValue={getOptionLabel as (option: unknown) => string}
|
|
262
|
+
onItemHighlighted={(item) => {
|
|
263
|
+
highlightedRef.current = item as Option | undefined;
|
|
264
|
+
}}
|
|
265
|
+
disabled={disabled}
|
|
266
|
+
readOnly={readOnly}
|
|
267
|
+
required={required}
|
|
268
|
+
name={name}
|
|
269
|
+
id={id}
|
|
270
|
+
open={open}
|
|
271
|
+
defaultOpen={defaultOpen}
|
|
272
|
+
onOpenChange={(nextOpen) => onOpenChange?.(nextOpen)}
|
|
273
|
+
openOnInputClick={openOnFocus}
|
|
274
|
+
autoHighlight={autoHighlight}
|
|
275
|
+
limit={limit}
|
|
276
|
+
modal={modal}
|
|
277
|
+
>
|
|
278
|
+
<AutocompleteFrame
|
|
279
|
+
id={id}
|
|
280
|
+
label={props.label}
|
|
281
|
+
description={props.description}
|
|
282
|
+
error={props.error}
|
|
283
|
+
className={className}
|
|
284
|
+
style={style}
|
|
285
|
+
controlClassName={controlClassName}
|
|
286
|
+
popupClassName={popupClassName}
|
|
287
|
+
fullWidth={fullWidth}
|
|
288
|
+
size={size}
|
|
289
|
+
color={color}
|
|
290
|
+
disabled={disabled}
|
|
291
|
+
readOnly={readOnly}
|
|
292
|
+
required={required}
|
|
293
|
+
placeholder={placeholder}
|
|
294
|
+
loading={loading}
|
|
295
|
+
disableClearable={disableClearable}
|
|
296
|
+
inputRef={inputRef}
|
|
297
|
+
chips={chips}
|
|
298
|
+
trigger={
|
|
299
|
+
<BaseAutocomplete.Trigger
|
|
300
|
+
className="ObAutocomplete-button ObAutocomplete-trigger"
|
|
301
|
+
disabled={disabled || readOnly}
|
|
302
|
+
aria-label={getLocalizedString("Open")}
|
|
303
|
+
>
|
|
304
|
+
<ChevronDownIcon aria-hidden fontSize="1.25em" />
|
|
305
|
+
</BaseAutocomplete.Trigger>
|
|
306
|
+
}
|
|
307
|
+
emptyContent={
|
|
308
|
+
loading ? (loadingText ?? getLocalizedString("Loading")) : (noOptionsText ?? getLocalizedString("No options"))
|
|
309
|
+
}
|
|
310
|
+
renderListItem={renderListItem}
|
|
311
|
+
onInputKeyDown={handleInputKeyDown}
|
|
312
|
+
onInputBlur={handleInputBlur}
|
|
313
|
+
/>
|
|
314
|
+
</BaseAutocomplete.Root>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Combobox as BaseCombobox } from "@base-ui/react/combobox";
|
|
2
|
+
import { FieldDescription, Label, Spinner, ValidationMessage } from "@digdir/designsystemet-react";
|
|
3
|
+
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
4
|
+
import { XMarkIcon } from "@navikt/aksel-icons";
|
|
5
|
+
import clsx from "clsx";
|
|
6
|
+
import type { ReactNode, Ref } from "react";
|
|
7
|
+
|
|
8
|
+
import type { AutocompleteSize } from "./types.js";
|
|
9
|
+
|
|
10
|
+
export interface AutocompleteFrameProps {
|
|
11
|
+
id?: string;
|
|
12
|
+
label?: ReactNode;
|
|
13
|
+
description?: ReactNode;
|
|
14
|
+
error?: ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
style?: React.CSSProperties;
|
|
17
|
+
controlClassName?: string;
|
|
18
|
+
popupClassName?: string;
|
|
19
|
+
fullWidth?: boolean;
|
|
20
|
+
size?: AutocompleteSize;
|
|
21
|
+
color?: string;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
readOnly?: boolean;
|
|
24
|
+
required?: boolean;
|
|
25
|
+
placeholder?: string;
|
|
26
|
+
loading?: boolean;
|
|
27
|
+
disableClearable?: boolean;
|
|
28
|
+
inputRef?: Ref<HTMLInputElement>;
|
|
29
|
+
/**
|
|
30
|
+
* Chips for the selected values, rendered above the input.
|
|
31
|
+
*/
|
|
32
|
+
chips?: ReactNode;
|
|
33
|
+
/**
|
|
34
|
+
* The trigger button, which differs between `Autocomplete` and `Combobox`.
|
|
35
|
+
*/
|
|
36
|
+
trigger?: ReactNode;
|
|
37
|
+
/**
|
|
38
|
+
* Rendered in the popup when the filtered list is empty.
|
|
39
|
+
*/
|
|
40
|
+
emptyContent?: ReactNode;
|
|
41
|
+
/**
|
|
42
|
+
* Renders an entry of the filtered list. Receives either an option or, when
|
|
43
|
+
* `groupBy` is used, a group.
|
|
44
|
+
*/
|
|
45
|
+
renderListItem: (item: any, index: number) => ReactNode;
|
|
46
|
+
onInputKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
|
47
|
+
onInputBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Shared chrome for `Autocomplete` and `Combobox`: the Designsystemet field
|
|
52
|
+
* (label, description, validation message), the bordered control holding the
|
|
53
|
+
* chips and the input, and the popup with the option list.
|
|
54
|
+
*
|
|
55
|
+
* All the parts used here are shared between the Base UI `Autocomplete` and
|
|
56
|
+
* `Combobox` namespaces, so they can be imported from `combobox` regardless of
|
|
57
|
+
* which root the caller renders.
|
|
58
|
+
*/
|
|
59
|
+
export function AutocompleteFrame({
|
|
60
|
+
id,
|
|
61
|
+
label,
|
|
62
|
+
description,
|
|
63
|
+
error,
|
|
64
|
+
className,
|
|
65
|
+
style,
|
|
66
|
+
controlClassName,
|
|
67
|
+
popupClassName,
|
|
68
|
+
fullWidth = true,
|
|
69
|
+
size,
|
|
70
|
+
color,
|
|
71
|
+
disabled,
|
|
72
|
+
readOnly,
|
|
73
|
+
required,
|
|
74
|
+
placeholder,
|
|
75
|
+
loading,
|
|
76
|
+
disableClearable,
|
|
77
|
+
inputRef,
|
|
78
|
+
chips,
|
|
79
|
+
trigger,
|
|
80
|
+
emptyContent,
|
|
81
|
+
renderListItem,
|
|
82
|
+
onInputKeyDown,
|
|
83
|
+
onInputBlur,
|
|
84
|
+
}: AutocompleteFrameProps) {
|
|
85
|
+
return (
|
|
86
|
+
// Designsystemet's `Field` is a custom element that expects to own the
|
|
87
|
+
// layout, so the root here is a plain grid instead.
|
|
88
|
+
<div
|
|
89
|
+
className={clsx("ObAutocomplete-root", className)}
|
|
90
|
+
style={style}
|
|
91
|
+
data-size={size}
|
|
92
|
+
data-color={color}
|
|
93
|
+
data-full-width={fullWidth}
|
|
94
|
+
>
|
|
95
|
+
{label && (
|
|
96
|
+
<Label htmlFor={id} data-size={size}>
|
|
97
|
+
{label}
|
|
98
|
+
{required && <span aria-hidden> *</span>}
|
|
99
|
+
</Label>
|
|
100
|
+
)}
|
|
101
|
+
{description && <FieldDescription data-size={size}>{description}</FieldDescription>}
|
|
102
|
+
<div
|
|
103
|
+
className={clsx("ObAutocomplete-control", controlClassName)}
|
|
104
|
+
data-invalid={error ? true : undefined}
|
|
105
|
+
data-disabled={disabled ? true : undefined}
|
|
106
|
+
>
|
|
107
|
+
{chips && <div className="ObAutocomplete-chips">{chips}</div>}
|
|
108
|
+
<BaseCombobox.InputGroup className="ObAutocomplete-inputGroup">
|
|
109
|
+
<BaseCombobox.Input
|
|
110
|
+
id={id}
|
|
111
|
+
ref={inputRef}
|
|
112
|
+
className="ObAutocomplete-input"
|
|
113
|
+
placeholder={placeholder}
|
|
114
|
+
disabled={disabled}
|
|
115
|
+
readOnly={readOnly}
|
|
116
|
+
required={required}
|
|
117
|
+
aria-invalid={error ? true : undefined}
|
|
118
|
+
onKeyDown={onInputKeyDown}
|
|
119
|
+
onBlur={onInputBlur}
|
|
120
|
+
/>
|
|
121
|
+
{loading && (
|
|
122
|
+
<span className="ObAutocomplete-spinner">
|
|
123
|
+
<Spinner aria-label={getLocalizedString("Loading")} data-size="xs" />
|
|
124
|
+
</span>
|
|
125
|
+
)}
|
|
126
|
+
{!disableClearable && (
|
|
127
|
+
<BaseCombobox.Clear
|
|
128
|
+
className="ObAutocomplete-button ObAutocomplete-clear"
|
|
129
|
+
disabled={disabled || readOnly}
|
|
130
|
+
aria-label={getLocalizedString("Clear")}
|
|
131
|
+
>
|
|
132
|
+
<XMarkIcon aria-hidden fontSize="1.25em" />
|
|
133
|
+
</BaseCombobox.Clear>
|
|
134
|
+
)}
|
|
135
|
+
{trigger}
|
|
136
|
+
</BaseCombobox.InputGroup>
|
|
137
|
+
</div>
|
|
138
|
+
{error && <ValidationMessage data-size={size}>{error}</ValidationMessage>}
|
|
139
|
+
<BaseCombobox.Portal>
|
|
140
|
+
<BaseCombobox.Positioner className="ObAutocomplete-positioner" sideOffset={4}>
|
|
141
|
+
<BaseCombobox.Popup className={clsx("ObAutocomplete-popup", popupClassName)} data-size={size}>
|
|
142
|
+
<BaseCombobox.Empty className="ObAutocomplete-empty">{emptyContent}</BaseCombobox.Empty>
|
|
143
|
+
<BaseCombobox.List className="ObAutocomplete-list">{renderListItem}</BaseCombobox.List>
|
|
144
|
+
</BaseCombobox.Popup>
|
|
145
|
+
</BaseCombobox.Positioner>
|
|
146
|
+
</BaseCombobox.Portal>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|