import * as React from 'react'; import { ValidationRule, Validation, ValidationState } from '../../models/Validation'; import { validation } from '../../utils/helper'; import validationRules from '../../utils/validationRules'; import { Query } from '../../models/query'; import { Colors } from '../../models/colors'; export interface DropdownProps { label?: string; type?: string; name?: string; validationStates?: { [name: string]: Validation }; placeholder?: string; validationRules?: Array; required?: boolean; options?: Array<{ label: string; id: string }>; optionsWithCategories?: Array<{ category: string; options: Array<{ label: string; id: string }>; }>; value?: string; query?: Query; automationName?: string; valid?: ValidationState; errorMessage?: string; palette?: Colors; notifyValidity?: (name: string, state: ValidationState, errorMessage?: string) => void; onOptionClick?: (option: { label: string; id: string }) => void; } type DropdownContainer = DropdownProps & { render: ( onOptionClick: (option: { label: string; id: string }) => void, onclick: (e: React.MouseEvent) => void, onKeyDown: ( value: string, e: React.KeyboardEvent, option?: { label: string; id: string } ) => void, value: string, opened: boolean, ref: React.RefObject, query: string, close: () => void ) => JSX.Element; }; interface DropdownState { value: string; opened: boolean; focusedLi: number; } class Dropdown extends React.Component { private ref: React.RefObject; constructor(props: DropdownContainer) { super(props); this.state = { value: this.props.value, opened: false, focusedLi: -1 }; this.ref = React.createRef(); } shouldComponentUpdate(nextProps: DropdownContainer, nextState: DropdownState) { return ( this.state.value !== nextState.value || this.state.opened !== nextState.opened || this.props.valid !== nextProps.valid ); } open = () => { if (this.state.opened) return; if ( (this.props.options && this.props.options.length > 0) || (this.props.optionsWithCategories && this.props.optionsWithCategories.length > 0) ) { this.setState({ opened: true }); if (this.props.query !== Query.small) { document.addEventListener('click', this.close); } } }; close = () => { this.setState({ opened: false, focusedLi: -1 }); if (this.props.query !== Query.small) { document.removeEventListener('click', this.close); } }; onOptionClick = (option: { label: string; id: string }) => { if (this.props.onOptionClick) { this.props.onOptionClick(option); let assignedValidationRules = Object.assign([], this.props.validationRules || []); if (this.props.required) { assignedValidationRules.unshift({ name: 'required' }); } let { state, errorMessage } = validation( validationRules, assignedValidationRules, option.label, this.props.name ); if (this.props.notifyValidity) { this.props.notifyValidity(this.props.name, state, errorMessage); } } this.setState({ value: option.label }); this.close(); }; onClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); e.nativeEvent.stopImmediatePropagation(); if ((this.props.options || this.props.optionsWithCategories) && !this.state.opened) { this.open(); } if (this.state.opened) { this.close(); } }; onKeyDown = (value: string, e: React.KeyboardEvent, option?: { label: string; id: string }) => { let options: Array<{ label: string; id: string }> = []; if (this.props.optionsWithCategories && this.props.optionsWithCategories.length) { this.props.optionsWithCategories.forEach(option => { options = options.concat(option.options); }); } else { options = this.props.options; } if (e.keyCode === 40) { //arrow down e.preventDefault(); if (!this.state.opened) { return this.setState({ opened: true }); } let focusedIndex = this.state.focusedLi === options.length - 1 ? 0 : this.state.focusedLi + 1; Array.from(this.ref.current.querySelectorAll('li')) .filter(li => { return !li.dataset['category']; }) [focusedIndex].focus(); this.setState({ focusedLi: focusedIndex, value: options[focusedIndex].label }); } if (e.keyCode === 38) { //arrow up e.preventDefault(); if (!this.state.opened) return; let focusedIndex = this.state.focusedLi === 0 ? options.length - 1 : this.state.focusedLi - 1; Array.from(this.ref.current.querySelectorAll('li')) .filter(li => { return !li.dataset['category']; }) [focusedIndex].focus(); this.setState({ focusedLi: focusedIndex, value: options[focusedIndex].label }); } if (e.keyCode === 13) { //enter this.onOptionClick(option); } if (e.keyCode === 9) { //tab this.close(); } }; render() { return this.props.render( this.onOptionClick, this.onClick, this.onKeyDown, this.state.value, this.state.opened, this.ref, this.props.query, this.close ); } } export default Dropdown;