UNPKG

@engie-group/fluid-design-system-react

Version:

Fluid Design System React

202 lines (199 loc) 10.8 kB
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 };