@expofp/ui 3.39.0 → 3.39.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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import './Autocomplete.scss';
|
|
3
3
|
import classNames from 'classnames';
|
|
4
|
-
import { useContext, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { lazy, Suspense, useContext, useEffect, useId, useMemo, useRef, useState, } from 'react';
|
|
5
5
|
import { createPortal } from 'react-dom';
|
|
6
6
|
import { ResizeObserver } from 'resize-observer';
|
|
7
7
|
import useOnClickOutside from '../../shared/hooks/useOnClickOutside';
|
|
@@ -18,6 +18,9 @@ const Autocomplete = ({ placeholder, options, value, showClear = false, onChange
|
|
|
18
18
|
const refAutocomplete = useRef(null);
|
|
19
19
|
const refSearchInput = useRef(null);
|
|
20
20
|
const refOptions = useRef(null);
|
|
21
|
+
const virtuosoRef = useRef(null);
|
|
22
|
+
// State, not a ref: the list can only mount once its scroll parent exists.
|
|
23
|
+
const [scrollElement, setScrollElement] = useState(null);
|
|
21
24
|
const blurTimeoutRef = useRef(null);
|
|
22
25
|
const listboxId = useId();
|
|
23
26
|
const getOptionId = (index) => `${listboxId}-option-${index}`;
|
|
@@ -109,10 +112,10 @@ const Autocomplete = ({ placeholder, options, value, showClear = false, onChange
|
|
|
109
112
|
return () => observer.disconnect();
|
|
110
113
|
}, [inline, showOptionsDropdown, scrollContainer]);
|
|
111
114
|
useEffect(() => {
|
|
112
|
-
if (focusOptionIndex === null
|
|
115
|
+
if (focusOptionIndex === null)
|
|
113
116
|
return;
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
// Through the list, not the DOM: a focused option far down is not rendered until scrolled to.
|
|
118
|
+
virtuosoRef.current?.scrollIntoView({ index: focusOptionIndex });
|
|
116
119
|
}, [focusOptionIndex]);
|
|
117
120
|
useOnClickOutside([refAutocomplete, refOptions], () => setShowOptionsDropdown(false), showOptionsDropdown);
|
|
118
121
|
useEffect(() => {
|
|
@@ -215,12 +218,17 @@ const Autocomplete = ({ placeholder, options, value, showClear = false, onChange
|
|
|
215
218
|
if (searchText && filteredOptions.length === 0) {
|
|
216
219
|
return _jsx("div", { className: "efp-autocomplete__empty", children: noOptionsLabel });
|
|
217
220
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
221
|
+
// Virtualized: a plan's booth list runs to thousands of options, and rendering, laying out and
|
|
222
|
+
// compositing every one of them froze the frame the dropdown opened in for over half a second.
|
|
223
|
+
if (!scrollElement)
|
|
224
|
+
return null;
|
|
225
|
+
const context = {
|
|
226
|
+
listboxId,
|
|
227
|
+
focusIndex: focusOptionIndex,
|
|
228
|
+
activeIndex: filteredOptions.length === 0 ? activeOptionIndex : null,
|
|
229
|
+
onSelect: (index) => selectOption(currentOptions[index]),
|
|
230
|
+
};
|
|
231
|
+
return (_jsx(Suspense, { fallback: null, children: _jsx(LazyVirtuoso, { ref: virtuosoRef, customScrollParent: scrollElement, data: currentOptions, context: context, computeItemKey: (_index, option) => getOptionKey(option), itemContent: (_index, option) => getOptionLabel(option), components: { List: OptionList, Item: OptionItem }, increaseViewportBy: 200 }) }));
|
|
224
232
|
};
|
|
225
233
|
return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: refAutocomplete, "data-no-drag": true, className: classNames('efp-autocomplete', {
|
|
226
234
|
'is-open': showOptionsDropdown,
|
|
@@ -230,7 +238,7 @@ const Autocomplete = ({ placeholder, options, value, showClear = false, onChange
|
|
|
230
238
|
: undefined, onChange: onInputChange, onFocus: onFocus, onBlur: onBlur, onKeyDown: onKeyDown, value: displayValue, placeholder: placeholder, ref: refSearchInput }), showClear && showOptionsDropdown && displayValue && (_jsx("button", { type: "button", className: "efp-autocomplete__clear", "aria-label": clearLabel, onMouseDown: (e) => {
|
|
231
239
|
e.preventDefault();
|
|
232
240
|
clearValue(true);
|
|
233
|
-
} }))] }), showOptionsDropdown && inline && (_jsx("div", { ref: refOptions, "data-no-drag": true, className: "efp-autocomplete-options efp-autocomplete-options--inline", style: inlineMaxHeight !== null ? { maxHeight: `${inlineMaxHeight}px` } : undefined, children: _jsx("div", { className: "efp-autocomplete-options__scroll", children: renderOptions() }) }))] }), showOptionsDropdown &&
|
|
241
|
+
} }))] }), showOptionsDropdown && inline && (_jsx("div", { ref: refOptions, "data-no-drag": true, className: "efp-autocomplete-options efp-autocomplete-options--inline", style: inlineMaxHeight !== null ? { maxHeight: `${inlineMaxHeight}px` } : undefined, children: _jsx("div", { className: "efp-autocomplete-options__scroll", ref: setScrollElement, children: renderOptions() }) }))] }), showOptionsDropdown &&
|
|
234
242
|
!inline &&
|
|
235
243
|
container &&
|
|
236
244
|
createPortal(_jsx("div", { ref: refOptions, className: "efp-autocomplete-options", style: {
|
|
@@ -241,6 +249,40 @@ const Autocomplete = ({ placeholder, options, value, showClear = false, onChange
|
|
|
241
249
|
left: `${dropdownPosition.left}px`,
|
|
242
250
|
width: `${dropdownPosition.width}px`,
|
|
243
251
|
maxHeight: `${dropdownPosition.maxHeight}px`,
|
|
244
|
-
}, children: _jsx("div", { className: "efp-autocomplete-options__scroll", children: renderOptions() }) }), container)] }));
|
|
252
|
+
}, children: _jsx("div", { className: "efp-autocomplete-options__scroll", ref: setScrollElement, children: renderOptions() }) }), container)] }));
|
|
245
253
|
};
|
|
254
|
+
/**
|
|
255
|
+
* The listbox itself; Virtuoso positions it inside the scroll parent.
|
|
256
|
+
* @param props - Virtuoso's list props, plus the autocomplete's context.
|
|
257
|
+
* @returns The `ul`.
|
|
258
|
+
*/
|
|
259
|
+
function OptionList({ context, ref, ...props }) {
|
|
260
|
+
// Virtuoso types its list element as a `div`; the listbox is a `ul`, which it handles the same.
|
|
261
|
+
return (_jsx("ul", { ...props, ref: ref, role: "listbox", id: context.listboxId }));
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* One option; Virtuoso renders only the ones in and near view.
|
|
265
|
+
* @param props - Virtuoso's item props, plus the autocomplete's context.
|
|
266
|
+
* @returns The `li`.
|
|
267
|
+
*/
|
|
268
|
+
function OptionItem({ context, item: _item, ...props }) {
|
|
269
|
+
const index = props['data-index'];
|
|
270
|
+
const isFocused = index === context.focusIndex;
|
|
271
|
+
const isActive = !isFocused && index === context.activeIndex;
|
|
272
|
+
return (_jsx("li", { ...props, id: `${context.listboxId}-option-${index}`, role: "option", "aria-selected": index === context.activeIndex, className: isFocused ? 'is-focus' : isActive ? 'is-active' : undefined, onMouseDown: (e) => e.preventDefault(), onClick: () => context.onSelect(index) }));
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Loads Virtuoso on first open rather than with the SDK's main chunk, as the floorplan's own lists
|
|
276
|
+
* do; the retry covers a chunk request lost to a flaky network.
|
|
277
|
+
* @returns The module, shaped for `lazy`.
|
|
278
|
+
*/
|
|
279
|
+
function importVirtuoso() {
|
|
280
|
+
return import('react-virtuoso')
|
|
281
|
+
.then((mod) => ({ default: mod.Virtuoso }))
|
|
282
|
+
.catch(() => new Promise((resolve) => {
|
|
283
|
+
setTimeout(() => resolve(importVirtuoso()), 3000);
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
// Typed as the generic component it resolves to; `lazy` alone erases the data and context types.
|
|
287
|
+
const LazyVirtuoso = lazy(importVirtuoso);
|
|
246
288
|
export default Autocomplete;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expofp/ui",
|
|
3
|
-
"version": "3.39.
|
|
3
|
+
"version": "3.39.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ExpoFP SDK internal: shared pure React UI components",
|
|
6
6
|
"homepage": "https://developer.expofp.com/",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"classnames": "^2.2.6",
|
|
34
34
|
"copy-to-clipboard": "^3.2.0",
|
|
35
|
+
"react-virtuoso": "^4.18.7",
|
|
35
36
|
"react-zoom-pan-pinch-sr": "^0.2.0",
|
|
36
37
|
"resize-observer": "^1.0.4",
|
|
37
38
|
"swiper": "^9.4.1",
|