UNPKG

@spark-web/combobox

Version:

--- title: Combobox storybookPath: forms-combobox--default isExperimentalPackage: true ---

412 lines (401 loc) 20.1 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _objectSpread = require('@babel/runtime/helpers/objectSpread2'); var _toConsumableArray = require('@babel/runtime/helpers/toConsumableArray'); var _typeof = require('@babel/runtime/helpers/typeof'); var _asyncToGenerator = require('@babel/runtime/helpers/asyncToGenerator'); var _slicedToArray = require('@babel/runtime/helpers/slicedToArray'); var _regeneratorRuntime = require('@babel/runtime/regenerator'); var field = require('@spark-web/field'); var theme = require('@spark-web/theme'); var react = require('react'); var ReactSelect = require('react-select'); var _objectWithoutProperties = require('@babel/runtime/helpers/objectWithoutProperties'); var a11y = require('@spark-web/a11y'); var box = require('@spark-web/box'); var icon = require('@spark-web/icon'); var spinner = require('@spark-web/spinner'); var text = require('@spark-web/text'); var internal = require('@spark-web/utils/internal'); var jsxRuntime = require('@emotion/react/jsx-runtime'); function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; } var _regeneratorRuntime__default = /*#__PURE__*/_interopDefault(_regeneratorRuntime); var ReactSelect__default = /*#__PURE__*/_interopDefault(ReactSelect); var _excluded = ["children"]; var useReactSelectComponentsOverride = function useReactSelectComponentsOverride(_ref) { var data = _ref.data, startAdornment = _ref.startAdornment; var _useFieldContext = field.useFieldContext(), _useFieldContext2 = _slicedToArray(_useFieldContext, 2), invalid = _useFieldContext2[0].invalid, fieldProps = _useFieldContext2[1]; return react.useMemo(function () { return { DropdownIndicator: function DropdownIndicator(props) { return jsxRuntime.jsx(ReactSelect.components.DropdownIndicator, _objectSpread(_objectSpread({}, props), {}, { children: jsxRuntime.jsx(icon.ChevronDownIcon, { size: "xxsmall", tone: "muted" }) })); }, Input: function Input(props) { return jsxRuntime.jsx(ReactSelect.components.Input, _objectSpread(_objectSpread(_objectSpread({}, props), data ? internal.buildDataAttributes(data) : undefined), {}, { "aria-invalid": fieldProps['aria-invalid'], "aria-describedby": fieldProps['aria-describedby'] })); }, IndicatorSeparator: function IndicatorSeparator() { return null; }, LoadingIndicator: function LoadingIndicator() { return null; }, LoadingMessage: function LoadingMessage(props) { return jsxRuntime.jsx(ReactSelect.components.LoadingMessage, _objectSpread(_objectSpread({}, props), {}, { children: jsxRuntime.jsx(box.Box, { paddingY: "large", children: jsxRuntime.jsx(spinner.Spinner, { size: "xsmall", tone: "primary" }) }) })); }, NoOptionsMessage: function NoOptionsMessage(props) { return jsxRuntime.jsx(ReactSelect.components.NoOptionsMessage, _objectSpread(_objectSpread({}, props), {}, { children: jsxRuntime.jsx(box.Box, { paddingY: "large", children: jsxRuntime.jsx(text.Text, { children: "No matching results" }) }) })); }, SingleValue: function SingleValue(_ref2) { var children = _ref2.children, props = _objectWithoutProperties(_ref2, _excluded); return jsxRuntime.jsx(ReactSelect.components.SingleValue, _objectSpread(_objectSpread({}, props), {}, { children: jsxRuntime.jsx(box.Box, { data: invalid ? { invalid: invalid } : undefined, children: children }) })); }, Control: function Control(props) { return jsxRuntime.jsxs(ReactSelect.components.Control, _objectSpread(_objectSpread({}, props), {}, { children: [startAdornment, props.children] })); } }; }, [data, fieldProps, invalid, startAdornment]); }; var useReactSelectStylesOverride = function useReactSelectStylesOverride(_ref3) { var invalid = _ref3.invalid; var theme$1 = theme.useTheme(); var _useText = text.useText({ baseline: false, tone: 'neutral', size: 'standard', weight: 'regular' }), _useText2 = _slicedToArray(_useText, 2), responsiveTextStyles = _useText2[0], textStyles = _useText2[1]; var _useText3 = text.useText({ baseline: true, tone: 'muted', size: 'xsmall', weight: 'semibold' }), _useText4 = _slicedToArray(_useText3, 2), responsiveGroupHeadingStyles = _useText4[0], groupHeadingStyles = _useText4[1]; var focusRingStyles = a11y.useFocusRing({ always: true }); return { control: function control(provided, state) { var _theme$components$tex, _theme$components$tex2, _theme$components$tex3, _theme$components$tex4, _theme$components$tex5, _theme$components$tex6, _theme$components$tex7; return _objectSpread(_objectSpread(_objectSpread(_objectSpread(_objectSpread(_objectSpread({}, provided), responsiveTextStyles), textStyles), { borderColor: (_theme$components$tex = theme$1.components.textInput) === null || _theme$components$tex === void 0 ? void 0 : _theme$components$tex.borderColor }), state.isFocused ? (_theme$components$tex2 = (_theme$components$tex3 = theme$1.components.textInput) === null || _theme$components$tex3 === void 0 ? void 0 : _theme$components$tex3.focused) !== null && _theme$components$tex2 !== void 0 ? _theme$components$tex2 : focusRingStyles : invalid ? { borderColor: theme$1.color.foreground.critical } : (_theme$components$tex4 = theme$1.components.textInput) !== null && _theme$components$tex4 !== void 0 && _theme$components$tex4.boxShadow ? { boxShadow: (_theme$components$tex5 = theme$1.components.textInput) === null || _theme$components$tex5 === void 0 ? void 0 : _theme$components$tex5.boxShadow } : {}), (_theme$components$tex6 = theme$1.components.textInput) !== null && _theme$components$tex6 !== void 0 && _theme$components$tex6.hover ? { ':hover': (_theme$components$tex7 = theme$1.components.textInput) === null || _theme$components$tex7 === void 0 ? void 0 : _theme$components$tex7.hover } : {}); }, dropdownIndicator: function dropdownIndicator(provided, state) { return _objectSpread(_objectSpread({}, provided), {}, { transitionProperty: 'transform', transitionTimingFunction: 'linear', transitionDuration: '150ms' }, state.isFocused ? { transform: 'rotate(180deg)' } : {}); }, group: function group(provided) { return _objectSpread(_objectSpread(_objectSpread(_objectSpread({}, provided), responsiveGroupHeadingStyles), groupHeadingStyles), {}, { padding: 0, margin: 0 }); }, groupHeading: function groupHeading(provided) { return _objectSpread(_objectSpread(_objectSpread(_objectSpread({}, provided), responsiveGroupHeadingStyles), groupHeadingStyles), {}, { padding: theme$1.spacing.medium, paddingBottom: theme$1.spacing.small, margin: 0 }); }, menu: function menu(provided) { var _theme$components$tex8, _theme$components$tex9, _theme$components$tex10; return _objectSpread(_objectSpread({}, provided), {}, { padding: (_theme$components$tex8 = theme$1.components.textInput) === null || _theme$components$tex8 === void 0 || (_theme$components$tex8 = _theme$components$tex8.menu) === null || _theme$components$tex8 === void 0 ? void 0 : _theme$components$tex8.padding, boxShadow: (_theme$components$tex9 = theme$1.components.textInput) === null || _theme$components$tex9 === void 0 || (_theme$components$tex9 = _theme$components$tex9.menu) === null || _theme$components$tex9 === void 0 ? void 0 : _theme$components$tex9.boxShadow, borderRadius: (_theme$components$tex10 = theme$1.components.textInput) === null || _theme$components$tex10 === void 0 || (_theme$components$tex10 = _theme$components$tex10.menu) === null || _theme$components$tex10 === void 0 ? void 0 : _theme$components$tex10.borderRadius }); }, menuList: function menuList(provided) { return _objectSpread(_objectSpread({}, provided), {}, { padding: 0, display: 'flex', flexDirection: 'column', gap: theme$1.spacing.xsmall }); }, option: function option(provided, state) { var _theme$components$tex11, _theme$components$tex12, _theme$components$tex13, _theme$components$tex14, _theme$components$tex15, _theme$components$tex16, _theme$components$tex17; return _objectSpread(_objectSpread(_objectSpread(_objectSpread(_objectSpread(_objectSpread({}, provided), responsiveTextStyles), textStyles), {}, { borderRadius: (_theme$components$tex11 = theme$1.components.textInput) === null || _theme$components$tex11 === void 0 || (_theme$components$tex11 = _theme$components$tex11.menuOption) === null || _theme$components$tex11 === void 0 ? void 0 : _theme$components$tex11.borderRadius }, state.isSelected ? { backgroundColor: (_theme$components$tex12 = theme$1.components.textInput) === null || _theme$components$tex12 === void 0 || (_theme$components$tex12 = _theme$components$tex12.menuOption) === null || _theme$components$tex12 === void 0 || (_theme$components$tex12 = _theme$components$tex12.selected) === null || _theme$components$tex12 === void 0 ? void 0 : _theme$components$tex12.backgroundColor, color: (_theme$components$tex13 = theme$1.components.textInput) === null || _theme$components$tex13 === void 0 || (_theme$components$tex13 = _theme$components$tex13.menuOption) === null || _theme$components$tex13 === void 0 || (_theme$components$tex13 = _theme$components$tex13.selected) === null || _theme$components$tex13 === void 0 ? void 0 : _theme$components$tex13.color } : {}), state.isFocused ? { backgroundColor: state.isSelected ? (_theme$components$tex14 = theme$1.components.textInput) === null || _theme$components$tex14 === void 0 || (_theme$components$tex14 = _theme$components$tex14.menuOption) === null || _theme$components$tex14 === void 0 || (_theme$components$tex14 = _theme$components$tex14.selected) === null || _theme$components$tex14 === void 0 || (_theme$components$tex14 = _theme$components$tex14.focused) === null || _theme$components$tex14 === void 0 ? void 0 : _theme$components$tex14.backgroundColor : (_theme$components$tex15 = theme$1.components.textInput) === null || _theme$components$tex15 === void 0 || (_theme$components$tex15 = _theme$components$tex15.menuOption) === null || _theme$components$tex15 === void 0 || (_theme$components$tex15 = _theme$components$tex15.focused) === null || _theme$components$tex15 === void 0 ? void 0 : _theme$components$tex15.backgroundColor, '> *': { color: state.isSelected ? theme$1.color.foreground.primaryHover : undefined, stroke: state.isSelected ? theme$1.color.foreground.primaryHover : undefined } } : {}), {}, { ':active': { backgroundColor: state.isSelected ? (_theme$components$tex16 = theme$1.components.textInput) === null || _theme$components$tex16 === void 0 || (_theme$components$tex16 = _theme$components$tex16.menuOption) === null || _theme$components$tex16 === void 0 || (_theme$components$tex16 = _theme$components$tex16.selected) === null || _theme$components$tex16 === void 0 || (_theme$components$tex16 = _theme$components$tex16.active) === null || _theme$components$tex16 === void 0 ? void 0 : _theme$components$tex16.backgroundColor : (_theme$components$tex17 = theme$1.components.textInput) === null || _theme$components$tex17 === void 0 || (_theme$components$tex17 = _theme$components$tex17.menuOption) === null || _theme$components$tex17 === void 0 || (_theme$components$tex17 = _theme$components$tex17.active) === null || _theme$components$tex17 === void 0 ? void 0 : _theme$components$tex17.backgroundColor, '> *': { color: state.isSelected ? theme$1.color.foreground.primaryActive : undefined, stroke: state.isSelected ? theme$1.color.foreground.primaryActive : undefined } } }); }, singleValue: function singleValue(provided) { return _objectSpread(_objectSpread({}, provided), {}, { '[data-invalid=true]': { color: theme$1.color.foreground.muted } }); }, placeholder: function placeholder(provided) { return _objectSpread(_objectSpread({}, provided), {}, { color: theme$1.color.foreground.placeholder }); } }; }; var useReactSelectThemeOverride = function useReactSelectThemeOverride() { var theme$1 = theme.useTheme(); return function (selectTheme) { var _theme$components$tex18; return _objectSpread(_objectSpread({}, selectTheme), {}, { borderRadius: ((_theme$components$tex18 = theme$1.components.textInput) === null || _theme$components$tex18 === void 0 ? void 0 : _theme$components$tex18.borderRadius) || 0, colors: _objectSpread(_objectSpread({}, selectTheme.colors), {}, { // TODO: map from theme object when tokens are revised primary: '#00a87b', primary75: '#00c28d', primary50: '#9acbb8', primary25: '#c8eada', danger: '#e61e32', dangerLight: '#fec1b5', neutral0: 'white', neutral5: '#fafcfe', neutral10: '#f1f4fb', neutral20: '#dce1ec', neutral30: '#c7cedb', // neutral40, neutral50: '#98a2b8', neutral60: '#646f84', neutral70: '#1a2a3a' // neutral80, // neutral90, }), spacing: { baseUnit: theme$1.spacing.xsmall, controlHeight: theme$1.sizing.medium, menuGutter: theme$1.spacing.xxsmall } }); }; }; var isBrowser = typeof window !== 'undefined'; var useAwaitableItems = function useAwaitableItems(awaitableItems) { var ref = react.useRef(); var _useState = react.useState(false), _useState2 = _slicedToArray(_useState, 2), loading = _useState2[0], setLoading = _useState2[1]; var _useState3 = react.useState([]), _useState4 = _slicedToArray(_useState3, 2), items = _useState4[0], setItems = _useState4[1]; react.useEffect(function () { _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime__default["default"].mark(function _callee() { var itemsResult; return _regeneratorRuntime__default["default"].wrap(function _callee$(_context) { while (1) switch (_context.prev = _context.next) { case 0: ref.current = awaitableItems; setLoading(true); _context.next = 4; return awaitableItems; case 4: itemsResult = _context.sent; if (!(ref.current !== awaitableItems)) { _context.next = 7; break; } return _context.abrupt("return"); case 7: setItems(itemsResult); setLoading(false); case 9: case "end": return _context.stop(); } }, _callee); }))(); }, [awaitableItems]); return { loading: loading, items: items }; }; var Combobox = function Combobox(_ref2) { var data = _ref2.data, getOptionLabel = _ref2.getOptionLabel, getOptionValue = _ref2.getOptionValue, inputValue = _ref2.inputValue, isLoading = _ref2.isLoading, _items = _ref2.items, menuPortalTarget = _ref2.menuPortalTarget, onChange = _ref2.onChange, onInputChange = _ref2.onInputChange, placeholder = _ref2.placeholder, value = _ref2.value, defaultOption = _ref2.defaultOption, _ref2$disableFilter = _ref2.disableFilter, disableFilter = _ref2$disableFilter === void 0 ? false : _ref2$disableFilter, startAdornment = _ref2.startAdornment, _ref2$isMulti = _ref2.isMulti, isMulti = _ref2$isMulti === void 0 ? false : _ref2$isMulti; var theme$1 = theme.useTheme(); var _useFieldContext = field.useFieldContext(), _useFieldContext2 = _slicedToArray(_useFieldContext, 2), _useFieldContext2$ = _useFieldContext2[0], disabled = _useFieldContext2$.disabled, invalid = _useFieldContext2$.invalid, inputId = _useFieldContext2[1].id; var _useAwaitableItems = useAwaitableItems(_items), items = _useAwaitableItems.items, loading = _useAwaitableItems.loading; // Memoize startAdornment to prevent unnecessary re-renders // This has been causing issues with the combobox losing focus when the startAdornment changes // eslint-disable-next-line react-hooks/exhaustive-deps var memoizedStartAdornment = react.useMemo(function () { return startAdornment || null; }, []); // Wrapper for onChange to adapt React Select's signature to our component's signature var handleChange = function handleChange(newValue) { if (onChange) { if (isMulti) { // For multi-select, we would need to handle array of values // For now, keeping the current single-value signature onChange(newValue); } else { onChange(newValue); } } }; var components = useReactSelectComponentsOverride({ data: data, startAdornment: memoizedStartAdornment }); var reactSelectStyles = useReactSelectStylesOverride({ invalid: invalid }); var reactSelectTheme = useReactSelectThemeOverride(); var defaultFilter = ReactSelect.createFilter(); var getDefaultOptionValue = function getDefaultOptionValue() { if (!defaultOption) { return undefined; } var option = defaultOption.option; if (option && _typeof(option) === 'object' && 'value' in option) { return option.value; } return getOptionValue === null || getOptionValue === void 0 ? void 0 : getOptionValue(defaultOption.option); }; var filterOptions = function filterOptions(candidate, input) { var defaultOptionValue = getDefaultOptionValue(); if (input && !disableFilter) { var isDefault = candidate.value === defaultOptionValue; return isDefault || defaultFilter(candidate, input); } return true; }; var getOptions = function getOptions() { if (defaultOption) { if (defaultOption.position === 'end') { return [].concat(_toConsumableArray(items), [defaultOption.option]); } return [defaultOption.option].concat(_toConsumableArray(items)); } return items; }; // This is a workaround for the issue where next interactive element loses focus when combobox is inside a dialog var onBlur = function onBlur(event) { var element = event.relatedTarget; if ((element === null || element === void 0 ? void 0 : element.getAttribute('role')) === 'dialog') return; element === null || element === void 0 || element.focus(); }; return jsxRuntime.jsx(ReactSelect__default["default"], { components: components, getOptionLabel: getOptionLabel, getOptionValue: getOptionValue, inputId: inputId, inputValue: inputValue, isDisabled: disabled, isLoading: isLoading !== null && isLoading !== void 0 ? isLoading : loading, onBlur: onBlur, menuPortalTarget: menuPortalTarget !== null && menuPortalTarget !== void 0 ? menuPortalTarget : isBrowser ? document.body : undefined, onChange: handleChange, onInputChange: onInputChange, options: getOptions(), placeholder: placeholder, styles: _objectSpread(_objectSpread({}, reactSelectStyles), {}, { menuPortal: function menuPortal(provided) { return _objectSpread(_objectSpread({}, provided), {}, { zIndex: theme$1.elevation.modal, pointerEvents: 'all' }); } }), theme: reactSelectTheme, value: value, filterOption: defaultOption || disableFilter ? filterOptions : undefined, isMulti: isMulti }); }; exports.Combobox = Combobox; exports.useAwaitableItems = useAwaitableItems;