UNPKG

@amaui/ui-react

Version:
670 lines (665 loc) 26.8 kB
import _extends from "@babel/runtime/helpers/extends"; import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; const _excluded = ["tonal", "color", "size", "version", "valueInput", "valueInputDefault", "onChangeInput", "value", "valueDefault", "onChange", "options", "label", "multiple", "prefix", "sufix", "start", "end", "autoWidth", "readOnly", "getLabel", "renderValues", "renderChip", "renderOption", "equal", "equalInput", "filter", "clear", "loading", "autoSelectOnBlur", "blurOnSelect", "noOptions", "noOptionsObject", "startOptionsObject", "noOptionsElement", "startOptionsElement", "endOptionsElement", "endOptionsObject", "openOnFocus", "closeOnSelect", "clearOnEscape", "groupBy", "limit", "filterOutSelectedOptions", "selectOnFocus", "clearOnBlur", "clearInputOnSelect", "chip", "fullWidth", "noInputValue", "disabled", "IconClear", "IconDropdown", "WrapperProps", "ChipProps", "ListProps", "MenuProps", "IconButtonProps", "InputProps", "IconProps", "className", "style", "children"]; function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } import React from 'react'; import { getObjectValue, is, isEnvironment } from '@amaui/utils'; import { classNames, style as styleMethod, useAmauiTheme } from '@amaui/style-react'; import IconMaterialClose from '@amaui/icons-material-rounded-react/IconMaterialCloseW100'; import IconMaterialArrowDropDown from '@amaui/icons-material-rounded-react/IconMaterialArrowDropDownW100'; import MenuElement from '../Menu'; import ChipElement from '../Chip'; import TypeElement from '../Type'; import ListElement from '../List'; import ListItemElement from '../ListItem'; import TextFieldElement from '../TextField'; import IconButtonElement from '../IconButton'; import RoundProgressElement from '../RoundProgress'; import ListSubheaderElement from '../ListSubheader'; import LineElement from '../Line'; import { iconFontSize, staticClassName } from '../utils'; const useStyle = styleMethod(theme => ({ root: { width: '100%', flex: 'unset', '& .amaui-TextField-input': { flex: '1 1 auto', width: 'auto' } }, wrapper: { position: 'relative' }, input_: { alignSelf: 'center' }, input: _objectSpread({ display: 'inline-flex', margin: '0px', border: '0px', color: theme.palette.text.default.primary, background: 'transparent', '-webkit-tap-highlight-color': 'transparent', textAlign: 'start', borderRadius: `${theme.shape.radius.unit / 2}px ${theme.shape.radius.unit / 2}px 0 0`, minHeight: 20 }, theme.typography.values.b2), inputWrapper_multiple_size_small: { minHeight: '48px', columnGap: '6px', rowGap: '12px' }, inputWrapper_multiple_size_regular: { minHeight: '56px', columnGap: '8px', rowGap: '16px' }, inputWrapper_multiple_size_large: { minHeight: '64px', columnGap: '10px', rowGap: '20px' }, multiple: { '&.amaui-TextField-input-wrapper': { height: 'unset' } }, chipGroup_padding: { paddingTop: theme.methods.space.value(0.5, 'px') }, arrow: { transition: theme.methods.transitions.make('transform') }, arrow_open: { transform: 'rotate(-180deg)' }, open: {}, readOnly: { '&.amaui-TextField-input-wrapper': { cursor: 'default' } }, list: { maxHeight: '250px', overflow: 'auto' }, limitText: { alignSelf: 'center' }, roundProgress: { padding: `0 ${theme.methods.space.value(1, 'px')}` }, disabled: { '&.amaui-TextField-input-wrapper': { cursor: 'default' } } }), { name: 'amaui-AutoComplete' }); const getText = value => { const value_ = value?.name || value?.label || value?.primary || value?.secondary || value?.tertiary || value?.children || value?.value || value; return is('simple', value_) ? String(value_) : ''; }; const getValue = value => value?.value !== undefined ? value.value : value; const AutoComplete = /*#__PURE__*/React.forwardRef((props_, ref) => { const theme = useAmauiTheme(); const props = React.useMemo(() => _objectSpread(_objectSpread(_objectSpread({}, theme?.ui?.elements?.all?.props?.default), theme?.ui?.elements?.amauiAutoComplete?.props?.default), props_), [props_]); const Line = React.useMemo(() => theme?.elements?.Line || LineElement, [theme]); const Menu = React.useMemo(() => theme?.elements?.Menu || MenuElement, [theme]); const Chip = React.useMemo(() => theme?.elements?.Chip || ChipElement, [theme]); const Type = React.useMemo(() => theme?.elements?.Type || TypeElement, [theme]); const List = React.useMemo(() => theme?.elements?.List || ListElement, [theme]); const ListItem = React.useMemo(() => theme?.elements?.ListItem || ListItemElement, [theme]); const TextField = React.useMemo(() => theme?.elements?.TextField || TextFieldElement, [theme]); const IconButton = React.useMemo(() => theme?.elements?.IconButton || IconButtonElement, [theme]); const RoundProgress = React.useMemo(() => theme?.elements?.RoundProgress || RoundProgressElement, [theme]); const ListSubheader = React.useMemo(() => theme?.elements?.ListSubheader || ListSubheaderElement, [theme]); const { tonal = true, color = 'primary', size = 'regular', version = 'filled', valueInput: valueInput_, valueInputDefault, onChangeInput: onChangeInput_, value: value_, valueDefault, onChange: onChange_, options: options_ = [], label, multiple, prefix, sufix, start, end, autoWidth = true, readOnly, getLabel: getLabel_, renderValues: renderValues_, renderChip, renderOption, equal, equalInput, filter, clear = true, loading, autoSelectOnBlur, blurOnSelect = false, noOptions, noOptionsObject, startOptionsObject, noOptionsElement, startOptionsElement, endOptionsElement, endOptionsObject, openOnFocus = true, closeOnSelect, clearOnEscape, groupBy, limit, filterOutSelectedOptions, selectOnFocus, clearOnBlur, clearInputOnSelect, chip, fullWidth, noInputValue, disabled, IconClear = IconMaterialClose, IconDropdown = IconMaterialArrowDropDown, WrapperProps, ChipProps, ListProps, MenuProps, IconButtonProps, InputProps, IconProps, className, style, children: children_ } = props, other = _objectWithoutProperties(props, _excluded); const children = React.Children.toArray(children_); const [init, setInit] = React.useState(false); const [valueInput, setValueInput] = React.useState(valueInputDefault !== undefined ? valueInputDefault : valueInput_); const [value, setValue] = React.useState((valueDefault !== undefined ? valueDefault : value_) || []); const [focus, setFocus] = React.useState(false); const [open, setOpen] = React.useState(false); const [mouseDown, setMouseDown] = React.useState(false); const [options, setOptions] = React.useState(options_); const [free, setFree] = React.useState(false); const { classes } = useStyle(); const refs = { root: React.useRef(undefined), wrapper: React.useRef(undefined), value: React.useRef(undefined), valueInput: React.useRef(undefined), menu: React.useRef(undefined), input: React.useRef(undefined), optionsProps: React.useRef(options_), ids: { list: React.useId() } }; refs.value.current = value; refs.valueInput.current = valueInput; refs.optionsProps.current = options_; const styles = { root: {}, menu: {} }; if (MenuProps?.portal && autoWidth) { styles.menu.width = refs.wrapper.current?.clientWidth; } React.useEffect(() => { const rootDocument = isEnvironment('browser') ? refs.root.current?.ownerDocument || window.document : undefined; const method = event => { if (event.key === 'Escape') { if (clearOnEscape) onClear(); onClose(true); } }; rootDocument.addEventListener('keydown', method); rootDocument.addEventListener('mouseup', onMouseUp); setInit(true); return () => { // Clean up rootDocument.removeEventListener('mouseup', onMouseUp); rootDocument.removeEventListener('keydown', method); }; }, []); React.useEffect(() => { const option = (refs.optionsProps.current || []).find(item_ => isEqualToInput(refs.valueInput.current, item_)); if (!!valueInput?.length && !open && !option && !disabled && !readOnly) setOpen(!free); }, [valueInput, free]); React.useEffect(() => { if (value_ !== undefined && value_ !== value) setValue(value_); }, [value_]); React.useEffect(() => { if (valueInput_ !== undefined && valueInput_ !== valueInput) setValueInput(valueInput_); }, [valueInput_]); React.useEffect(() => { if (init && loading) { setOpen(true); updateOptions(); } }, [loading]); React.useEffect(() => { updateOptions(undefined, options_); }, [options_]); const updateOptions = function () { let valueInputNew = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : refs.valueInput.current; let newOptions = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined; let optionsValue = refs.optionsProps.current; // reset setFree(false); if (loading) optionsValue = [{ label: 'Loading...', version: 'text' }];else if (newOptions) optionsValue = newOptions;else optionsValue = is('function', filter) ? filter(valueInputNew, refs.optionsProps.current) : refs.optionsProps.current.filter(option => isEqualToInput(valueInputNew, option)); if (!optionsValue.length) { if (noOptions) optionsValue.push(noOptionsObject !== undefined ? noOptionsObject : { primary: 'No options', version: 'text', noOptions: true });else { setOpen(false); setFree(true); setOptions(optionsValue); return; } } if (!loading) { if (startOptionsObject) optionsValue.unshift(startOptionsObject); if (endOptionsObject) optionsValue.push(endOptionsObject); } setOptions(optionsValue); }; const onMouseDown = React.useCallback(event => { if (!disabled && !readOnly) setMouseDown(true); }, [readOnly, disabled]); const onMouseUp = React.useCallback(event => { if (!disabled && !readOnly) setMouseDown(false); }, [readOnly, disabled]); const onFocus = React.useCallback(event => { if (!disabled && !readOnly) { setFocus(true); if (selectOnFocus) setTimeout(() => refs.input.current.select()); } }, [readOnly, disabled]); const onBlur = event => { if (!disabled && !readOnly) setFocus(false); }; const onClick = React.useCallback(event => { if (!disabled && !readOnly) setOpen(open_ => { if (!open_) { if (!openOnFocus) return open_; refs.input.current.focus(); // if input wrapper overflows event.target.scrollTo(0, 0); } return !open_; }); }, [readOnly, disabled]); const onClickArrowDown = React.useCallback(event => { if (!disabled && !readOnly) setOpen(open_ => { if (!open_) refs.input.current.focus(); return !open_; }); }, [readOnly, disabled]); const onEnterKeyDown = React.useCallback(event => { if (event.key === 'Enter' && !disabled && !readOnly) setOpen(open_ => { if (!open_) { if (!openOnFocus) return open_; refs.input.current.focus(); } return !open_; }); }, [readOnly, disabled]); const onClose = function () { let refocus = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; if (!disabled && !readOnly) { setOpen(open_ => { if (open_) { if (refocus) refs.input.current.focus(); if (clearOnBlur) { const option = options.find(item_ => isEqualToInput(refs.valueInput.current, item_)); if (!option) onClear(); } } return false; }); } }; const onExited = () => { if (!disabled && !readOnly) { if (!open) { const option = (refs.optionsProps.current || []).find(item_ => isEqualToInput(refs.valueInput.current, item_)); // Update options to all values // if value is one of the option values if (option || !refs.valueInput.current || options[0]?.noOptions) updateOptions(undefined, refs.optionsProps.current); } } }; const onChange = valueNew => { // Inner controlled value if (!props.hasOwnProperty('value')) setValue(valueNew); if (is('function', onChange_)) onChange_(valueNew); }; const onChangeInput = valueNew => { if (!disabled && !readOnly) { updateOptions(valueNew); if (!open) setOpen(true); // Inner controlled value if (!props.hasOwnProperty('valueInput')) setValueInput(valueNew); if (is('function', onChangeInput_)) onChangeInput_(valueNew); } }; const onClear = React.useCallback(function () { let refocus = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; if (!disabled && !readOnly) { onChangeInput(''); onChange(multiple ? [] : null); if (refocus) refs.input.current.focus(); } }, [multiple, readOnly, disabled]); const onClearInput = React.useCallback(function () { let refocus = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; if (!disabled && !readOnly) { onChangeInput(''); if (refocus) refs.input.current.focus(); } }, []); const isEqual = (value1, value2) => is('function', equal) ? equal(value1, value2) : getValue(value1) === getValue(value2); const isEqualToInput = function () { let inputValue = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : refs.valueInput.current; let item = arguments.length > 1 ? arguments[1] : undefined; return is('function', equalInput) ? equalInput(inputValue, item) : getText(item)?.toLowerCase().includes(inputValue?.toLowerCase()); }; const onSelect = valueNew => { const values = multiple ? is('array', value) ? value : [value] : value; const selected = multiple ? !!values.find(item => isEqual(valueNew, item)) : isEqual(valueNew, value); if (!selected) { onChange(!multiple ? valueNew : [...values, valueNew]); if (!multiple) clearInputOnSelect ? onClearInput() : onChangeInput(getText(valueNew));else if (clearInputOnSelect) onClearInput(); } }; const onUnselect = valueNew => { if (multiple) { let values = [...(is('array', value) ? value : [value])]; values = values.filter(item => !isEqual(valueNew, item)); onChange(values); } }; const items = React.useMemo(() => { return (options_ || []).map(item => _objectSpread(_objectSpread({}, item), {}, { name: String(item?.name !== undefined ? item?.name : item?.value !== undefined ? item.value : item), value: item?.value !== undefined ? item?.value : item })); }, [options_]); const getLabel = (item, propsOther) => { if (is('function', getLabel_)) return getLabel_(item, propsOther); const properties = ['name', 'label', 'primary', 'secondary', 'tertiary', 'value', 'children']; const objects = [item, item?.props].filter(Boolean); for (const itemObject of objects) { if (is('simple', itemObject)) return itemObject; const valueItem = getObjectValue(itemObject, ...properties); if (valueItem !== undefined) return valueItem; } return 'No name'; }; const renderValue = function () { let itemValue = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : value; const item = !!items?.length ? items.find(item_ => getValue(item_) === getValue(itemValue)) : children.find(item_ => getValue(item_.props?.value) === getValue(itemValue)); return item ? getLabel(item, props) : getLabel(itemValue, props) || ''; }; const renderValues = renderValues_ || function () { let value__ = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : refs.value.current; let onUnselectMethod = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : onUnselect; if (multiple) { if (chip) { let values = value__; if (is('number', limit) && !open) values = values.slice(0, limit); values = values.map((item, index) => { const other_ = { key: index, onClick: event => { event.preventDefault(); event.stopPropagation(); }, onRemove: event => { event.preventDefault(); event.stopPropagation(); onUnselectMethod(item); }, input: true }; if (is('function', renderChip)) return renderChip(item, renderValue(item), other_); return /*#__PURE__*/React.createElement(Chip, _extends({ key: index, size: "small" }, ChipProps, other_), renderValue(item)); }); if (is('number', limit) && !open && value.length - limit > 0) values.push( /*#__PURE__*/React.createElement(Type, { color: "default", className: classes.limitText }, "+", value.length - limit)); return values; } return value__.map(item => renderValue(item)).join(', '); } return renderValue(value); }; let optionsToUse = options; if (filterOutSelectedOptions) { optionsToUse = optionsToUse.filter(option => { const selected = !!(is('array', value) ? value : [value])?.find(item => isEqual(item, option)); return !selected; }); } const groups = {}; if (is('function', groupBy)) { optionsToUse.forEach(option => { const valueForGroupBy = groupBy(option) || 'Other'; if (!groups[valueForGroupBy]) groups[valueForGroupBy] = []; groups[valueForGroupBy].push(option); }); optionsToUse = []; if (Object.keys(groups).length) Object.keys(groups).forEach(item => { const array = groups[item]; optionsToUse.push({ label: item, version: 'subheader' }, ...array); }); } const renderOptionValue = values => { const result = values.map((item, index) => { let other_ = {}; const button = item.version === undefined || item.version === 'button'; const selected = !!(is('array', value) ? value : [value])?.find(item_ => isEqual(item, item_)); if (button) { other_ = { primary: getLabel(item), value: item, button, selected, onClick: event => { if (multiple && selected) onUnselect(item);else onSelect(item); if (is('function', item.props?.onClick)) item.props?.onClick(event); if (blurOnSelect) { if (closeOnSelect) setOpen(false); refs.input.current.blur(); } else if (closeOnSelect) onClose(); } }; } else { other_.secondary = getLabel(item); } other_.onMouseUp = onMouseUp; other_.onMouseDown = onMouseDown; if (item.noOptions) { if (noOptionsElement) return /*#__PURE__*/React.cloneElement(noOptionsElement, { key: 'noOptions' }); } return is('function', renderOption) ? renderOption(item, index, _objectSpread(_objectSpread({}, other_), item.props)) : /*#__PURE__*/React.createElement(ListItem, _extends({ key: item.value !== undefined ? item.value : index, role: "option", preselected: !open ? false : undefined }, other_, item.props)); }); if (startOptionsElement) result.unshift( /*#__PURE__*/React.cloneElement(startOptionsElement, { key: 'startOptionsElement' })); if (endOptionsElement) result.push( /*#__PURE__*/React.cloneElement(endOptionsElement, { key: 'endOptionsElement' })); return result; }; const renderList = () => { if (Object.keys(groups).length) { return Object.keys(groups).map((item, index) => /*#__PURE__*/React.createElement("li", { key: index, style: { width: '100%' } }, /*#__PURE__*/React.createElement(ListSubheader, { Component: "div" }, item), /*#__PURE__*/React.createElement(List, { size: size, paddingVertical: "none", menu: true }, renderOptionValue(groups[item])))); } else return renderOptionValue(optionsToUse); }; const endIcons = [end, ...(!readOnly ? [...(loading ? [/*#__PURE__*/React.createElement(RoundProgress, { key: 1, className: classes.roundProgress, size: "small" })] : []), ...(clear ? [!!(multiple ? value.length : valueInput) && /*#__PURE__*/React.createElement(IconButton, _extends({ onClick: onClear, size: "small", fontSize: iconFontSize, "aria-label": "Input clear" }, IconButtonProps), /*#__PURE__*/React.createElement(IconClear, IconProps))] : []), /*#__PURE__*/React.createElement(IconButton, _extends({ key: 3, onClick: onClickArrowDown, size: "small", fontSize: iconFontSize, "aria-expanded": open, "aria-controls": refs.ids.list, InteractionProps: { clear: !!(multiple ? value.length : valueInput) } }, IconButtonProps), /*#__PURE__*/React.createElement(IconDropdown, _extends({}, IconProps, { className: classNames([IconProps?.className, classes.arrow, open && classes.arrow_open]) })))] : [])].filter(Boolean); if (mouseDown) refs.input.current.focus(); const menuItems = renderList(); return /*#__PURE__*/React.createElement(Line, _extends({ ref: refs.wrapper, gap: 0, direction: "column", fullWidth: fullWidth }, WrapperProps, { className: classNames([staticClassName('AutoComplete', theme) && ['amaui-AutoComplete-wrapper', fullWidth && 'amaui-full-width'], WrapperProps?.className, classes.wrapper]) }), /*#__PURE__*/React.createElement(TextField, _extends({ ref: refs.input, rootRef: item => { if (ref) { if (is('function', ref)) ref(item);else ref.current = item; } refs.root.current = item; }, onBlur: onBlur, onFocus: onFocus, value: valueInput, onChange: onChangeInput, enabled: open || focus || mouseDown || !!(multiple ? !!value.length || valueInput : valueInput), focus: open || focus || mouseDown, className: classNames([staticClassName('AutoComplete', theme) && ['amaui-AutoComplete-root', `amaui-AutoComplete-version-${version}`, `amaui-AutoComplete-size-${size}`, open && `amaui-AutoComplete-open`, mouseDown && `amaui-AutoComplete-mouse-down`, focus && `amaui-AutoComplete-focus`, loading && `amaui-AutoComplete-loading`], className, classes.root, open && classes.open, disabled && classes.disabled]), tonal: tonal, color: color, size: size, version: version, label: label, prefix: prefix, sufix: sufix, start: start, end: endIcons, readOnly: readOnly, endVerticalAlign: "center", role: "combobox", "aria-autocomplete": "list", "aria-multiselectable": multiple, "aria-controls": refs.ids.list, "aria-expanded": open, "aria-haspopup": "listbox", "aria-labelledby": label, "aria-disabled": disabled, fullWidth: fullWidth, disabled: disabled, InputWrapperProps: { className: classNames([staticClassName('AutoComplete', theme) && ['amaui-AutoComplete-input-wrapper'], classes.inputWrapper, multiple && [classes.multiple, classes[`inputWrapper_multiple_size_${size}`]], chip && classes.chip, open && classes.open, readOnly && classes.readOnly]), onMouseDown, onMouseUp, onClick, onKeyDown: onEnterKeyDown }, inputProps: _objectSpread(_objectSpread({ disabled: multiple, readOnly: multiple }, InputProps), {}, { className: classNames([InputProps?.className, multiple && classes.input_]) }), style: _objectSpread(_objectSpread({}, styles.root), style) }, other), !noInputValue && multiple && !chip && !!value.length && /*#__PURE__*/React.createElement("div", { ref: refs.value, tabIndex: 0, onFocus: onFocus, onBlur: onBlur, onMouseDown: onMouseDown, onKeyDown: onEnterKeyDown, className: classNames([staticClassName('AutoComplete', theme) && ['amaui-AutoComplete-input', multiple && [chip && `amaui-AutoComplete-chip`, open && `amaui-AutoComplete-open`, readOnly && `amaui-Select-readOnly`]], multiple && [classes.input, chip && classes.chip, open && classes.open, readOnly && classes.readOnly]]) }, renderValues(value, onUnselect)), !noInputValue && multiple && chip && !!value.length && renderValues(value, onUnselect)), /*#__PURE__*/React.createElement(Menu, _extends({ ref: refs.menu, open: open && !!menuItems?.length, autoSelectOnBlur: autoSelectOnBlur, portal: true, onClose: () => onClose(false), anchorElement: refs.root.current, onExited: onExited, menuItems: menuItems, transformOrigin: "center top", transformOriginSwitch: "center bottom", maxWidth: "unset", AppendProps: { alignment: 'start' }, ModalProps: { // focus: !MenuProps.portal freezeScroll: false }, ListProps: _objectSpread(_objectSpread({ menu: true, paddingVertical: is('function', groupBy) && !!options.length ? 'none' : undefined, size, role: 'listbox', id: refs.ids.list, 'aria-label': label }, ListProps), {}, { className: classNames([ListProps?.className, classes.list]) }) }, MenuProps, { style: _objectSpread(_objectSpread({}, styles.menu), MenuProps?.menu), className: classNames([MenuProps?.className]) }))); }); AutoComplete.displayName = 'amaui-AutoComplete'; export default AutoComplete;