UNPKG

@boomerang-io/carbon-addons-boomerang-react

Version:
224 lines (221 loc) 11.1 kB
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 };