import React, { ReactElement, useLayoutEffect, useRef, useState } from 'react' import Downshift from 'downshift' import styled, { css } from 'styled-components' import * as O from 'fp-ts/lib/Option' import { pipe } from 'fp-ts/lib/pipeable' import { baseDisabledStyles, baseErrorBackgroundStyles, baseErrorBorderStyles, BorderRadius, borderRadius, flexFlow, FontSizes, typographyFont, } from '@monorail/helpers/exports' import { defaultPopOverPosition, getOverlayPosition, } from '@monorail/metaComponents/popOver/PopOver' import { PortalController } from '@monorail/metaComponents/portal/PortalController' import { isUndefined } from '@monorail/sharedHelpers/typeGuards' import { CommonComponentType } from '@monorail/types' import { DisplayType } from '@monorail/visualComponents/inputs/inputTypes' import { Label } from '@monorail/visualComponents/inputs/Label' import { StdErr } from '@monorail/visualComponents/inputs/StdErr' import { Menu, MenuContent } from '@monorail/visualComponents/menu/Menu' import { DropdownItem } from './DropdownItem' import { DownshiftGetInputProps, DownshiftItemPropsGetter, DownshiftMenuPropsGetter, DropdownStateType, DropdownType, } from './helpers' import { InteractionController } from './interaction' import { DropdownParser } from './parsers' import { createDefaultDropdownRender, DropdownRender } from './render' type DropdownContainerProps = CommonComponentType & { disabled: boolean error: boolean } const DropdownWrapper = styled.div` ${flexFlow('column')} min-height: 25px; /* fix for cut off bottom border */ flex: 1; ` const DropdownContainer = styled.div( ({ disabled, error }) => css` ${borderRadius(BorderRadius.Small)}; ${flexFlow('column')}; ${typographyFont(400, FontSizes.Title5)}; flex: 1; position: relative; width: 100%; min-height: 24px; ${error && css` ${baseErrorBackgroundStyles}; input { background: inherit; ${baseErrorBorderStyles}; } `} ${disabled && baseDisabledStyles}; `, ) const HandlerContainer = styled.div` ${flexFlow('column')}; border-radius: inherit; flex: 1; pointer-events: auto; position: relative; width: 100%; ` const MenuContainer = styled.div` height: 100%; overflow: auto; ` export const ItemContainer = styled.div`` export type DropdownSkinCommonType = { placeholder?: string disabled?: boolean clearable?: boolean error?: O.Option required?: boolean label?: string display?: DisplayType extraWidth?: number } export type DropdownSkinHookProps = { parser: DropdownParser interaction: InteractionController } & DropdownSkinCommonType & CommonComponentType export type DropdownSkinComponent = ( props: DropdownSkinHookProps, ) => (state: DropdownStateType) => ReactElement type DropdownSkinProps = { skin: DropdownSkinHookProps state: DropdownStateType render: DropdownRender } export const DropdownSkin = ({ skin, state, render, }: DropdownSkinProps): ReactElement> => { /** Menu references **/ const [menuTarget, setMenuTarget] = useState() const menuRef = useRef(null) /* eslint-disable react-hooks/exhaustive-deps */ useLayoutEffect(() => { if (menuRef && menuRef.current) { setMenuTarget(menuRef.current) } }, [menuRef.current]) /* eslint-enable react-hooks/exhaustive-deps */ const { items, downshiftProps } = state const { disabled, error, label, required, display, clearable } = skin const renderHandler = () => { const { getInputProps, itemToString, selectedItem, toggleMenu, } = downshiftProps const { interaction, placeholder } = skin const dropdownValue = pipe( O.fromNullable(selectedItem), O.map(itemToString), O.toUndefined, ) const inputOptions: DownshiftGetInputProps = { disabled, placeholder, onKeyDown: interaction.eventHandler(state), onClick: () => toggleMenu({ type: Downshift.stateChangeTypes.clickButton, highlightedIndex: pipe( O.fromNullable(selectedItem), O.fold( () => -1, item => items.indexOf(item), ), ), }), } const handlerProps = { ...getInputProps(inputOptions), display } return ( ) } const renderList = () => { const { parser } = skin const { getItemProps, highlightedIndex, selectedItem, itemToString, } = downshiftProps const ListComponent = isUndefined(render.list) ? React.Fragment : render.list return ( {items.map((item: T, index: number) => { const itemProps = { item, disabled: !parser.isActive(item), highlighted: highlightedIndex === index, selected: pipe( O.fromNullable(selectedItem), O.fold(() => false, parser.compare(item)), ), } const itemDownshiftProps = getItemProps({ ...itemProps, index, }) as DownshiftItemPropsGetter return ( {itemToString(item)} ) })} ) } const renderMenu = () => { const { isOpen, getMenuProps, toggleMenu } = downshiftProps const { extraWidth = 0 } = skin const menuProps = getMenuProps() as DownshiftMenuPropsGetter const position = menuTarget ? getOverlayPosition({ target: menuTarget }) : defaultPopOverPosition const width = menuTarget ? menuTarget.getBoundingClientRect().width + extraWidth : 0 return (
toggleMenu({ type: Downshift.stateChangeTypes.keyDownEscape, inputValue: '', }) } closingAnimationCompleted={() => {}} onClick={() => {}} width={width} > {items.length > 0 ? ( renderList() ) : ( No results )}
) } const renderError = () => ( <> {error && pipe( error, O.fold( () => <>, msg => (
), ), )} ) return ( {label && ( ) } const createDropdownSkin = ( render: DropdownRender, ): DropdownSkinComponent => skin => state => ( render={render} state={state} skin={skin} /> ) export const useDropdownSkin = ( skin: DropdownSkinHookProps, ) => createDropdownSkin(createDefaultDropdownRender())(skin) export const createDropdownCustomSkin = ( render: Partial>, ) => createDropdownSkin({ ...createDefaultDropdownRender(), ...render, })