@mskcc/carbon-react
Version:
Carbon react components for the MSKCC DSM
577 lines (568 loc) • 20.5 kB
JavaScript
/**
* 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 PropTypes = require('prop-types');
var React = require('react');
require('../Text/index.js');
var index = require('../ListBox/index.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 MskIcon = require('../Icon/MskIcon.js');
var match = require('../../internal/keyboard/match.js');
var Text = require('../Text/Text.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 PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes);
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
const {
keyDownArrowDown,
keyDownArrowUp,
keyDownEscape,
clickButton,
blurButton,
changeInput
} = Downshift__default["default"].stateChangeTypes;
const defaultItemToString = item => {
if (typeof item === 'string') {
return item;
}
return item && item.label;
};
const defaultShouldFilterItem = () => true;
const getInputValue = _ref => {
let {
initialSelectedItem,
inputValue,
itemToString,
selectedItem
} = _ref;
if (selectedItem) {
return itemToString(selectedItem);
}
if (initialSelectedItem) {
return itemToString(initialSelectedItem);
}
return inputValue || '';
};
const findHighlightedIndex = (_ref2, inputValue) => {
let {
items,
itemToString = defaultItemToString
} = _ref2;
if (!inputValue) {
return -1;
}
const searchValue = inputValue.toLowerCase();
for (let i = 0; i < items.length; i++) {
const item = itemToString(items[i]).toLowerCase();
if (item.indexOf(searchValue) !== -1) {
return i;
}
}
return -1;
};
const getInstanceId = setupGetInstanceId["default"]();
const ComboBox = /*#__PURE__*/React__default["default"].forwardRef((props, ref) => {
var _Text;
const {
['aria-label']: ariaLabel,
ariaLabel: deprecatedAriaLabel,
className: containerClassName,
direction,
disabled,
downshiftProps,
helperText,
id,
initialSelectedItem,
invalid,
invalidText,
items,
itemToElement,
itemToString,
light,
onChange,
onInputChange,
onToggleClick,
placeholder,
readOnly,
selectedItem,
shouldFilterItem,
size,
titleText,
translateWithId,
type: _type,
warn,
warnText,
onStateChange: _onStateChange,
...rest
} = props;
const prefix = usePrefix.usePrefix();
const {
isFluid
} = React.useContext(FormContext.FormContext);
const textInput = React.useRef(null);
const comboBoxInstanceId = getInstanceId();
const [inputValue, setInputValue] = React.useState(getInputValue({
initialSelectedItem,
inputValue: '',
itemToString,
selectedItem
}));
const [isFocused, setIsFocused] = React.useState(false);
const [prevSelectedItem, setPrevSelectedItem] = React.useState();
const [doneInitialSelectedItem, setDoneInitialSelectedItem] = React.useState(false);
const [highlightedIndex, setHighlightedIndex] = React.useState();
const savedOnInputChange = React.useRef(onInputChange);
if (!doneInitialSelectedItem || prevSelectedItem !== selectedItem) {
setDoneInitialSelectedItem(true);
setPrevSelectedItem(selectedItem);
setInputValue(getInputValue({
initialSelectedItem,
inputValue,
itemToString,
selectedItem
}));
}
const filterItems = (items, itemToString, inputValue) => items.filter(item => shouldFilterItem ? shouldFilterItem({
item,
itemToString,
inputValue
}) : defaultShouldFilterItem());
const handleOnChange = selectedItem => {
if (onChange) {
onChange({
selectedItem
});
}
};
const handleOnInputValueChange = inputValue => {
setInputValue(inputValue || '');
};
React.useEffect(() => {
savedOnInputChange.current = onInputChange;
}, [onInputChange]);
React.useEffect(() => {
if (savedOnInputChange.current) {
savedOnInputChange.current(inputValue);
}
}, [inputValue]);
const handleSelectionClear = () => {
if (textInput?.current) {
textInput.current.focus();
}
};
const getHighlightedIndex = changes => {
if (Object.prototype.hasOwnProperty.call(changes, 'inputValue')) {
const {
inputValue
} = changes;
const filteredItems = filterItems(items, itemToString, inputValue);
const indexToHighlight = findHighlightedIndex({
...props,
items: filteredItems
}, inputValue);
setHighlightedIndex(indexToHighlight);
return indexToHighlight;
}
return highlightedIndex;
};
const handleOnStateChange = (changes, _ref3) => {
let {
setHighlightedIndex: updateHighlightedIndex
} = _ref3;
const {
type
} = changes;
switch (type) {
case keyDownArrowDown:
case keyDownArrowUp:
setHighlightedIndex(changes.highlightedIndex);
break;
case blurButton:
case keyDownEscape:
setHighlightedIndex(changes.highlightedIndex);
break;
case clickButton:
setHighlightedIndex(changes.highlightedIndex);
break;
case changeInput:
updateHighlightedIndex(getHighlightedIndex(changes));
break;
}
};
const handleToggleClick = isOpen => event => {
if (onToggleClick) {
onToggleClick(event);
}
if (event.target === textInput.current && isOpen) {
event.preventDownshiftDefault = true;
event.persist();
}
};
const showWarning = !invalid && warn;
const className = cx__default["default"](`${prefix}--combo-box`, {
[`${prefix}--list-box--up`]: direction === 'top',
[`${prefix}--combo-box--warning`]: showWarning,
[`${prefix}--combo-box--readonly`]: readOnly
});
const titleClasses = cx__default["default"](`${prefix}--label`, {
[`${prefix}--label--disabled`]: disabled
});
const comboBoxHelperId = !helperText ? undefined : `combobox-helper-text-${comboBoxInstanceId}`;
const helperClasses = cx__default["default"](`${prefix}--form__helper-text`, {
[`${prefix}--form__helper-text--disabled`]: disabled
});
const wrapperClasses = cx__default["default"](`${prefix}--list-box__wrapper`, [containerClassName, {
[`${prefix}--list-box__wrapper--fluid--invalid`]: isFluid && invalid,
[`${prefix}--list-box__wrapper--fluid--focus`]: isFluid && isFocused
}]);
const inputClasses = cx__default["default"](`${prefix}--text-input`, {
[`${prefix}--text-input--empty`]: !inputValue,
[`${prefix}--combo-box--input--focus`]: isFocused && !isFluid
});
// needs to be Capitalized for react to render it correctly
const ItemToElement = itemToElement;
return /*#__PURE__*/React__default["default"].createElement(Downshift__default["default"], _rollupPluginBabelHelpers["extends"]({}, downshiftProps, {
onChange: handleOnChange,
onInputValueChange: handleOnInputValueChange,
onStateChange: function () {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
handleOnStateChange(...args);
downshiftProps?.onStateChange?.(...args);
},
inputValue: inputValue || '',
itemToString: itemToString,
initialSelectedItem: initialSelectedItem,
inputId: id,
selectedItem: selectedItem
}), _ref4 => {
let {
getInputProps,
getItemProps,
getLabelProps,
getMenuProps,
getRootProps,
getToggleButtonProps,
isOpen,
inputValue,
selectedItem,
clearSelection,
toggleMenu
} = _ref4;
const rootProps = getRootProps(
// @ts-ignore this is not supposed to be a required property
{}, {
suppressRefError: true
});
const labelProps = getLabelProps();
const buttonProps = getToggleButtonProps({
disabled: disabled || readOnly,
onClick: handleToggleClick(isOpen),
// 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 if the menu is already open.
// This allows the toggleMenu behavior for the toggleButton to correctly open and
// close the menu.
onMouseUp(event) {
if (isOpen) {
event.stopPropagation();
}
}
});
const inputProps = getInputProps({
// Remove excess aria `aria-labelledby`. HTML <label for> provides this aria information.
'aria-labelledby': null,
disabled,
placeholder,
onClick() {
toggleMenu();
},
onKeyDown: event => {
if (match.match(event, keys.Space)) {
event.stopPropagation();
}
if (match.match(event, keys.Enter) && !inputValue) {
toggleMenu();
}
if (match.match(event, keys.Escape) && inputValue) {
if (event.target === textInput.current && isOpen) {
toggleMenu();
event.preventDownshiftDefault = true;
event.persist();
}
}
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);
}
}
});
const handleFocus = evt => {
setIsFocused(evt.type === 'focus');
};
const readOnlyEventHandlers = readOnly ? {
onKeyDown: evt => {
// This prevents the select from opening for the above keys
if (evt.key !== 'Tab') {
evt.preventDefault();
}
}
} : {};
return /*#__PURE__*/React__default["default"].createElement("div", {
className: wrapperClasses
}, titleText && /*#__PURE__*/React__default["default"].createElement(Text.Text, _rollupPluginBabelHelpers["extends"]({
as: "label",
className: titleClasses
}, labelProps), titleText), /*#__PURE__*/React__default["default"].createElement(index["default"], {
onFocus: handleFocus,
onBlur: handleFocus,
className: className,
disabled: disabled,
invalid: invalid,
invalidText: invalidText,
isOpen: isOpen,
light: light,
size: size,
warn: warn,
warnText: warnText
}, /*#__PURE__*/React__default["default"].createElement("div", {
className: `${prefix}--list-box__field`
}, /*#__PURE__*/React__default["default"].createElement("input", _rollupPluginBabelHelpers["extends"]({
role: "combobox",
disabled: disabled,
className: inputClasses,
type: "text",
tabIndex: "0",
"aria-autocomplete": "list",
"aria-expanded": rootProps['aria-expanded'],
"aria-haspopup": "listbox",
"aria-controls": inputProps['aria-controls'],
"aria-owns": getMenuProps().id,
title: textInput?.current?.value
}, inputProps, rest, readOnlyEventHandlers, {
readOnly: readOnly,
ref: mergeRefs["default"](textInput, ref),
"aria-describedby": helperText && !invalid && !warn && !isFluid ? comboBoxHelperId : undefined
})), invalid && /*#__PURE__*/React__default["default"].createElement(MskIcon.Icon, {
icon: "warning",
className: `${prefix}--list-box__invalid-icon`
}), showWarning && /*#__PURE__*/React__default["default"].createElement(MskIcon.Icon, {
icon: "warning",
className: `${prefix}--list-box__invalid-icon ${prefix}--list-box__invalid-icon--warning`
}), inputValue && /*#__PURE__*/React__default["default"].createElement(ListBoxSelection["default"], {
clearSelection: clearSelection,
translateWithId: translateWithId,
disabled: disabled || readOnly,
onClearSelection: handleSelectionClear,
selectionCount: 0
}), /*#__PURE__*/React__default["default"].createElement(ListBoxTrigger["default"], _rollupPluginBabelHelpers["extends"]({}, buttonProps, {
isOpen: isOpen,
translateWithId: translateWithId
}))), /*#__PURE__*/React__default["default"].createElement(index["default"].Menu, getMenuProps({
'aria-label': deprecatedAriaLabel || ariaLabel
}), isOpen ? filterItems(items, itemToString, inputValue).map((item, index$1) => {
const itemProps = getItemProps({
item,
index: index$1,
['aria-current']: selectedItem === item ? 'true' : 'false',
['aria-selected']: highlightedIndex === index$1 ? 'true' : 'false',
disabled: item.disabled
});
return /*#__PURE__*/React__default["default"].createElement(index["default"].MenuItem, _rollupPluginBabelHelpers["extends"]({
key: itemProps.id,
isActive: selectedItem === item,
isHighlighted: highlightedIndex === index$1,
title: itemToElement ? item.text : itemToString ? itemToString(item) : undefined
}, itemProps), itemToElement ?
/*#__PURE__*/
// @ts-ignore
React__default["default"].createElement(ItemToElement, _rollupPluginBabelHelpers["extends"]({
key: itemProps.id
}, item)) : itemToString ? itemToString(item) : defaultItemToString(item), selectedItem === item && /*#__PURE__*/React__default["default"].createElement(MskIcon.Icon, {
icon: "check",
className: `${prefix}--list-box__menu-item__selected-icon`
}));
}) : null)), helperText && !invalid && !warn && !isFluid && (_Text || (_Text = /*#__PURE__*/React__default["default"].createElement(Text.Text, {
as: "div",
id: comboBoxHelperId,
className: helperClasses
}, helperText))));
});
});
ComboBox.displayName = 'ComboBox';
ComboBox.propTypes = {
/**
* 'aria-label' of the ListBox component.
* 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.
* 'aria-label' of the ListBox component.
*/
ariaLabel: deprecate["default"](PropTypes__default["default"].string, 'This prop syntax has been deprecated. Please use the new `aria-label`.'),
/**
* An optional className to add to the container node
*/
className: PropTypes__default["default"].string,
/**
* Specify the direction of the combobox dropdown. Can be either top or bottom.
*/
direction: PropTypes__default["default"].oneOf(['top', 'bottom']),
/**
* Specify if the control should be disabled, or not
*/
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"].string,
/**
* Specify a custom `id` for the input
*/
id: PropTypes__default["default"].string.isRequired,
/**
* Allow users to pass in an arbitrary item or a string (in case their items are an array of strings)
* from their collection that are pre-selected
*/
initialSelectedItem: PropTypes__default["default"].oneOfType([PropTypes__default["default"].object, PropTypes__default["default"].string, PropTypes__default["default"].number]),
/**
* Specify if the currently selected value is invalid.
*/
invalid: PropTypes__default["default"].bool,
/**
* Message which is displayed if the value is invalid.
*/
invalidText: PropTypes__default["default"].node,
/**
* Optional 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,
/**
* should use "light theme" (white background)?
*/
light: deprecate["default"](PropTypes__default["default"].bool, 'The `light` prop for `Combobox` has ' + 'been deprecated in favor of the new `Layer` component. It will be removed in the next major release.'),
/**
* `onChange` is a utility for this controlled component to communicate to a
* consuming component when a specific dropdown item is selected.
* `({ selectedItem }) => void`
* @param {{ selectedItem }}
*/
onChange: PropTypes__default["default"].func.isRequired,
/**
* Callback function to notify consumer when the text input changes.
* This provides support to change available items based on the text.
* `(inputText) => void`
* @param {string} inputText
*/
onInputChange: PropTypes__default["default"].func,
/**
* Helper function passed to Downshift that allows the user to observe internal
* state changes
* `(changes, stateAndHelpers) => void`
*/
onStateChange: PropTypes__default["default"].func,
/**
* Callback function that fires when the combobox menu toggle is clicked
* `(evt) => void`
* @param {MouseEvent} event
*/
onToggleClick: PropTypes__default["default"].func,
/**
* Used to provide a placeholder text node before a user enters any input.
* This is only present if the control has no items selected
*/
placeholder: PropTypes__default["default"].string,
/**
* Is the ComboBox readonly?
*/
readOnly: PropTypes__default["default"].bool,
/**
* For full control of the selection
*/
selectedItem: PropTypes__default["default"].oneOfType([PropTypes__default["default"].object, PropTypes__default["default"].string, PropTypes__default["default"].number]),
/**
* Specify your own filtering logic by passing in a `shouldFilterItem`
* function that takes in the current input and an item and passes back
* whether or not the item should be filtered.
*/
shouldFilterItem: PropTypes__default["default"].func,
/**
* 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
* combobox via ARIA attributes.
*/
titleText: PropTypes__default["default"].node,
/**
* Specify a custom translation function that takes in a message identifier
* and returns the localized string for the message
*/
translateWithId: PropTypes__default["default"].func,
/**
* Currently supports either the default type, or an inline variant
*/
type: ListBoxPropTypes.ListBoxType,
/**
* 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
};
ComboBox.defaultProps = {
disabled: false,
itemToString: defaultItemToString,
itemToElement: null,
shouldFilterItem: defaultShouldFilterItem,
type: 'default',
['aria-label']: 'Choose an item',
direction: 'bottom'
};
var ComboBox$1 = ComboBox;
exports["default"] = ComboBox$1;