@engie-group/fluid-design-system-react
Version:
Fluid Design System React
202 lines (199 loc) • 10.8 kB
JavaScript
import React__default, { useId, useState, useEffect, useMemo, useRef, useCallback, createElement } from 'react';
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import { useFloating, useRole, useClick, useDismiss, useListNavigation, useInteractions, useMergeRefs, FloatingPortal, FloatingFocusManager } from '../../node_modules/.pnpm/@floating-ui_react@0.27.3_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@floating-ui/react/dist/floating-ui.react.js';
import { useStateControl } from '../../utils/hook.js';
import { Utils } from '../../utils/util.js';
import { NJFormItem } from '../form-item/NJFormItem.js';
import { NJListItem } from '../list/item/NJListItem.js';
import '../list/root/NJListRoot.js';
import '../menu/anchor/NJMenuAnchor.js';
import '../menu/dropdown/NJMenuDropdown.js';
import { NJMenuGroup } from '../menu/group/NJMenuGroup.js';
import '../menu/item/NJMenuItem.js';
import '../popover/anchor/NJPopoverAnchor.js';
import '../popover/NJPopoverContext.js';
import '../popover/NJPopoverInteractionContext.js';
import '../menu/NJMenuContext.js';
import '../menu/NJMenuSelectionContext.js';
import '../menu/NJMenuItemContext.js';
import { offset, flip, size } from '../../node_modules/.pnpm/@floating-ui_react-dom@2.1.6_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@floating-ui/react-dom/dist/floating-ui.react-dom.js';
import { autoUpdate } from '../../node_modules/.pnpm/@floating-ui_dom@1.7.4/node_modules/@floating-ui/dom/dist/floating-ui.dom.js';
const Item = React__default.forwardRef((props, forwardedRef) => {
const { active, children, id, 'aria-label': ariaLabel, ...rest } = props;
return (jsx(NJListItem, { ref: forwardedRef, role: "option", "aria-label": ariaLabel, virtuallyFocused: active, wrapperAsChild: true, children: jsx("button", { id: id, ...rest, children: children }) }));
});
Item.displayName = '';
const NJAutocomplete = React__default.forwardRef((props, forwardedRef) => {
const { className, data = [], searchLimit, inputInstructions, id = useId(), name, isDisabled, isRequired, form, resultsCountMessage, showNumberOfResults = true, showNoResultsMessage = true, listLabel, value: controlledValue, initialValue, onChange, inputValue: controlledInputValue, initialInputValue, onInputValueChange, clearable = true, onClear, clearButtonAriaLabel = 'Clear input', size: scale, label, labelKind, hasSuccess, hasError, subscriptMessage, ...htmlProps } = props;
if (initialValue && controlledValue) {
throw new Error('NJAutocomplete: initialValue and value cannot be used together, the component is either controlled or uncontrolled');
}
if (initialInputValue !== undefined && controlledInputValue !== undefined) {
throw new Error('NJAutocomplete: initialInputValue and inputValue cannot be used together, the component is either controlled or uncontrolled');
}
if (controlledInputValue !== undefined && controlledValue) {
throw new Error('NJAutocomplete: inputValue and value cannot be used together');
}
const filterData = (filterQuery, itemsToFilter, resultLimit) => {
if (!filterQuery) {
return itemsToFilter;
}
return itemsToFilter
.filter((item) => Utils.normalizeAndSearchInText(item?.name, filterQuery))
.slice(0, resultLimit);
};
const [inputValue, setInputValue] = useStateControl(initialInputValue ?? '', controlledInputValue, onInputValueChange);
const [value, setValue] = useStateControl(initialValue, controlledValue, onChange);
const [isFilled, setIsFilled] = useState(false);
useEffect(() => {
if (inputValue)
setIsFilled(inputValue.length > 0);
}, [inputValue]);
const items = useMemo(() => filterData(inputValue, data, searchLimit), [inputValue, data, searchLimit]);
useEffect(() => {
if (inputValue && items.length) {
setActiveIndex(0);
}
else {
setActiveIndex(null);
}
}, [inputValue, items.length]);
const hasMounted = useRef(false);
useEffect(() => {
if (!controlledInputValue && (initialValue || controlledValue)) {
setInputValue(controlledValue?.name ?? initialValue?.name);
}
}, []);
useEffect(() => {
if (controlledValue?.name && hasMounted.current) {
setInputValue(controlledValue?.name);
}
hasMounted.current = true;
}, [controlledValue]);
useEffect(() => {
if (value && controlledInputValue && controlledInputValue !== value.name) {
setValue(undefined);
}
}, [controlledInputValue]);
const [opened, setOpened] = useState(false);
const [activeIndex, setActiveIndex] = useState(null);
const listRef = useRef([]);
const inputRef = useRef(null);
const { refs, floatingStyles, context } = useFloating({
whileElementsMounted: autoUpdate,
open: opened,
onOpenChange: setOpened,
middleware: [
offset(8),
flip({ padding: 10 }),
size({
apply({ rects, elements }) {
Object.assign(elements.floating.style, {
width: `${rects.reference.width}px`
});
},
padding: 10
})
]
});
const role = useRole(context, { role: 'listbox' });
const click = useClick(context, { toggle: false });
const dismiss = useDismiss(context);
const listNav = useListNavigation(context, {
listRef,
activeIndex,
onNavigate: setActiveIndex,
virtual: true,
loop: true
});
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
click,
role,
dismiss,
listNav
]);
const handleChevronClick = () => {
if (isDisabled || htmlProps.readOnly)
return;
setOpened(!opened);
if (inputRef.current) {
inputRef.current.focus({ preventScroll: true });
}
};
const handleClear = () => {
if (isDisabled || htmlProps.readOnly)
return;
setInputValue('');
setValue(undefined);
setOpened(true);
if (onClear && typeof onClear === 'function') {
onClear();
}
if (inputRef.current) {
inputRef.current.focus({ preventScroll: true });
}
};
function handleInputChange(event) {
setOpened(true);
if (value) {
setValue(undefined);
}
setInputValue(event.target.value);
}
function selectItem(item) {
setInputValue(item.name);
setValue(item);
setActiveIndex(null);
setOpened(false);
}
useEffect(() => {
if (opened && items.length === 0 && !showNoResultsMessage) {
setOpened(false);
}
}, [opened, items.length, showNoResultsMessage]);
const inputClass = Utils.classNames('nj-form-item--select', 'nj-form-item--autocomplete', { ['nj-form-item--open']: opened }, className);
/** Update markup to highlight part of an option name. */
const getHighlightedName = useCallback((name) => {
if (!inputValue) {
return name;
}
return Utils.highlightTextAsHtml(name, inputValue);
}, [inputValue]);
const groupHeaderValue = useMemo(() => {
if ((showNumberOfResults && items.length > 0) ||
(showNoResultsMessage && items.length === 0)) {
return resultsCountMessage(items.length);
}
return undefined;
}, [showNumberOfResults, showNoResultsMessage, items.length, resultsCountMessage]);
return (jsxs(Fragment, { children: [jsx("p", { id: `${id}-instructions`, hidden: true, children: inputInstructions }), jsx(NJFormItem, { id: id, ref: refs.setPositionReference, iconName: "keyboard_arrow_down", iconClick: handleChevronClick, iconTitle: "Toggle dropdown", isSelect: true, clearable: !isDisabled && !htmlProps.readOnly && clearable && isFilled, clearButtonAriaLabel: clearButtonAriaLabel, onClear: handleClear, className: inputClass, iconClassName: "nj-form-item__icon", labelClassName: "nj-form-item__label", isDisabled: isDisabled, size: scale, isMultiline: false, label: label, labelKind: labelKind, hasError: hasError, hasSuccess: hasSuccess, subscriptMessage: subscriptMessage, children: jsx("input", { "data-child-name": "inputField", type: "text", ...getReferenceProps({
...htmlProps,
ref: useMergeRefs([refs.setReference, forwardedRef, inputRef]),
onChange: handleInputChange,
value: inputValue ?? '',
placeholder: ' ', // Placeholder must be " " because of webkit browser behavior with floating labels
'aria-autocomplete': 'list',
onKeyDown(event) {
if (event.key === 'Enter' && activeIndex != null && items[activeIndex]) {
selectItem(items[activeIndex]);
}
htmlProps.onKeyDown?.(event);
}
}), id: id, name: name, disabled: isDisabled, required: isRequired, "aria-describedby": `${id}-instructions`, form: form, autoComplete: "off" }) }), jsx("div", { className: "nj-sr-only", "aria-live": "polite", "aria-atomic": "true", children: jsx("p", { children: opened && resultsCountMessage(items.length) }) }), jsx(FloatingPortal, { children: opened && (jsx(FloatingFocusManager, { context: context, initialFocus: -1, visuallyHiddenDismiss: true, children: jsx("div", { className: "nj-menu nj-menu--scrollable", ...getFloatingProps({
ref: refs.setFloating,
style: floatingStyles
}), children: jsx(NJMenuGroup, { "aria-label": listLabel, header: groupHeaderValue, children: items.map((item, index) => (createElement(Item, { ...getItemProps({
id: `${id}-option-${index}`,
ref(node) {
listRef.current[index] = node;
},
onClick: () => {
selectItem(item);
}
}), "aria-label": item?.name, key: item?.value, active: index === activeIndex },
jsx("span", { dangerouslySetInnerHTML: {
__html: getHighlightedName(item?.name)
} })))) }) }) })) })] }));
});
NJAutocomplete.displayName = 'NJAutocomplete';
export { NJAutocomplete };