dlx

import { forwardRef, useEffect, useRef, useState } from "react"

 

import type { ForwardRefReturn, PolymorphicProps, PolymorphicRef } from '../system'

import StyledInputDropdown, { StyledButtonWrapper, StyledPanelWrapper, type StyledInputDropdownProps, StyledInputDropdownOption, StyledInputDropdownOptionWrapper, StyledInputDropdownOptionIcon } from "./InputDropdown.styled"

import { AllowedElementType } from "./types"

import { generateId, safeRestProps } from "../utils";

import { Disclosure } from "../Disclosure";

import { Icon } from "../Icon";

import { Label } from "../Label";

import { InputField } from "../InputField"

import { HelperText } from "../HelperText";

 

export type listBoxRoles = "listbox" | "grid" | "tree" | "dialog"

 

export type InputDropdownProps<T extends React.ElementType> =

PolymorphicProps<T,

StyledInputDropdownProps & {

    htmlFor?: string;

    ref?: PolymorphicRef<T>;

    placeholder?: string;

    label?: string;

    value?: string | number;

    options: string[];

    hideLabel?: boolean;

    hideHelperText?: boolean;

    titleText?: string;

    roleCombobox?: string;

    roleListbox?: listBoxRoles;

    onChange?: any;

    onFocus?: () => void;

    onBlur?: () => void;

    disabled?: boolean;

    isOpen?: boolean;

    dropdownId?: string | number;

    isDropdownPanelOpen?: any;

  }& Omit<StyledInputDropdownProps, "theme"> &

  Omit<React.ComponentPropsWithRef<T>, "className">>;

 

/**

 * Expected return type of InputDropdown

 * it includes the constructor and forward-ref

 * this ensure type-saftey being passed along

 */

export type InputDropdownComponent<

  E extends React.ElementType = AllowedElementType

> = {

  <T extends React.ElementType = E>(

    props: InputDropdownProps<T>

  ): React.ReactNode;

} & ForwardRefReturn<E, InputDropdownProps<E>>;

 

