@spark-web/combobox
Version:
--- title: Combobox storybookPath: forms-combobox--default isExperimentalPackage: true ---
402 lines (395 loc) • 19.4 kB
JavaScript
import _objectSpread from '@babel/runtime/helpers/esm/objectSpread2';
import _toConsumableArray from '@babel/runtime/helpers/esm/toConsumableArray';
import _typeof from '@babel/runtime/helpers/esm/typeof';
import _asyncToGenerator from '@babel/runtime/helpers/esm/asyncToGenerator';
import _slicedToArray from '@babel/runtime/helpers/esm/slicedToArray';
import _regeneratorRuntime from '@babel/runtime/regenerator';
import { useFieldContext } from '@spark-web/field';
import { useTheme } from '@spark-web/theme';
import { useMemo, useRef, useState, useEffect } from 'react';
import ReactSelect, { components, createFilter } from 'react-select';
import _objectWithoutProperties from '@babel/runtime/helpers/esm/objectWithoutProperties';
import { useFocusRing } from '@spark-web/a11y';
import { Box } from '@spark-web/box';
import { ChevronDownIcon } from '@spark-web/icon';
import { Spinner } from '@spark-web/spinner';
import { Text, useText } from '@spark-web/text';
import { buildDataAttributes } from '@spark-web/utils/internal';
import { jsx, jsxs } from '@emotion/react/jsx-runtime';
var _excluded = ["children"];
var useReactSelectComponentsOverride = function useReactSelectComponentsOverride(_ref) {
var data = _ref.data,
startAdornment = _ref.startAdornment;
var _useFieldContext = useFieldContext(),
_useFieldContext2 = _slicedToArray(_useFieldContext, 2),
invalid = _useFieldContext2[0].invalid,
fieldProps = _useFieldContext2[1];
return useMemo(function () {
return {
DropdownIndicator: function DropdownIndicator(props) {
return jsx(components.DropdownIndicator, _objectSpread(_objectSpread({}, props), {}, {
children: jsx(ChevronDownIcon, {
size: "xxsmall",
tone: "muted"
})
}));
},
Input: function Input(props) {
return jsx(components.Input, _objectSpread(_objectSpread(_objectSpread({}, props), data ? 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 jsx(components.LoadingMessage, _objectSpread(_objectSpread({}, props), {}, {
children: jsx(Box, {
paddingY: "large",
children: jsx(Spinner, {
size: "xsmall",
tone: "primary"
})
})
}));
},
NoOptionsMessage: function NoOptionsMessage(props) {
return jsx(components.NoOptionsMessage, _objectSpread(_objectSpread({}, props), {}, {
children: jsx(Box, {
paddingY: "large",
children: jsx(Text, {
children: "No matching results"
})
})
}));
},
SingleValue: function SingleValue(_ref2) {
var children = _ref2.children,
props = _objectWithoutProperties(_ref2, _excluded);
return jsx(components.SingleValue, _objectSpread(_objectSpread({}, props), {}, {
children: jsx(Box, {
data: invalid ? {
invalid: invalid
} : undefined,
children: children
})
}));
},
Control: function Control(props) {
return jsxs(components.Control, _objectSpread(_objectSpread({}, props), {}, {
children: [startAdornment, props.children]
}));
}
};
}, [data, fieldProps, invalid, startAdornment]);
};
var useReactSelectStylesOverride = function useReactSelectStylesOverride(_ref3) {
var invalid = _ref3.invalid;
var theme = useTheme();
var _useText = useText({
baseline: false,
tone: 'neutral',
size: 'standard',
weight: 'regular'
}),
_useText2 = _slicedToArray(_useText, 2),
responsiveTextStyles = _useText2[0],
textStyles = _useText2[1];
var _useText3 = useText({
baseline: true,
tone: 'muted',
size: 'xsmall',
weight: 'semibold'
}),
_useText4 = _slicedToArray(_useText3, 2),
responsiveGroupHeadingStyles = _useText4[0],
groupHeadingStyles = _useText4[1];
var focusRingStyles = 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.components.textInput) === null || _theme$components$tex === void 0 ? void 0 : _theme$components$tex.borderColor
}), state.isFocused ? (_theme$components$tex2 = (_theme$components$tex3 = theme.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.color.foreground.critical
} : (_theme$components$tex4 = theme.components.textInput) !== null && _theme$components$tex4 !== void 0 && _theme$components$tex4.boxShadow ? {
boxShadow: (_theme$components$tex5 = theme.components.textInput) === null || _theme$components$tex5 === void 0 ? void 0 : _theme$components$tex5.boxShadow
} : {}), (_theme$components$tex6 = theme.components.textInput) !== null && _theme$components$tex6 !== void 0 && _theme$components$tex6.hover ? {
':hover': (_theme$components$tex7 = theme.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.spacing.medium,
paddingBottom: theme.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.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.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.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.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.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.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.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.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.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.color.foreground.primaryHover : undefined,
stroke: state.isSelected ? theme.color.foreground.primaryHover : undefined
}
} : {}), {}, {
':active': {
backgroundColor: state.isSelected ? (_theme$components$tex16 = theme.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.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.color.foreground.primaryActive : undefined,
stroke: state.isSelected ? theme.color.foreground.primaryActive : undefined
}
}
});
},
singleValue: function singleValue(provided) {
return _objectSpread(_objectSpread({}, provided), {}, {
'[data-invalid=true]': {
color: theme.color.foreground.muted
}
});
},
placeholder: function placeholder(provided) {
return _objectSpread(_objectSpread({}, provided), {}, {
color: theme.color.foreground.placeholder
});
}
};
};
var useReactSelectThemeOverride = function useReactSelectThemeOverride() {
var theme = useTheme();
return function (selectTheme) {
var _theme$components$tex18;
return _objectSpread(_objectSpread({}, selectTheme), {}, {
borderRadius: ((_theme$components$tex18 = theme.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.spacing.xsmall,
controlHeight: theme.sizing.medium,
menuGutter: theme.spacing.xxsmall
}
});
};
};
var isBrowser = typeof window !== 'undefined';
var useAwaitableItems = function useAwaitableItems(awaitableItems) {
var ref = useRef();
var _useState = useState(false),
_useState2 = _slicedToArray(_useState, 2),
loading = _useState2[0],
setLoading = _useState2[1];
var _useState3 = useState([]),
_useState4 = _slicedToArray(_useState3, 2),
items = _useState4[0],
setItems = _useState4[1];
useEffect(function () {
_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee() {
var itemsResult;
return _regeneratorRuntime.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 = useTheme();
var _useFieldContext = 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 = 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 = 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 jsx(ReactSelect, {
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.elevation.modal,
pointerEvents: 'all'
});
}
}),
theme: reactSelectTheme,
value: value,
filterOption: defaultOption || disableFilter ? filterOptions : undefined,
isMulti: isMulti
});
};
export { Combobox, useAwaitableItems };