import React, { FocusEvent, HTMLProps } from 'react' import Downshift from 'downshift' import styled from 'styled-components' import * as O from 'fp-ts/lib/Option' import { pipe } from 'fp-ts/lib/pipeable' import { flexFlow } from '@monorail/helpers/flex' import { isNonEmptyString } from '@monorail/sharedHelpers/typeGuards' import { DisplayType } from '@monorail/visualComponents/inputs/inputTypes' import { Label } from '@monorail/visualComponents/inputs/Label' import { useFormMultiSelectInput } from './FormMultiSelectInput.hooks' import { FormMultiSelectInputProps } from './FormMultiSelectInput.types' const ENTER_KEY_VALUE = 'Enter' const ESCAPE_KEY_VALUE = 'Escape' const FlexColumn = styled.div` ${flexFlow('column')}; margin-bottom: 24px; width: 100%; ` /** * Provides the functionality behind creating an input of some kind that will * filter a set of suggestions or can be used to create new values by pressing * the 'Enter' key. Multiple UIs have been mentioned by design, so this * defers UI concerns to render props to leave it open for extension. * * It has been attempted to try to satisfy the Open-Closed Principle - the "O" in SOLID - * to make it easy enough to build forms with multiple UI variants with the same * multi-select functionality. * * @example *
* -------------------------- * | renderSelectedOptions | * -------------------------- * | renderInput | * -------------------------- * | renderSuggestions | * -------------------------- *
*/ export function FormMultiSelectInput(props: FormMultiSelectInputProps) { const { containerCss, defaultHighlightedIndex, disabled, display, label, renderInput, renderSelectedOptions, renderSuggestions, required = false, searchValueToItem, selectedOptions, } = props const { addItem, checkIsHighlighted, removeOption, searchValue, setSearchValue, suggestions, } = useFormMultiSelectInput(props) return ( {({ getInputProps, getItemProps, getRootProps, highlightedIndex, isOpen, toggleMenu, }) => { const defaultInputProps = { disabled, onFocus: () => toggleMenu({ isOpen: true }), onBlur: (ev: FocusEvent) => { toggleMenu({ isOpen: false }) setSearchValue('') ev.target.value = '' }, onChange: (ev: { currentTarget: HTMLInputElement }) => setSearchValue(ev.currentTarget.value), onKeyDown: ( ev: KeyboardEvent & { currentTarget: HTMLInputElement }, ) => { const notSelectingHighlightedOption = highlightedIndex === null const enterKeyWasPressed = ev.key === ENTER_KEY_VALUE if (notSelectingHighlightedOption && enterKeyWasPressed) { // Don't trigger a form submit ev.preventDefault() pipe( ev.currentTarget.value, val => val.replace(/\s+/g, ' ').trim(), // remove excess whitespace O.fromPredicate(isNonEmptyString), // confirm searchValue is non-empty O.chain(searchValueToItem), O.fold( () => {}, v => { addItem(v) ev.currentTarget.value = '' }, ), ) } }, placeholder: 'Type any tag...', } const suggestionInfo = { getSuggestionProps: getItemProps, isHighlighted: (option: A) => checkIsHighlighted(option, highlightedIndex), isOpen: isOpen && suggestions.length > 0 && searchValue.length > 0, searchValue, isFocused: isOpen, selectedOptions, } const sectionProps = getRootProps() as HTMLProps return ( ) }} ) }