const InputDropdown = forwardRef(

    <T extends React.ElementType = AllowedElementType>(

      props: InputDropdownProps<T>,

      ref: PolymorphicRef<T>

    ) => {

     

      const {

        styledComponent: StyledComponent = StyledInputDropdown,

        label,

        helperText,

        variant,

        placeholder,

        value,

        options,

        hideLabel = false,

        hideHelperText,

        titleText,

        roleCombobox,

        roleListbox,

        onChange,

        onBlur,

        onFocus,

        disabled,

        isOpen = false,

        htmlFor,

        dropdownId,

        isDropdownPanelOpen = () => {},

        onOptionClick,

        ...rest

      } = safeRestProps(props);


 

      const[isPanelOpen, setIsPanelOpen] = useState(isOpen);

      const [mount, setMount] = useState(false);

      const[selectedValue, setSelectedValue] = useState<any>(value);

      const [ID] = useState(dropdownId ? dropdownId : generateId('input-dropdown'))

 

      //ref's declared

      const dropdownEl = useRef<any>(null);      

      const optionWrapperRef = useRef<any>(null);  

      const comboBoxRef = useRef<any>(null);

      const optionRef = useRef<any[]>([]);

 

      const comboBoxFocus = () => {

        setTimeout(() => {

          comboBoxRef.current?.focus();

        }, 100);

      }

 

      //default isOpen update

      useEffect(() => {

        if(isOpen === true){

          setIsPanelOpen(true)

        }else{

          setIsPanelOpen(false)

        }

      }, [isOpen])      

 

      //combobox keyboard accessible options

      const handleKeyDown = (event: any) => {

        switch (event.code) {

          case 'ArrowDown':

            setIsPanelOpen(true);

            comboBoxFocus();

            break;

          case 'Space':

            event.preventDefault();

            comboBoxRef.current.focus();

            // comboBoxFocus();

            break;    

          case 'NumpadEnter':

            event.preventDefault();

            comboBoxRef.current.focus();

            // comboBoxFocus();

            break;    

          case 'Enter':

            event.preventDefault();

            comboBoxRef.current.focus();

            // comboBoxFocus();

            break;  

          case 'Alt + ArrowDown':

            setIsPanelOpen(true);

            event.preventDefault();

            comboBoxFocus();

            break;

          case 'ArrowUp':

            event.preventDefault();

            setIsPanelOpen(false);

            comboBoxFocus();

            break;

          case 'Escape':

            if(isPanelOpen === true){

              setIsPanelOpen(false);

            }

            break;

          case 'Home':

            event.preventDefault();

            setIsPanelOpen(true);

            setTimeout(() => {

              (optionRef.current[0].parentElement as HTMLDivElement)?.focus();

            }, 100);

            break;

          case 'End':

            setIsPanelOpen(true);

            setTimeout(() => {

              (optionRef.current[options?.length - 1].parentElement as HTMLDivElement)?.focus();

            }, 100);

          break;

        default:

          break;

        }

      };  

 

      //Outside click handler

      const handleClickOutside = (event: MouseEvent) => {

        if (

          dropdownEl.current &&

          !dropdownEl.current.contains(event.target as Node)

        ) {

          setIsPanelOpen(false);

        }

      };

      useEffect(() => {    

        document.addEventListener('mousedown', handleClickOutside);

        return () => {

          document.removeEventListener('mousedown', handleClickOutside);

        };

      }, []);

   

      //handle toggle behaviour of the panel

      const handlePanelToggle = (newVal: boolean) => {

        setIsPanelOpen(newVal);

        isDropdownPanelOpen(newVal);

      }

   

      const onValueChange = (e:any) => {

        onChange && onChange(e)

        setSelectedValue(e);

      }

 

      useEffect(() => {

          if(mount)

            onValueChange(selectedValue);

          // setSelectedValue(selectedValue);

          // onChange && onChange(selectedValue)  

      }, [selectedValue])

     

      useEffect(() => {

        setMount(true);      

      }, [])

     

      //combobox wrapper logic

      const ButtonWrapper = () => {

        return (

          <>

            <StyledButtonWrapper onKeyDown={handleKeyDown} ref={comboBoxRef} aria-labelledby={label ? label : "dropdown-component"} aria-controls={dropdownId} aria-expanded={isPanelOpen} role={roleCombobox ? roleCombobox : "combobox"} tabIndex={0} aria-haspopup={roleListbox ? roleListbox : "listbox"} aria-label={label ? label : "Input Dropdown Component"} id={htmlFor ? htmlFor : "input-dropdown-component"} variant={variant} {...rest}>

                <InputField onChange={onValueChange} id={ID} icon={true} iconName={isPanelOpen ? "ChevronUp" : "ChevronDown"} disabled={disabled} title={titleText ? titleText : selectedValue} placeholder={placeholder} variant={variant ? variant : "neutral"} value={selectedValue} readOnly={true}/>

            </StyledButtonWrapper>

          </>

        )

      }

 

      //listbox wrapper

      const PanelWrapper = () => {

        return (

          <StyledPanelWrapper role={roleListbox ? roleListbox : "listbox"} onKeyDown={onOptionsKeyDown} ref={optionWrapperRef}>

              {options?.map((option: string, index: any) => (

                    <StyledInputDropdownOptionWrapper tabIndex={0} key={index}>

                      {option === selectedValue ?

                        <>                      

                          <StyledInputDropdownOptionIcon>

                            {<Icon iconName="Check"/>}

                          </StyledInputDropdownOptionIcon>

                        </>

                        :

                      ""}

                    <StyledInputDropdownOption ref={(el:any) => (optionRef.current[index] = el)} id={index} key={option} onClick={onOptionClicked(option)} role="option" data-option={option} aria-selected={selectedValue?.length > 0 ? "true" : "false"}>

                      {option}

                    </StyledInputDropdownOption>

                    </StyledInputDropdownOptionWrapper>

              ))}

          </StyledPanelWrapper>

        )

      }

 

      const onOptionClicked = (value:any) => () => {

        setSelectedValue(value);

        setIsPanelOpen(false);      

      };

 

      //options wrapper keyboard navigation

      const onOptionsKeyDown = (event: any) => {

        if(event.key === "Escape"){

          setIsPanelOpen(false);

          comboBoxFocus();

        }

        if(event.key === "Enter" || event.key === "NumpadEnter" || event.code === "Space"){

            event.preventDefault();

            const focusedOption = document.activeElement;

            const val = focusedOption?.childNodes[0].textContent;

            setSelectedValue(val);  

            setTimeout(() => {

              setIsPanelOpen(false);

            }, 100);

            comboBoxFocus();

        }

        if(event.key === "Home"){

          (optionRef.current[0].parentElement as HTMLDivElement)?.focus();

        }

        if(event.key === "End"){

          (optionRef.current[optionRef.current.length - 1].parentElement as HTMLDivElement)?.focus();

        }

        if(event.code === "ArrowDown" || event.code === "ArrowUp"){

          event.preventDefault();

          // debugger

 

          // Determine our current position

          let currentIndex = optionRef?.current.indexOf(event.target);

 

          // Do nothing if somehow we couldn't find this element.

          // This shouldn't be possible so warn us devs

          if (currentIndex === -1) {

            console.warn("Unexpected Arrow key on Options menu", event);

            return;

          }

 

          // Up means we're going down one index

          currentIndex += event.code === "ArrowUp" ? -1 : 1;

 

          // Focus the new element

          (optionRef?.current[currentIndex]?.parentElement as HTMLDivElement).focus();

        }

      }

   

      return (

        <StyledInputDropdown aria-controls={dropdownId} aria-expanded={isPanelOpen} ref={ref} disabled={disabled} onFocus={onFocus} onBlur={onBlur} {...rest}>

          <Label label={label ? label : ""} htmlFor={htmlFor ? htmlFor : ID} disabled={disabled} hidden={hideLabel} onClick={comboBoxFocus}/>

          <Disclosure ref={dropdownEl} open={isPanelOpen} buttonContent={<ButtonWrapper />} panelContent={<PanelWrapper />} disabled={disabled} isClickedValueChange={handlePanelToggle} variant={variant} {...rest}/>

          <HelperText content={hideHelperText ? "" : helperText} disabled={disabled} variant={variant} />

        </StyledInputDropdown>

      );

    }

  );

 

export default InputDropdown as InputDropdownComponent