@mskcc/carbon-react
Version:
Carbon react components for the MSKCC DSM
585 lines (574 loc) • 21.6 kB
JavaScript
/**
* MSKCC 2021, 2024
*/
;
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;