import React from "react"; import { ComponentProps } from "widgets/BaseComponent"; import { Classes } from "@blueprintjs/core"; import { DropdownOption } from "../constants"; import { IItemListRendererProps, IItemRendererProps, } from "@blueprintjs/select"; import { debounce, findIndex, isEmpty, isNil, isNumber } from "lodash"; import "../../../../node_modules/@blueprintjs/select/lib/css/blueprint-select.css"; import { FixedSizeList } from "react-window"; import { TextSize } from "constants/WidgetConstants"; import { StyledLabel, TextLabelWrapper, StyledControlGroup, StyledSingleDropDown, DropdownStyles, DropdownContainer, MenuItem, } from "./index.styled"; import Fuse from "fuse.js"; import { WidgetContainerDiff } from "widgets/WidgetUtils"; import SelectButton from "./SelectButton"; const FUSE_OPTIONS = { shouldSort: true, threshold: 0.5, location: 0, minMatchCharLength: 3, findAllMatches: true, keys: ["label", "value"], }; const DEBOUNCE_TIMEOUT = 800; const ITEM_SIZE = 40; interface SelectComponentState { activeItemIndex: number | undefined; query?: string; isOpen?: boolean; } class SelectComponent extends React.Component< SelectComponentProps, SelectComponentState > { state = { // used to show focused item for keyboard up down key interection activeItemIndex: 0, query: "", isOpen: false, }; componentDidMount = () => { // set default selectedIndex as focused index this.setState({ activeItemIndex: this.props.selectedIndex ?? 0 }); this.setState({ query: this.props.filterText }); }; componentDidUpdate = (prevProps: SelectComponentProps) => { if ( prevProps.selectedIndex !== this.props.selectedIndex && this.state.activeItemIndex !== this.props.selectedIndex ) { // update focus index if selectedIndex changed by property pane this.setState({ activeItemIndex: this.props.selectedIndex }); } }; togglePopoverVisibility = () => { this.setState({ isOpen: !this.state.isOpen }); }; handleActiveItemChange = (activeItem: DropdownOption | null) => { // Update state.activeItemIndex if activeItem is different from the current value if ( activeItem?.value !== this.props?.options[this.state.activeItemIndex]?.value ) { // find new index from options const activeItemIndex = findIndex(this.props.options, [ "label", activeItem?.label, ]); this.setState({ activeItemIndex }); } }; itemListPredicate(query: string, items: DropdownOption[]) { if (!query) return items; const fuse = new Fuse(items, FUSE_OPTIONS); return fuse.search(query); } onItemSelect = (item: DropdownOption): void => { this.props.onOptionSelected(item); // If Popover is open, then toggle visibility. // Required when item selection is made via keyboard input. if (this.state.isOpen) this.togglePopoverVisibility(); }; isOptionSelected = (selectedOption: DropdownOption) => { if (this.props.value) return selectedOption.value === this.props.value; const optionIndex = findIndex(this.props.options, (option) => { return option.value === selectedOption.value; }); return optionIndex === this.props.selectedIndex; }; onQueryChange = (filterValue: string) => { this.setState({ query: filterValue }); if (!this.props.serverSideFiltering) return; return this.serverSideSearch(filterValue); }; serverSideSearch = debounce((filterValue: string) => { this.props.onFilterChange(filterValue); }, DEBOUNCE_TIMEOUT); renderSingleSelectItem = ( option: DropdownOption, itemProps: IItemRendererProps, ) => { if (!this.state.isOpen) return null; if (!itemProps.modifiers.matchesPredicate) { return null; } const isSelected: boolean = this.isOptionSelected(option); // For tabbable menuItems const isFocused = itemProps.modifiers.active; const focusClassName = `${isFocused && "has-focus"}`; const selectedClassName = `${isSelected} && "menu-item-active"`; return (
{option.label}
); }; handleCancelClick = (event: React.MouseEvent) => { event.stopPropagation(); this.onItemSelect({}); }; handleCloseList = () => { if (this.state.isOpen) { this.togglePopoverVisibility(); if (!this.props.selectedIndex) return; return this.handleActiveItemChange( this.props.options[this.props.selectedIndex], ); } }; noResultsUI = (
No Results Found
); itemListRenderer = ( props: IItemListRendererProps, ): JSX.Element | null => { if (!this.state.isOpen) return null; let activeItemIndex = this.props.selectedIndex || null; if (props.activeItem && activeItemIndex === null) { activeItemIndex = props.filteredItems?.findIndex( (item) => item.value === props.activeItem?.value, ); } if (!props.filteredItems || !props.filteredItems.length) return this.noResultsUI; return this.renderList( props.filteredItems, activeItemIndex, props.renderItem, ); }; renderList = ( items: DropdownOption[], activeItemIndex: number | null, renderItem: (item: any, index: number) => JSX.Element | null, ): JSX.Element | null => { const width: number = Math.floor(this.props.width); const RowRenderer = (itemProps: any) => (
{renderItem(items[itemProps.index], itemProps.index)}
); return ( {RowRenderer} ); }; render() { const { compactMode, disabled, isLoading, labelStyle, labelText, labelTextColor, labelTextSize, widgetId, } = this.props; // active focused item const activeItem = !isEmpty(this.props.options) ? this.props.options[this.state.activeItemIndex] : undefined; // get selected option label from selectedIndex const selectedOption = !isEmpty(this.props.options) && this.props.selectedIndex !== undefined && this.props.selectedIndex > -1 ? this.props.options[this.props.selectedIndex].label : this.props.label; // for display selected option, there is no separate option to show placeholder const value = !isNil(selectedOption) && selectedOption !== "" ? selectedOption : this.props.placeholder || "-- Select --"; return ( {labelText && ( {labelText} )} ); } } export interface SelectComponentProps extends ComponentProps { disabled?: boolean; onOptionSelected: (optionSelected: DropdownOption) => void; placeholder?: string; labelText?: string; labelTextColor?: string; labelTextSize?: TextSize; labelStyle?: string; compactMode: boolean; selectedIndex?: number; options: DropdownOption[]; isLoading: boolean; isFilterable: boolean; isValid: boolean; width: number; dropDownWidth: number; height: number; serverSideFiltering: boolean; hasError?: boolean; onFilterChange: (text: string) => void; value?: string; label?: string; filterText?: string; } export default React.memo(SelectComponent);