import classNames from "classnames"; import * as React from "react"; import { DISPLAYNAME_PREFIX, HTMLInputProps, IInputGroupProps, InputGroup, IPopoverProps, Keys, Popover, Position, Utils, } from "reui-core"; import { Classes, IListItemsProps } from "../../common"; import { IQueryListRendererProps, QueryList } from "../query-list/queryList"; export interface ISuggestProps extends IListItemsProps { /** * Whether the popover should close after selecting an item. * @default true */ closeOnSelect?: boolean; /** * Props to spread to `InputGroup`. All props are supported except `ref` (use `inputRef` instead). * If you want to control the filter input, you can pass `value` and `onChange` here * to override `Suggest`'s own behavior. */ inputProps?: IInputGroupProps & HTMLInputProps; /** Custom renderer to transform an item into a string for the input value. */ inputValueRenderer: (item: T) => string; /** * Whether the popover opens on key down or when the input is focused. * @default false */ openOnKeyDown?: boolean; /** Props to spread to `Popover`. Note that `content` cannot be changed. */ popoverProps?: Partial & object; } export interface ISuggestState { isOpen: boolean; selectedItem?: T; } export class Suggest extends React.PureComponent, ISuggestState> { public static displayName = `${DISPLAYNAME_PREFIX}.Suggest`; // Note: can't use in static members, so this remains dynamically typed. public static defaultProps = { closeOnSelect: true, openOnKeyDown: false, }; public static ofType() { return Suggest as new (props: ISuggestProps) => Suggest; } public state: ISuggestState = { 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 { inputProps = {} } = this.props; Utils.safeInvoke(inputProps.inputRef, ref); }, queryList: (ref: QueryList | null) => (this.queryList = ref), }; public render() { // omit props specific to this component, spread the rest. const { inputProps, popoverProps, ...restProps } = this.props; return ( ); } public componentDidUpdate(_prevProps: ISuggestProps, prevState: ISuggestState) { if (this.state.isOpen && !prevState.isOpen && this.queryList != null) { this.queryList.scrollActiveItemIntoView(); } } private renderQueryList = (listProps: IQueryListRendererProps) => { const { inputProps = {}, popoverProps = {} } = this.props; const { isOpen, selectedItem } = this.state; const { handleKeyDown, handleKeyUp } = listProps; const { placeholder = "Search..." } = inputProps; const selectedItemText = selectedItem ? this.props.inputValueRenderer(selectedItem) : ""; return (
{listProps.itemList}
); }; private selectText = () => { // wait until the input is properly focused to select the text inside of it requestAnimationFrame(() => { if (this.input != null) { this.input.setSelectionRange(0, this.input.value.length); } }); }; private handleInputFocus = (event: React.FocusEvent) => { const { openOnKeyDown, inputProps = {} } = this.props; this.selectText(); // TODO can we leverage Popover.openOnTargetFocus for this? if (!openOnKeyDown) { this.setState({ isOpen: true }); } Utils.safeInvoke(inputProps.onFocus, event); }; private handleItemSelect = (item: T, event?: React.SyntheticEvent) => { let nextOpenState: boolean; if (!this.props.closeOnSelect) { if (this.input != null) { this.input.focus(); } this.selectText(); nextOpenState = true; } else { if (this.input != null) { this.input.blur(); } nextOpenState = false; } this.setState({ isOpen: nextOpenState, selectedItem: item, }); Utils.safeInvoke(this.props.onItemSelect, item, event); }; private handlePopoverInteraction = (nextOpenState: boolean) => requestAnimationFrame(() => { 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 }); } Utils.safeInvoke(popoverProps.onInteraction, nextOpenState); }); private handlePopoverOpened = (node: HTMLElement) => { const { popoverProps = {} } = this.props; // scroll active item into view after popover transition completes and all dimensions are stable. if (this.queryList != null) { this.queryList.scrollActiveItemIntoView(); } Utils.safeInvoke(popoverProps.onOpened, node); }; private getTargetKeyDownHandler = ( handleQueryListKeyDown: React.EventHandler>, ) => { return (evt: React.KeyboardEvent) => { const { which } = evt; const { inputProps = {}, openOnKeyDown } = this.props; if (which === Keys.ESCAPE || which === Keys.TAB) { if (this.input != null) { this.input.blur(); } this.setState({ isOpen: false, }); } else if ( openOnKeyDown && which !== Keys.BACKSPACE && which !== Keys.ARROW_LEFT && which !== Keys.ARROW_RIGHT ) { this.setState({ isOpen: true }); } if (this.state.isOpen) { Utils.safeInvoke(handleQueryListKeyDown, evt); } Utils.safeInvoke(inputProps.onKeyDown, evt); }; }; private getTargetKeyUpHandler = (handleQueryListKeyUp: React.EventHandler>) => { return (evt: React.KeyboardEvent) => { const { inputProps = {} } = this.props; if (this.state.isOpen) { Utils.safeInvoke(handleQueryListKeyUp, evt); } Utils.safeInvoke(inputProps.onKeyUp, evt); }; }; }