import classNames from "classnames"; import * as React from "react"; import { DISPLAYNAME_PREFIX, IPopoverProps, ITagInputProps, Keys, Popover, Position, TagInput, Utils, } from "reui-core"; import { Classes, IListItemsProps } from "../../common"; import { IQueryListRendererProps, QueryList } from "../query-list/queryList"; export interface IMultiSelectProps extends IListItemsProps { /** Controlled selected values. */ selectedItems?: T[]; /** * Whether the popover opens on key down or when `TagInput` is focused. * @default false */ openOnKeyDown?: boolean; /** Props to spread to `Popover`. Note that `content` cannot be changed. */ popoverProps?: Partial & object; /** Props to spread to `TagInput`. Use `query` and `onQueryChange` to control the input. */ tagInputProps?: Partial & object; /** Custom renderer to transform an item into tag content. */ tagRenderer: (item: T) => React.ReactNode; } export interface IMultiSelectState { isOpen: boolean; } export class MultiSelect extends React.PureComponent, IMultiSelectState> { public static displayName = `${DISPLAYNAME_PREFIX}.MultiSelect`; public static ofType() { return MultiSelect as new (props: IMultiSelectProps) => MultiSelect; } public state: IMultiSelectState = { isOpen: (this.props.popoverProps && this.props.popoverProps.isOpen) || false, }; private TypedQueryList = QueryList.ofType(); private input?: HTMLInputElement | null; private queryList?: QueryList | null; private refHandlers = { input: (ref: HTMLInputElement | null) => { this.input = ref; const { tagInputProps = {} } = this.props; Utils.safeInvoke(tagInputProps.inputRef, ref); }, queryList: (ref: QueryList | null) => (this.queryList = ref), }; public render() { // omit props specific to this component, spread the rest. const { openOnKeyDown, popoverProps, tagInputProps, ...restProps } = this.props; return ( ); } private renderQueryList = (listProps: IQueryListRendererProps) => { const { tagInputProps = {}, popoverProps = {}, selectedItems = [] } = this.props; const { handleKeyDown, handleKeyUp } = listProps; return (
{listProps.itemList}
); }; private handleItemSelect = (item: T, evt?: React.SyntheticEvent) => { if (this.input != null) { this.input.focus(); } Utils.safeInvoke(this.props.onItemSelect, item, evt); }; private handleQueryChange = (query: string, evt?: React.ChangeEvent) => { this.setState({ isOpen: query.length > 0 || !this.props.openOnKeyDown }); Utils.safeInvoke(this.props.onQueryChange, query, evt); }; private handlePopoverInteraction = (nextOpenState: boolean) => requestAnimationFrame(() => { // deferring to rAF to get properly updated activeElement const { popoverProps = {} } = this.props; if (this.input != null && this.input !== document.activeElement) { // the input is no longer focused so we can close the popover this.setState({ isOpen: false }); } else if (!this.props.openOnKeyDown) { // open the popover when focusing the tag input this.setState({ isOpen: true }); } Utils.safeInvoke(popoverProps.onInteraction, nextOpenState); }); private handlePopoverOpened = (node: HTMLElement) => { const { popoverProps = {} } = this.props; if (this.queryList != null) { // scroll active item into view after popover transition completes and all dimensions are stable. this.queryList.scrollActiveItemIntoView(); } Utils.safeInvoke(popoverProps.onOpened, node); }; private getTargetKeyDownHandler = ( handleQueryListKeyDown: React.EventHandler>, ) => { return (e: React.KeyboardEvent) => { const { which } = e; if (which === Keys.ESCAPE || which === Keys.TAB) { // By default the escape key will not trigger a blur on the // input element. It must be done explicitly. if (this.input != null) { this.input.blur(); } this.setState({ isOpen: false }); } else if (!(which === Keys.BACKSPACE || which === Keys.ARROW_LEFT || which === Keys.ARROW_RIGHT)) { this.setState({ isOpen: true }); } if (this.state.isOpen) { Utils.safeInvoke(handleQueryListKeyDown, e); } }; }; }