@wonderflow/react-components
Version:
UI components from Wonderflow's Wanda design system
119 lines (118 loc) • 6.57 kB
JavaScript
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';