UNPKG

@wonderflow/react-components

Version:

UI components from Wonderflow's Wanda design system

119 lines (118 loc) 6.57 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; /* * Copyright 2022-2023 Wonderflow Design Team * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { useClickAway, useDebounce, useFocusWithin, useKeyPress, useSize, } from 'ahooks'; import { AnimatePresence, domMax, LazyMotion, m, } from 'framer-motion'; import { Children, cloneElement, forwardRef, isValidElement, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { createPortal } from 'react-dom'; import { mergeRefs } from 'react-merge-refs'; import { usePopperTooltip } from 'react-popper-tooltip'; import { useUIDSeed } from 'react-uid'; import { Menu, Skeleton, Stack, Text, Textfield, } from '../..'; import { isBrowser } from '../../utils/browser'; import * as styles from './autocomplete.module.css'; import { AutocompleteOption } from './autocomplete-option'; const AutocompleteAnimation = { visible: { y: 0, opacity: 1, transition: { ease: [0, 0, 0.34, 1], duration: 0.1, }, }, hidden: { y: -5, opacity: 0, transition: { ease: [0.3, 0.07, 1, 1], duration: 0.1, }, }, }; export const Autocomplete = forwardRef(({ children, onChange, disabled, readOnly, value: val, busy, maxHeight = '200px', emptyContent = 'No items to show', root, ...otherProps }, forwardedRef) => { const seedID = useUIDSeed(); const autocompleteRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const [query, setQuery] = useState(val ? String(val) : ''); const [value, setValue] = useState(val ? String(val) : ''); const [optionsValues, setOptionValues] = useState([]); const isInteractive = useMemo(() => !disabled && !readOnly, [disabled, readOnly]); const debounceQuery = useDebounce(query, { wait: 100 }); const filteredOptions = useMemo(() => { const items = Children.toArray(children); return debounceQuery ? items.filter((o) => { const props = isValidElement(o) && o.props.children; const stringToMatch = typeof props === 'string' ? props : props.join(''); return stringToMatch?.toLowerCase().includes(debounceQuery.toLowerCase()); }) : items; }, [debounceQuery, children]); const isFocusWithin = useFocusWithin(autocompleteRef, { onChange: (isFocusWithin) => { const m = isBrowser() && document.getElementById(seedID('autocomplete-menu')); if (isFocusWithin || m) { setIsOpen(true); } else { setIsOpen(false); } }, }); useClickAway(() => setIsOpen(false), autocompleteRef); useKeyPress('esc', () => setIsOpen(false)); const { getTooltipProps, setTooltipRef, setTriggerRef, triggerRef, // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars visible, } = usePopperTooltip({ delayShow: 0, delayHide: 0, trigger: null, visible: isInteractive ? isOpen : false, closeOnTriggerHidden: true, placement: 'bottom-start', offset: [0, 8], }); const triggerSize = useSize(triggerRef); const handleOptionClick = useCallback((optionValue, optionContent) => { setQuery(optionContent); setValue(optionValue); onChange?.({ query: optionContent, value: optionValue }); setIsOpen(false); }, [onChange]); const handleFilter = useCallback(({ currentTarget }) => { const targetValue = currentTarget.value.toLowerCase(); setQuery(currentTarget.value); setValue(currentTarget.value); onChange?.({ query: currentTarget.value, value: optionsValues.includes(targetValue) ? optionsValues[optionsValues.indexOf(targetValue)] : '', }); }, [onChange, optionsValues]); useEffect(() => { const currentValues = Children.map(filteredOptions, (o) => (typeof o.props.children === 'string' ? o.props.children.toLowerCase() : o.props.children.join('').toLowerCase())); if (val) setValue(String(val)); setOptionValues(currentValues); }, [filteredOptions, val]); return (_jsxs("div", { ref: autocompleteRef, className: styles.Autocomplete, "data-focus-within": isFocusWithin, children: [_jsx(Textfield, { "aria-haspopup": true, "aria-controls": seedID('autocomplete-menu'), "aria-expanded": isOpen, role: "combobox", ref: mergeRefs([setTriggerRef, forwardedRef]), id: seedID('autocomplete-trigger'), onChange: handleFilter, "data-testid": "Autocomplete", "data-current-value": value, value: query, autoComplete: "off", disabled: disabled, readOnly: readOnly, ...otherProps }, seedID('autocomplete-trigger')), isBrowser() && visible && createPortal(_jsx(AnimatePresence, { children: _jsx("div", { ref: setTooltipRef, ...getTooltipProps({ className: styles.PopUp, style: { minInlineSize: triggerSize ? (triggerSize.width + 2) : 'auto' } }), children: _jsx(LazyMotion, { features: domMax, strict: true, children: _jsx(m.div, { variants: AutocompleteAnimation, initial: "hidden", animate: "visible", exit: "hidden", children: _jsxs(Menu, { role: "listbox", id: seedID('autocomplete-menu'), className: styles.OptionsList, maxHeight: maxHeight, "aria-labelledby": seedID('autocomplete-trigger'), children: [(filteredOptions.length === 0 && !busy) && _jsx(Text, { as: "div", textAlign: "center", children: emptyContent }), busy ? _jsx(Stack, { hPadding: 8, as: "span", children: _jsx(Skeleton, { count: 3 }) }) : Children.map(filteredOptions, child => isValidElement(child) && cloneElement(child, { onClick: handleOptionClick, }))] }) }) }) }) }), root ?? document.body)] })); }); Autocomplete.Option = AutocompleteOption; Autocomplete.displayName = 'Autocomplete';