UNPKG

@mskcc/carbon-react

Version:

Carbon react components for the MSKCC DSM

585 lines (574 loc) 21.6 kB
/** * MSKCC 2021, 2024 */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js'); var cx = require('classnames'); var Downshift = require('downshift'); var isEqual = require('lodash.isequal'); var PropTypes = require('prop-types'); var React = require('react'); var index = require('../ListBox/index.js'); var MultiSelectPropTypes = require('./MultiSelectPropTypes.js'); var sorting = require('./tools/sorting.js'); var Selection = require('../../internal/Selection.js'); var setupGetInstanceId = require('../../tools/setupGetInstanceId.js'); var mergeRefs = require('../../tools/mergeRefs.js'); var deprecate = require('../../prop-types/deprecate.js'); var usePrefix = require('../../internal/usePrefix.js'); require('../FluidForm/FluidForm.js'); var FormContext = require('../FluidForm/FormContext.js'); var match = require('../../internal/keyboard/match.js'); var ListBoxPropTypes = require('../ListBox/ListBoxPropTypes.js'); var keys = require('../../internal/keyboard/keys.js'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var cx__default = /*#__PURE__*/_interopDefaultLegacy(cx); var Downshift__default = /*#__PURE__*/_interopDefaultLegacy(Downshift); var isEqual__default = /*#__PURE__*/_interopDefaultLegacy(isEqual); var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes); var React__default = /*#__PURE__*/_interopDefaultLegacy(React); var _span, _span2, _span3, _span4, _span5; const noop = () => {}; const getInstanceId = setupGetInstanceId["default"](); const { ItemClick, MenuBlur, MenuKeyDownArrowDown, MenuKeyDownArrowUp, MenuKeyDownEscape, MenuKeyDownSpaceButton, ToggleButtonClick } = Downshift.useSelect.stateChangeTypes; const defaultItemToString = item => { if (typeof item === 'string') { return item; } if (typeof item === 'number') { return `${item}`; } if (item !== null && typeof item === 'object' && 'label' in item && typeof item['label'] === 'string') { return item['label']; } return ''; }; const MultiSelect = /*#__PURE__*/React__default["default"].forwardRef(function MultiSelect(_ref, ref) { let { countOnly = false, outsideItems = false, className: containerClassName, id, items, itemToElement, itemToString = defaultItemToString, titleText, hideLabel, helperText, label, labelKey, type, size, disabled, initialSelectedItems, sortItems, compareItems, clearSelectionText, clearSelectionDescription, light, invalid, invalidText, warn, warnText, useTitleInItem, translateWithId, downshiftProps, open, selectionFeedback, onChange, onMenuChange, direction, selectedItems: selected, readOnly, locale } = _ref; const prefix = usePrefix.usePrefix(); const { isFluid } = React.useContext(FormContext.FormContext); const { current: multiSelectInstanceId } = React.useRef(getInstanceId()); const [highlightedIndex, setHighlightedIndex] = React.useState(); const [isFocused, setIsFocused] = React.useState(false); const [inputFocused, setInputFocused] = React.useState(false); const [isOpen, setIsOpen] = React.useState(open || false); const [prevOpenProp, setPrevOpenProp] = React.useState(open); const [topItems, setTopItems] = React.useState([]); const { selectedItems: controlledSelectedItems, onItemChange, clearSelection } = Selection.useSelection({ disabled, initialSelectedItems, onChange, selectedItems: selected }); const { getToggleButtonProps, getLabelProps, getMenuProps, getItemProps, selectedItem } = Downshift.useSelect({ ...downshiftProps, highlightedIndex, isOpen, itemToString: items => { return Array.isArray(items) && items.map(function (item) { return itemToString(item); }).join(', ') || ''; }, onStateChange, selectedItem: controlledSelectedItems, items }); const toggleButtonProps = getToggleButtonProps({ onFocus: () => { setInputFocused(true); }, onBlur: () => { setInputFocused(false); } }); const mergedRef = mergeRefs["default"](toggleButtonProps.ref, ref); const selectedItems = selectedItem; /** * wrapper function to forward changes to consumer */ const setIsOpenWrapper = open => { setIsOpen(open); if (onMenuChange) { onMenuChange(open); } }; /** * programmatically control this `open` prop */ if (prevOpenProp !== open) { setIsOpenWrapper(open); setPrevOpenProp(open); } const inline = type === 'inline'; const showWarning = !invalid && warn; const wrapperClasses = cx__default["default"](`${prefix}--multi-select__wrapper`, `${prefix}--list-box__wrapper`, containerClassName, { [`${prefix}--multi-select__wrapper--inline`]: inline, [`${prefix}--list-box__wrapper--inline`]: inline, [`${prefix}--multi-select__wrapper--inline--invalid`]: inline && invalid, [`${prefix}--list-box__wrapper--inline--invalid`]: inline && invalid, [`${prefix}--list-box__wrapper--fluid--invalid`]: isFluid && invalid, [`${prefix}--list-box__wrapper--fluid--focus`]: !isOpen && isFluid && isFocused, [`msk-multiselect--outside`]: outsideItems }); const titleClasses = cx__default["default"](`${prefix}--label`, { [`${prefix}--label--disabled`]: disabled, [`${prefix}--visually-hidden`]: hideLabel }); const helperId = !helperText ? undefined : `multiselect-helper-text-${multiSelectInstanceId}`; const fieldLabelId = `multiselect-field-label-${multiSelectInstanceId}`; const helperClasses = cx__default["default"](`${prefix}--form__helper-text`, { [`${prefix}--form__helper-text--disabled`]: disabled }); const className = cx__default["default"](`${prefix}--multi-select`, { [`${prefix}--multi-select--invalid`]: invalid, [`${prefix}--multi-select--invalid--focused`]: invalid && inputFocused, [`${prefix}--multi-select--warning`]: showWarning, [`${prefix}--multi-select--inline`]: inline, [`${prefix}--multi-select--selected`]: selectedItems && selectedItems.length > 0, [`${prefix}--list-box--up`]: direction === 'top', [`${prefix}--multi-select--readonly`]: readOnly }); // needs to be capitalized for react to render it correctly // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const ItemToElement = itemToElement; const sortOptions = { selectedItems: controlledSelectedItems, itemToString, compareItems, locale }; if (selectionFeedback === 'fixed') { sortOptions.selectedItems = []; } else if (selectionFeedback === 'top-after-reopen') { sortOptions.selectedItems = topItems; } function onStateChange(changes) { if (changes.isOpen && !isOpen) { setTopItems(controlledSelectedItems); } const { type } = changes; switch (type) { case ItemClick: case MenuKeyDownSpaceButton: if (changes.selectedItem === undefined) { break; } onItemChange(changes.selectedItem); break; case MenuKeyDownArrowDown: case MenuKeyDownArrowUp: setHighlightedIndex(changes.highlightedIndex); break; case MenuBlur: case MenuKeyDownEscape: setIsOpenWrapper(false); setHighlightedIndex(changes.highlightedIndex); break; case ToggleButtonClick: setIsOpenWrapper(changes.isOpen || false); setHighlightedIndex(changes.highlightedIndex); break; } } const onKeyDown = e => { if (!disabled) { if (match.match(e, keys.Delete) || match.match(e, keys.Escape)) { clearSelection(); e.stopPropagation(); } if (match.match(e, keys.Space) || match.match(e, keys.ArrowDown)) { setIsOpenWrapper(true); } } }; const multiSelectFieldWrapperClasses = cx__default["default"](`${prefix}--list-box__field--wrapper`, { [`${prefix}--list-box__field--wrapper--input-focused`]: inputFocused }); const handleFocus = evt => { evt.target.classList.contains(`${prefix}--tag__close-icon`) ? setIsFocused(false) : setIsFocused(evt.type === 'focus' ? true : false); }; const readOnlyEventHandlers = readOnly ? { onClick: evt => { // NOTE: does not prevent click evt.preventDefault(); // focus on the element as per readonly input behavior if (mergedRef.current !== undefined) { mergedRef.current.focus(); } }, onKeyDown: evt => { const selectAccessKeys = ['ArrowDown', 'ArrowUp', ' ', 'Enter']; // This prevents the select from opening for the above keys if (selectAccessKeys.includes(evt.key)) { evt.preventDefault(); } } } : {}; return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, /*#__PURE__*/React__default["default"].createElement("div", { className: wrapperClasses }, /*#__PURE__*/React__default["default"].createElement("label", _rollupPluginBabelHelpers["extends"]({ className: titleClasses }, getLabelProps()), titleText && titleText, selectedItems.length > 0 && /*#__PURE__*/React__default["default"].createElement("span", { className: `${prefix}--visually-hidden` }, clearSelectionDescription, " ", selectedItems.length, ",", clearSelectionText)), /*#__PURE__*/React__default["default"].createElement(index["default"], { onFocus: isFluid ? handleFocus : undefined, onBlur: isFluid ? handleFocus : undefined, type: type, size: size, className: className, disabled: disabled, light: light, invalid: invalid, invalidText: invalidText, warn: warn, warnText: warnText, isOpen: isOpen, id: id }, /*#__PURE__*/React__default["default"].createElement("div", { className: multiSelectFieldWrapperClasses }, selectedItems.length > 0 && (countOnly ? /*#__PURE__*/React__default["default"].createElement(index["default"].Selection, { readOnly: readOnly, clearSelection: !disabled && !readOnly ? clearSelection : noop, selectionCount: selectedItems.length // eslint-disable-next-line @typescript-eslint/no-non-null-assertion , translateWithId: translateWithId, disabled: disabled }) : !outsideItems ? /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, selectedItems.map((item, index$1) => { const clearSelection = () => { onItemChange(item); }; const itemLabel = typeof labelKey === 'string' ? item[labelKey] : item; return /*#__PURE__*/React__default["default"].createElement(index["default"].Selection, { key: index$1, index: index$1 // @ts-ignore , label: itemLabel, clearSelection: !disabled && !readOnly ? clearSelection : noop // @ts-ignore , translateWithId: translateWithId, disabled: disabled }); }), /*#__PURE__*/React__default["default"].createElement("button", { type: "button", className: "msk-multiselect--clear-all-btn", onClick: clearSelection }, _span || (_span = /*#__PURE__*/React__default["default"].createElement("span", { className: "msk-multiselect--clear-all-btn-text" }, "Clear all")), _span2 || (_span2 = /*#__PURE__*/React__default["default"].createElement("span", { className: "msk-icon msk-multiselect--clear-all-btn-icon" }, "clear")))) : null), /*#__PURE__*/React__default["default"].createElement("button", _rollupPluginBabelHelpers["extends"]({ type: "button", className: `${prefix}--list-box__field msk-multiselect--expand-btn ${selectedItems.length > 0 ? 'msk-multiselect--selected' : ''}`.trim(), disabled: disabled, "aria-disabled": disabled || readOnly, "aria-describedby": !inline && !invalid && !warn && helperText ? helperId : undefined }, toggleButtonProps, { ref: mergedRef, onKeyDown: onKeyDown }, readOnlyEventHandlers), outsideItems || selectedItems.length === 0 ? /*#__PURE__*/React__default["default"].createElement("span", { id: fieldLabelId, className: `${prefix}--list-box__label` }, label) : null, /*#__PURE__*/React__default["default"].createElement("div", { className: `msk-multiselect--expand-btn-icon-wrapper ${isOpen ? 'open' : ''}`.trim() }, _span3 || (_span3 = /*#__PURE__*/React__default["default"].createElement("span", { className: `msk-icon msk-multiselect--expand-btn-icon` }, "expand_more"))))), /*#__PURE__*/React__default["default"].createElement(index["default"].Menu, _rollupPluginBabelHelpers["extends"]({ "aria-multiselectable": "true" }, getMenuProps()), isOpen && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion sortItems(items, sortOptions).map((item, index$1) => { const isChecked = selectedItems.filter(selected => isEqual__default["default"](selected, item)).length > 0; const itemProps = getItemProps({ item, // we don't want Downshift to set aria-selected for us // we also don't want to set 'false' for reader verbosity's sake ['aria-selected']: isChecked ? true : undefined, disabled: item.disabled }); const itemText = itemToString(item); return /*#__PURE__*/React__default["default"].createElement(index["default"].MenuItem, _rollupPluginBabelHelpers["extends"]({ key: itemProps.id, isActive: isChecked, "aria-label": itemText, isHighlighted: highlightedIndex === index$1, title: itemText }, itemProps), /*#__PURE__*/React__default["default"].createElement("div", { className: `${prefix}--checkbox-wrapper` }, /*#__PURE__*/React__default["default"].createElement("span", { title: useTitleInItem ? itemText : undefined, className: `${prefix}--checkbox-label`, "data-contained-checkbox-state": isChecked, id: `${itemProps.id}__checkbox` }, itemToElement ? /*#__PURE__*/React__default["default"].createElement(ItemToElement, _rollupPluginBabelHelpers["extends"]({ key: itemProps.id }, item)) : itemText))); })))), !inline && !invalid && !warn && helperText && /*#__PURE__*/React__default["default"].createElement("div", { id: helperId, className: helperClasses }, helperText), outsideItems && selectedItems.length > 0 && /*#__PURE__*/React__default["default"].createElement("div", { className: "msk-multiselect--outside-items-wrapper" }, selectedItems.map((item, index$1) => { const clearSelection = () => { onItemChange(item); }; const itemLabel = typeof labelKey === 'string' ? item[labelKey] : item; return /*#__PURE__*/React__default["default"].createElement(index["default"].Selection, { key: index$1, index: index$1 // @ts-ignore , label: itemLabel, clearSelection: !disabled && !readOnly ? clearSelection : noop // @ts-ignore , translateWithId: translateWithId, disabled: disabled }); }), /*#__PURE__*/React__default["default"].createElement("button", { type: "button", className: "msk-multiselect--clear-all-btn-outside", onClick: clearSelection }, _span4 || (_span4 = /*#__PURE__*/React__default["default"].createElement("span", { className: "msk-multiselect--clear-all-btn-text" }, "Clear all")), _span5 || (_span5 = /*#__PURE__*/React__default["default"].createElement("span", { className: "msk-icon msk-multiselect--clear-all-btn-icon" }, "clear"))))); }); MultiSelect.displayName = 'MultiSelect'; MultiSelect.propTypes = { ...MultiSelectPropTypes.sortingPropTypes, /** * Provide a custom class name to be added to the outermost node in the * component */ className: PropTypes__default["default"].string, /** * Specify the text that should be read for screen readers that describes total items selected */ clearSelectionDescription: PropTypes__default["default"].string, /** * Specify the text that should be read for screen readers to clear selection. */ clearSelectionText: PropTypes__default["default"].string, /** * only show the count and not individual selections */ countOnly: PropTypes__default["default"].bool, /** * Specify the direction of the multiselect dropdown. Can be either top or bottom. */ direction: PropTypes__default["default"].oneOf(['top', 'bottom']), /** * Disable the control */ disabled: PropTypes__default["default"].bool, /** * Additional props passed to Downshift */ // @ts-ignore downshiftProps: PropTypes__default["default"].shape(Downshift__default["default"].propTypes), /** * Provide helper text that is used alongside the control label for * additional help */ helperText: PropTypes__default["default"].node, /** * Specify whether the title text should be hidden or not */ hideLabel: PropTypes__default["default"].bool, /** * Specify a custom `id` */ id: PropTypes__default["default"].string.isRequired, /** * Allow users to pass in arbitrary items from their collection that are * pre-selected */ initialSelectedItems: PropTypes__default["default"].array, /** * Is the current selection invalid? */ invalid: PropTypes__default["default"].bool, /** * If invalid, what is the error? */ invalidText: PropTypes__default["default"].node, /** * Function to render items as custom components instead of strings. * Defaults to null and is overridden by a getter */ itemToElement: PropTypes__default["default"].func, /** * Helper function passed to downshift that allows the library to render a * given item to a string label. By default, it extracts the `label` field * from a given item to serve as the item label in the list. */ itemToString: PropTypes__default["default"].func, /** * We try to stay as generic as possible here to allow individuals to pass * in a collection of whatever kind of data structure they prefer */ items: PropTypes__default["default"].array.isRequired, /** * Generic `label` that will be used as the textual representation of what * this field is for */ label: PropTypes__default["default"].node.isRequired, /** * Specify the key in the object to use for the label. */ labelKey: PropTypes__default["default"].string, /** * `true` to use the light version. */ light: deprecate["default"](PropTypes__default["default"].bool, 'The `light` prop for `MultiSelect` has ' + 'been deprecated in favor of the new `Layer` component. It will be removed in the next major release.'), /** * Specify the locale of the control. Used for the default `compareItems` * used for sorting the list of items in the control. */ locale: PropTypes__default["default"].string, /** * `onChange` is a utility for this controlled component to communicate to a * consuming component what kind of internal state changes are occurring. */ onChange: PropTypes__default["default"].func, /** * `onMenuChange` is a utility for this controlled component to communicate to a * consuming component that the menu was open(`true`)/closed(`false`). */ onMenuChange: PropTypes__default["default"].func, /** * Initialize the component with an open(`true`)/closed(`false`) menu. */ open: PropTypes__default["default"].bool, /** * put selected items outside of the dropdown */ outsideItems: PropTypes__default["default"].bool, /** * Whether or not the Dropdown is readonly */ readOnly: PropTypes__default["default"].bool, /** * For full control of the selected items */ selectedItems: PropTypes__default["default"].array, /** * Specify feedback (mode) of the selection. * `top`: selected item jumps to top * `fixed`: selected item stays at it's position * `top-after-reopen`: selected item jump to top after reopen dropdown */ selectionFeedback: PropTypes__default["default"].oneOf(['top', 'fixed', 'top-after-reopen']), /** * Specify the size of the ListBox. Currently supports either `sm`, `md` or `lg` as an option. */ size: ListBoxPropTypes.ListBoxSize, /** * Provide text to be used in a `<label>` element that is tied to the * multiselect via ARIA attributes. */ titleText: PropTypes__default["default"].node, /** * Callback function for translating ListBoxMenuIcon SVG title */ translateWithId: PropTypes__default["default"].func, /** * Specify 'inline' to create an inline multi-select. */ type: PropTypes__default["default"].oneOf(['default', 'inline']), /** * Specify title to show title on hover */ useTitleInItem: PropTypes__default["default"].bool, /** * Specify whether the control is currently in warning state */ warn: PropTypes__default["default"].bool, /** * Provide the text that is displayed when the control is in warning state */ warnText: PropTypes__default["default"].node }; MultiSelect.defaultProps = { compareItems: sorting.defaultCompareItems, disabled: false, locale: 'en', itemToString: defaultItemToString, initialSelectedItems: [], sortItems: sorting.defaultSortItems, type: 'default', titleText: false, open: false, selectionFeedback: 'top-after-reopen', direction: 'bottom', clearSelectionText: 'To clear selection, press Delete or Backspace,', clearSelectionDescription: 'Total items selected: ', selectedItems: undefined }; exports["default"] = MultiSelect;