@boomerang-io/carbon-addons-boomerang-react
Version:
Carbon Addons for Boomerang apps
224 lines (221 loc) • 11.1 kB
JavaScript
import React from 'react';
import { Tag } from '@carbon/react';
import { WarningFilled } from '@carbon/react/icons';
import Downshift from 'downshift';
import cx from 'classnames';
import isEqual from 'lodash.isequal';
import * as index from '../../internal/ListBox/index.js';
import { isAccessibleKeyDownEvent } from '../../tools/accessibility.js';
import { mapDownshiftProps } from '../../tools/createPropAdapter.js';
import setupGetInstanceId from '../../tools/setupGetInstanceId.js';
import { prefix } from '../../internal/settings.js';
/*
IBM Confidential
694970X, 69497O0
© Copyright IBM Corp. 2022, 2024
*/
const defaultItemToString = (item) => {
if (typeof item === "string") {
return item;
}
return item && item.label;
};
const defaultShouldFilterItem = ({ item, selectedItems, itemToString, inputValue }) => {
let keepItem = true;
const itemString = itemToString(item);
if (selectedItems.some((selectedItem) => itemString === itemToString(selectedItem)) ||
!itemString.toLowerCase().includes(inputValue.toLowerCase())) {
keepItem = false;
}
return keepItem;
};
const getInputValue = (state) => {
return state.inputValue || "";
};
const getInstanceId = setupGetInstanceId();
class MultiSelectComboBox extends React.Component {
static defaultProps = {
itemToString: defaultItemToString,
};
comboBoxInstanceId;
inputNode;
textInput;
constructor(props) {
super(props);
this.textInput = React.createRef();
this.comboBoxInstanceId = getInstanceId();
this.state = {
inputValue: getInputValue({}),
isOpen: props.open ?? false,
stateSelectedItems: props.initialSelectedItems || props.selectedItems || [],
};
}
//eslint disable-next-line
static getDerivedStateFromProps(nextProps, state) {
/**
* programmatically control this `open` prop
*/
const { open } = nextProps;
const { prevOpen } = state;
return prevOpen === open
? { inputValue: getInputValue(state) }
: {
isOpen: open,
prevOpen: open,
inputValue: getInputValue(state),
};
}
filterItems = (items, selectedItems, itemToString, inputValue) => {
const { shouldFilterItem = defaultShouldFilterItem } = this.props;
return shouldFilterItem
? items.filter((item) => shouldFilterItem({
item,
selectedItems,
itemToString,
inputValue,
}))
: items;
};
handleOnInputKeyDown = (event) => {
event.stopPropagation();
};
handleOnInputValueChange = (inputValue) => {
const { onInputChange } = this.props;
this.setState(() => ({
// Default to empty string if we have a false-y `inputValue`
inputValue: inputValue || "",
}), () => {
if (onInputChange) {
onInputChange(inputValue);
}
});
};
handleOnChange = (item) => {
if (!item) {
return;
}
const selectedItems = [...this.state.stateSelectedItems];
let selectedIndex;
selectedItems.forEach((selectedItem, index) => {
if (isEqual(selectedItem, item)) {
selectedIndex = index;
}
});
if (selectedIndex === undefined) {
selectedItems.push(item);
}
else {
selectedItems.splice(selectedIndex, 1);
}
this.setState({ stateSelectedItems: selectedItems });
if (typeof this.props.onChange === "function") {
this.props.onChange({ selectedItems });
}
};
onToggleClick = (isOpen) => (event) => {
if (this.props.onToggleClick) {
this.props.onToggleClick(event);
}
if (event.target === this.textInput.current && isOpen) {
event.preventDownshiftDefault = true;
event.persist();
}
};
openMenu = () => {
this.setState({ isOpen: true });
};
closeMenu = () => {
this.setState({ isOpen: false });
};
handleClearSelection = () => {
this.setState({ stateSelectedItems: [] });
if (typeof this.props.onChange === "function") {
this.props.onChange({ selectedItems: [] });
}
};
handleInputBlur = (e) => {
this.props.onInputBlur && this.props.onInputBlur(e);
this.closeMenu();
};
handleOnStateChange = (changes) => {
const { type } = changes;
switch (type) {
case Downshift.stateChangeTypes.keyDownEscape:
case Downshift.stateChangeTypes.mouseUp:
this.setState({ isOpen: false });
break;
// Opt-in to some cases where we should be toggling the menu based on
// a given key press or mouse handler
// Reference: https://github.com/paypal/downshift/issues/206
case Downshift.stateChangeTypes.clickButton:
case Downshift.stateChangeTypes.keyDownSpaceButton:
this.setState(() => {
let nextIsOpen = changes.isOpen || false;
if (changes.isOpen === false) {
// If Downshift is trying to close the menu, but we know the input
// is the active element in the document, then keep the menu open
if (this.inputNode === document.activeElement) {
nextIsOpen = true;
}
}
return {
isOpen: nextIsOpen,
};
});
break;
}
};
render() {
const { ariaLabel = "Choose an item", className: containerClassName, disabled = false, direction, downshiftProps, id, invalid, invalidText, items, itemToString = defaultItemToString, itemToElement, initialSelectedItems, titleText, helperText, placeholder, onChange, onInputBlur, onInputChange, light = false, selectedItems: propsSelectedItems, size,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
shouldFilterItem = defaultShouldFilterItem, tagProps, translateWithId, type = "default", ...rest } = this.props;
const { stateSelectedItems, isOpen } = this.state;
const { ListBox, ListBoxField: Field, ListBoxSelection: Selection, ListBoxMenu: Menu, ListBoxMenuItem: MenuItem, ListBoxMenuIcon: MenuIcon, } = index;
// externally controlled if selectedItems props exist
const selectedItems = propsSelectedItems || stateSelectedItems;
const className = cx(`${prefix}--combo-box`, containerClassName, {
[`${prefix}--list-box--up`]: direction === "top",
});
const titleClasses = cx(`${prefix}--label`, {
[`${prefix}--label--disabled`]: disabled,
});
const comboBoxHelperId = !helperText ? undefined : `combobox-helper-text-${this.comboBoxInstanceId}`;
const helperClasses = cx(`${prefix}--form__helper-text`, {
[`${prefix}--form__helper-text--disabled`]: disabled,
});
const wrapperClasses = cx(`${prefix}--list-box__wrapper`);
const inputClasses = cx(`${prefix}--text-input`, {
[`${prefix}--text-input--empty`]: !this.state.inputValue,
});
const ItemToElement = itemToElement;
return (React.createElement(Downshift, { ...mapDownshiftProps(downshiftProps), onChange: this.handleOnChange, onInputValueChange: this.handleOnInputValueChange, inputValue: this.state.inputValue || "", isOpen: isOpen, itemToString: itemToString, onStateChange: this.handleOnStateChange, onOuterClick: this.closeMenu, selectedItem: selectedItems }, ({ getInputProps, getItemProps, getLabelProps, getMenuProps, getToggleButtonProps, isOpen, inputValue, selectedItem, highlightedIndex, clearSelection, }) => (React.createElement("div", { className: wrapperClasses },
titleText && (React.createElement("label", { htmlFor: id, className: titleClasses, ...getLabelProps() }, titleText)),
React.createElement(ListBox, { className: className, disabled: disabled, invalid: invalid, "aria-label": ariaLabel, invalidText: invalidText, isOpen: isOpen, light: light, type: type, size: size },
React.createElement("div", { className: `${prefix}--bmrg-multi-select-selected` }, Array.isArray(selectedItems) &&
selectedItems.map((item, index) => {
const itemString = itemToString(item);
return (React.createElement(Tag, { key: `${itemString}-${index}`, disabled: disabled, type: "teal", onClick: () => this.handleOnChange(item), onKeyDown: (e) => isAccessibleKeyDownEvent(e) && this.handleOnChange(item), filter: true, ...tagProps }, itemString));
})),
React.createElement(Field, { id: id, disabled: disabled, ...getToggleButtonProps({
disabled,
onClick: this.onToggleClick(isOpen),
}) },
React.createElement("input", { className: inputClasses, "aria-label": ariaLabel, "aria-controls": `${id}__menu`, "aria-autocomplete": "list", tabIndex: 0, ref: this.textInput, ...rest, ...getInputProps({
disabled,
id,
placeholder,
onKeyDown: this.handleOnInputKeyDown,
onFocus: this.openMenu,
onBlur: this.handleInputBlur,
}) }),
invalid && React.createElement(WarningFilled, { size: 16, className: `${prefix}--list-box__invalid-icon` }),
(inputValue || selectedItems.length > 0) && (React.createElement(Selection, { clearSelection: clearSelection, onClearSelection: this.handleClearSelection, disabled: disabled, translateWithId: translateWithId })),
React.createElement(MenuIcon, { isOpen: isOpen, translateWithId: translateWithId })),
isOpen && (React.createElement(Menu, { id: id, ...getMenuProps({ "aria-label": ariaLabel }) }, this.filterItems(items, selectedItem, itemToString, inputValue).map((item, index) => {
const itemProps = getItemProps({ item, index });
return (React.createElement(MenuItem, { key: itemProps.id, isHighlighted: highlightedIndex === index, title: itemToElement ? item.text : itemToString(item), ...itemProps }, typeof ItemToElement !== "undefined" ? (React.createElement(ItemToElement, { key: itemToString(item), ...item })) : (itemToString(item))));
})))),
helperText && !invalid && (React.createElement("div", { id: comboBoxHelperId, className: helperClasses }, helperText))))));
}
}
export { MultiSelectComboBox as default };