@mskcc/carbon-react
Version:
Carbon react components for the MSKCC DSM
570 lines (562 loc) • 21.5 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 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;