UNPKG

@mskcc/carbon-react

Version:

Carbon react components for the MSKCC DSM

570 lines (562 loc) 21.5 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 MultiSelectPropTypes = require('./MultiSelectPropTypes.js'); var index = require('../ListBox/index.js'); var Selection = require('../../internal/Selection.js'); var mergeRefs = require('../../tools/mergeRefs.js'); var deprecate = require('../../prop-types/deprecate.js'); var useId = require('../../internal/useId.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 ListBoxSelection = require('../ListBox/next/ListBoxSelection.js'); var ListBoxTrigger = require('../ListBox/next/ListBoxTrigger.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); const FilterableMultiSelect = /*#__PURE__*/React__default["default"].forwardRef(function FilterableMultiSelect(_ref, ref) { let { ['aria-label']: ariaLabel, ariaLabel: deprecatedAriaLabel, className: containerClassName, compareItems, direction, disabled, downshiftProps, filterItems, helperText, hideLabel, id, initialSelectedItems, invalid, invalidText, items, itemToElement: ItemToElement, // needs to be capitalized for react to render it correctly itemToString, labelKey, light, locale, onInputValueChange, open, onChange, onMenuChange, placeholder, titleText, type, selectionFeedback, size, sortItems, translateWithId, useTitleInItem, warn, warnText } = _ref; const { isFluid } = React.useContext(FormContext.FormContext); const [isFocused, setIsFocused] = React.useState(false); const [isOpen, setIsOpen] = React.useState(open); const [prevOpen, setPrevOpen] = React.useState(open); const [inputValue, setInputValue] = React.useState(''); const [topItems, setTopItems] = React.useState(initialSelectedItems ?? []); const [inputFocused, setInputFocused] = React.useState(false); const [highlightedIndex, setHighlightedIndex] = React.useState(null); const [currentSelectedItems, setCurrentSelectedItems] = React.useState(initialSelectedItems ?? []); const textInput = React.useRef(); const filterableMultiSelectInstanceId = useId.useId(); const prefix = usePrefix.usePrefix(); if (prevOpen !== open) { setIsOpen(open); setPrevOpen(open); } const inline = type === 'inline'; const wrapperClasses = cx__default["default"](`${prefix}--multi-select__wrapper`, `${prefix}--multi-select--filterable__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--up`]: direction === 'top', [`${prefix}--list-box__wrapper--fluid--invalid`]: isFluid && invalid, [`${prefix}--list-box__wrapper--fluid--focus`]: isFluid && isFocused }); const helperId = !helperText ? undefined : `filterablemultiselect-helper-text-${filterableMultiSelectInstanceId}`; const labelId = `${id}-label`; const titleClasses = cx__default["default"]({ [`${prefix}--label`]: true, [`${prefix}--label--disabled`]: disabled, [`${prefix}--visually-hidden`]: hideLabel }); const helperClasses = cx__default["default"]({ [`${prefix}--form__helper-text`]: true, [`${prefix}--form__helper-text--disabled`]: disabled }); const inputClasses = cx__default["default"]({ [`${prefix}--text-input`]: true, [`${prefix}--text-input--empty`]: !inputValue, [`${prefix}--text-input--light`]: light }); const helper = helperText ? /*#__PURE__*/React__default["default"].createElement("div", { id: helperId, className: helperClasses }, helperText) : null; const menuId = `${id}__menu`; const inputId = `${id}-input`; React.useEffect(() => { if (!isOpen) { setTopItems(currentSelectedItems); } }, [currentSelectedItems, isOpen, setTopItems]); function handleOnChange(changes) { setCurrentSelectedItems(changes.selectedItems); if (onChange) { onChange(changes); } } function handleOnMenuChange(forceIsOpen) { const nextIsOpen = forceIsOpen ?? !isOpen; setIsOpen(nextIsOpen); if (onMenuChange) { onMenuChange(nextIsOpen); } } function handleOnOuterClick() { handleOnMenuChange(false); } function handleOnStateChange(changes) { const { type } = changes; const { stateChangeTypes } = Downshift__default["default"]; switch (type) { case stateChangeTypes.keyDownArrowDown: case stateChangeTypes.keyDownArrowUp: case stateChangeTypes.keyDownHome: case stateChangeTypes.keyDownEnd: setHighlightedIndex(changes.highlightedIndex !== undefined ? changes.highlightedIndex : null); if (stateChangeTypes.keyDownArrowDown === type && !isOpen) { handleOnMenuChange(true); } break; case stateChangeTypes.keyDownEscape: handleOnMenuChange(false); break; } } function handleOnInputValueChange(inputValue, _ref2) { let { type } = _ref2; if (onInputValueChange) { onInputValueChange(inputValue); } if (type !== Downshift__default["default"].stateChangeTypes.changeInput) { return; } if (Array.isArray(inputValue)) { clearInputValue(); } else { setInputValue(inputValue); } if (inputValue && !isOpen) { handleOnMenuChange(true); } else if (!inputValue && isOpen) { handleOnMenuChange(false); } } function clearInputValue() { setInputValue(''); if (textInput.current) { textInput.current.focus(); } } return /*#__PURE__*/React__default["default"].createElement(Selection["default"], { disabled: disabled, onChange: handleOnChange, initialSelectedItems: initialSelectedItems, render: _ref3 => { let { selectedItems, onItemChange, clearSelection } = _ref3; return /*#__PURE__*/React__default["default"].createElement(Downshift__default["default"], _rollupPluginBabelHelpers["extends"]({}, downshiftProps, { highlightedIndex: highlightedIndex, id: id, isOpen: isOpen, inputValue: inputValue, onInputValueChange: handleOnInputValueChange, onChange: selectedItem => { if (selectedItem !== null) { onItemChange(selectedItem); } }, itemToString: itemToString, onStateChange: handleOnStateChange, onOuterClick: handleOnOuterClick, selectedItem: selectedItems, labelId: labelId, menuId: menuId, inputId: inputId }), _ref4 => { let { getInputProps, getItemProps, getLabelProps, getMenuProps, getRootProps, getToggleButtonProps, isOpen, inputValue, selectedItem } = _ref4; const className = cx__default["default"](`${prefix}--multi-select`, `${prefix}--combo-box`, `${prefix}--multi-select--filterable`, { [`${prefix}--multi-select--invalid`]: invalid, [`${prefix}--multi-select--invalid--focused`]: invalid && inputFocused, [`${prefix}--multi-select--open`]: isOpen, [`${prefix}--multi-select--inline`]: inline, [`${prefix}--multi-select--selected`]: selectedItem.length > 0, [`${prefix}--multi-select--filterable--input-focused`]: inputFocused }); const rootProps = getRootProps({}, { suppressRefError: true }); const labelProps = getLabelProps(); const buttonProps = getToggleButtonProps({ disabled, onClick: () => { handleOnMenuChange(!isOpen); if (textInput.current) { textInput.current.focus(); } }, // When we moved the "root node" of Downshift to the <input> for // ARIA 1.2 compliance, we unfortunately hit this branch for the // "mouseup" event that downshift listens to: // https://github.com/downshift-js/downshift/blob/v5.2.1/src/downshift.js#L1051-L1065 // // As a result, it will reset the state of the component and so we // stop the event from propagating to prevent this. This allows the // toggleMenu behavior for the toggleButton to correctly open and // close the menu. onMouseUp(event) { if (isOpen) { event.stopPropagation(); } } }); const inputProps = getInputProps({ 'aria-controls': isOpen ? menuId : null, 'aria-describedby': helperText ? helperId : null, // Remove excess aria `aria-labelledby`. HTML <label for> // provides this aria information. 'aria-labelledby': null, disabled, placeholder, onClick: () => { handleOnMenuChange(true); }, onKeyDown: event => { if (match.match(event, keys.Space)) { event.stopPropagation(); } if (match.match(event, keys.Enter)) { handleOnMenuChange(true); } if (!disabled) { if (match.match(event, keys.Delete) || match.match(event, keys.Escape)) { if (isOpen) { handleOnMenuChange(true); clearInputValue(); event.stopPropagation(); } else if (!isOpen) { clearInputValue(); clearSelection(); event.stopPropagation(); } } } if (match.match(event, keys.Tab)) { handleOnMenuChange(false); } if (match.match(event, keys.Home)) { event.target.setSelectionRange(0, 0); } if (match.match(event, keys.End)) { event.target.setSelectionRange(event.target.value.length, event.target.value.length); } }, onFocus: () => { setInputFocused(true); }, onBlur: () => { setInputFocused(false); setInputValue(''); } }); const menuProps = getMenuProps({ 'aria-label': ariaLabel }, { suppressRefError: true }); const handleFocus = evt => { if (evt.target.classList.contains(`${prefix}--tag__close-icon`) || evt.target.classList.contains(`${prefix}--list-box__selection`)) { setIsFocused(false); } else { setIsFocused(evt.type === 'focus' ? true : false); } }; return /*#__PURE__*/React__default["default"].createElement("div", { className: wrapperClasses }, titleText ? /*#__PURE__*/React__default["default"].createElement("label", _rollupPluginBabelHelpers["extends"]({ className: titleClasses }, labelProps), titleText) : null, /*#__PURE__*/React__default["default"].createElement(index["default"], { "aria-label": deprecatedAriaLabel || ariaLabel, onFocus: isFluid ? handleFocus : null, onBlur: isFluid ? handleFocus : null, className: className, disabled: disabled, light: light, ref: ref, invalid: invalid, invalidText: invalidText, warn: warn, warnText: warnText, isOpen: isOpen, size: size }, /*#__PURE__*/React__default["default"].createElement("div", { className: `${prefix}--list-box__field` }, selectedItem.length > 0 && // <ListBoxSelection // clearSelection={() => { // clearSelection(); // if (textInput.current) { // textInput.current.focus(); // } // }} // selectionCount={selectedItem.length} // translateWithId={translateWithId} // disabled={disabled} // /> selectedItems.map((item, index$1) => { const itemLabel = typeof labelKey === 'string' ? item[labelKey] : item; return /*#__PURE__*/React__default["default"].createElement(index["default"].Selection, { key: index$1, index: index$1, clearSelection: () => { onItemChange(item); if (textInput.current) { textInput.current.focus(); } }, label: itemLabel // selectionCount={selectedItem.length} , translateWithId: translateWithId, disabled: disabled }); }), /*#__PURE__*/React__default["default"].createElement("div", { className: "msk-multi-select--filterable-input-wrapper" }, /*#__PURE__*/React__default["default"].createElement("input", _rollupPluginBabelHelpers["extends"]({ className: inputClasses }, rootProps, inputProps, { ref: mergeRefs["default"](textInput, rootProps.ref) })), inputValue && /*#__PURE__*/React__default["default"].createElement(ListBoxSelection["default"], { clearSelection: clearInputValue, disabled: disabled, translateWithId: translateWithId, onMouseUp: event => { // If we do not stop this event from propagating, // it seems like Downshift takes our event and // prevents us from getting `onClick` / // `clearSelection` from the underlying <button> in // ListBoxSelection event.stopPropagation(); } })), /*#__PURE__*/React__default["default"].createElement(ListBoxTrigger["default"], _rollupPluginBabelHelpers["extends"]({}, buttonProps, { isOpen: isOpen, translateWithId: translateWithId }))), isOpen ? /*#__PURE__*/React__default["default"].createElement(index["default"].Menu, menuProps, sortItems(filterItems(items, { itemToString, inputValue }), { selectedItems: { top: selectedItems, fixed: [], 'top-after-reopen': topItems }[selectionFeedback], itemToString, compareItems, locale }).map((item, index$1) => { const itemProps = getItemProps({ item, disabled: item.disabled }); const itemText = itemToString(item); const isChecked = selectedItem.filter(selected => isEqual__default["default"](selected, item)).length > 0; return /*#__PURE__*/React__default["default"].createElement(index["default"].MenuItem, _rollupPluginBabelHelpers["extends"]({ key: itemProps.id, "aria-label": itemText, isActive: isChecked, 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 : null, className: `${prefix}--checkbox-label`, "data-contained-checkbox-state": isChecked, id: `${itemProps.id}-item` }, ItemToElement ? /*#__PURE__*/React__default["default"].createElement(ItemToElement, _rollupPluginBabelHelpers["extends"]({ key: itemProps.id }, item)) : itemText))); })) : null), !inline && !invalid && !warn ? helper : null); }); } }); }); FilterableMultiSelect.propTypes = { /** * Specify a label to be read by screen readers on the container node */ ['aria-label']: PropTypes__default["default"].string, /** * Deprecated, please use `aria-label` instead. * Specify a label to be read by screen readers on the container note. */ ariaLabel: deprecate["default"](PropTypes__default["default"].string, 'This prop syntax has been deprecated. Please use the new `aria-label`.'), /** * 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 */ downshiftProps: PropTypes__default["default"].shape(Downshift__default["default"].propTypes), /** * 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, /** * 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 `FilterableMultiSelect` 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, /** * `onInputValueChange` is a utility for this controlled component to communicate to * the currently typed input. */ onInputValueChange: PropTypes__default["default"].func, /** * `onMenuChange` is a utility for this controlled component to communicate to a * consuming component that the menu was opened(`true`)/closed(`false`). */ onMenuChange: PropTypes__default["default"].func, /** * Initialize the component with an open(`true`)/closed(`false`) menu. */ open: PropTypes__default["default"].bool, /** * Generic `placeholder` that will be used as the textual representation of * what this field is for */ placeholder: PropTypes__default["default"].string, /** * 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, ...MultiSelectPropTypes.sortingPropTypes, /** * Callback function for translating ListBoxMenuIcon SVG title */ translateWithId: PropTypes__default["default"].func, /** * 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 }; var FilterableMultiSelect$1 = FilterableMultiSelect; exports["default"] = FilterableMultiSelect$1;