dlx
<p>import { forwardRef, useEffect, useRef, useState } from "react"</p>
<p> </p>
<p>import type { ForwardRefReturn, PolymorphicProps, PolymorphicRef } from '../system'</p>
<p>import StyledInputDropdown, { StyledButtonWrapper, StyledPanelWrapper, type StyledInputDropdownProps, StyledInputDropdownOption, StyledInputDropdownOptionWrapper, StyledInputDropdownOptionIcon } from "./InputDropdown.styled"</p>
<p>import { AllowedElementType } from "./types"</p>
<p>import { generateId, safeRestProps } from "../utils";</p>
<p>import { Disclosure } from "../Disclosure";</p>
<p>import { Icon } from "../Icon";</p>
<p>import { Label } from "../Label";</p>
<p>import { InputField } from "../InputField"</p>
<p>import { HelperText } from "../HelperText";</p>
<p> </p>
<p>export type listBoxRoles = "listbox" | "grid" | "tree" | "dialog"</p>
<p> </p>
<p>export type InputDropdownProps<T extends React.ElementType> =</p>
<p>PolymorphicProps<T,</p>
<p>StyledInputDropdownProps & {</p>
<p> htmlFor?: string;</p>
<p> ref?: PolymorphicRef<T>;</p>
<p> placeholder?: string;</p>
<p> label?: string;</p>
<p> value?: string | number;</p>
<p> options: string[];</p>
<p> hideLabel?: boolean;</p>
<p> hideHelperText?: boolean;</p>
<p> titleText?: string;</p>
<p> roleCombobox?: string;</p>
<p> roleListbox?: listBoxRoles;</p>
<p> onChange?: any;</p>
<p> onFocus?: () => void;</p>
<p> onBlur?: () => void;</p>
<p> disabled?: boolean;</p>
<p> isOpen?: boolean;</p>
<p> dropdownId?: string | number;</p>
<p> isDropdownPanelOpen?: any;</p>
<p> }& Omit<StyledInputDropdownProps, "theme"> &</p>
<p> Omit<React.ComponentPropsWithRef<T>, "className">>;</p>
<p> </p>
<p>/**</p>
<p> * Expected return type of InputDropdown</p>
<p> * it includes the constructor and forward-ref</p>
<p> * this ensure type-saftey being passed along</p>
<p> */</p>
<p>export type InputDropdownComponent<</p>
<p> E extends React.ElementType = AllowedElementType</p>
<p>> = {</p>
<p> <T extends React.ElementType = E>(</p>
<p> props: InputDropdownProps<T></p>
<p> ): React.ReactNode;</p>
<p>} & ForwardRefReturn<E, InputDropdownProps<E>>;</p>
<p> </p>
<p>const InputDropdown = forwardRef(</p>
<p> <T extends React.ElementType = AllowedElementType>(</p>
<p> props: InputDropdownProps<T>,</p>
<p> ref: PolymorphicRef<T></p>
<p> ) => {</p>
<p> </p>
<p> const {</p>
<p> styledComponent: StyledComponent = StyledInputDropdown,</p>
<p> label,</p>
<p> helperText,</p>
<p> variant,</p>
<p> placeholder,</p>
<p> value,</p>
<p> options,</p>
<p> hideLabel = false,</p>
<p> hideHelperText,</p>
<p> titleText,</p>
<p> roleCombobox,</p>
<p> roleListbox,</p>
<p> onChange,</p>
<p> onBlur,</p>
<p> onFocus,</p>
<p> disabled,</p>
<p> isOpen = false,</p>
<p> htmlFor,</p>
<p> dropdownId,</p>
<p> isDropdownPanelOpen = () => {},</p>
<p> onOptionClick,</p>
<p> ...rest</p>
<p> } = safeRestProps(props);</p>
<p><br />
</p>
<p> const[isPanelOpen, setIsPanelOpen] = useState(isOpen);</p>
<p> const [mount, setMount] = useState(false);</p>
<p> const[selectedValue, setSelectedValue] = useState<any>(value);</p>
<p> const [ID] = useState(dropdownId ? dropdownId : generateId('input-dropdown'))</p>
<p> </p>
<p> //ref's declared</p>
<p> const dropdownEl = useRef<any>(null); </p>
<p> const optionWrapperRef = useRef<any>(null); </p>
<p> const comboBoxRef = useRef<any>(null);</p>
<p> const optionRef = useRef<any[]>([]);</p>
<p> </p>
<p> const comboBoxFocus = () => {</p>
<p> setTimeout(() => {</p>
<p> comboBoxRef.current?.focus();</p>
<p> }, 100);</p>
<p> }</p>
<p> </p>
<p> //default isOpen update</p>
<p> useEffect(() => {</p>
<p> if(isOpen === true){</p>
<p> setIsPanelOpen(true)</p>
<p> }else{</p>
<p> setIsPanelOpen(false)</p>
<p> }</p>
<p> }, [isOpen]) </p>
<p> </p>
<p> //combobox keyboard accessible options</p>
<p> const handleKeyDown = (event: any) => {</p>
<p> switch (event.code) {</p>
<p> case 'ArrowDown':</p>
<p> setIsPanelOpen(true);</p>
<p> comboBoxFocus();</p>
<p> break;</p>
<p> case 'Space':</p>
<p> event.preventDefault();</p>
<p> comboBoxRef.current.focus();</p>
<p> // comboBoxFocus();</p>
<p> break; </p>
<p> case 'NumpadEnter':</p>
<p> event.preventDefault();</p>
<p> comboBoxRef.current.focus();</p>
<p> // comboBoxFocus();</p>
<p> break; </p>
<p> case 'Enter':</p>
<p> event.preventDefault();</p>
<p> comboBoxRef.current.focus();</p>
<p> // comboBoxFocus();</p>
<p> break; </p>
<p> case 'Alt + ArrowDown':</p>
<p> setIsPanelOpen(true);</p>
<p> event.preventDefault();</p>
<p> comboBoxFocus();</p>
<p> break;</p>
<p> case 'ArrowUp':</p>
<p> event.preventDefault();</p>
<p> setIsPanelOpen(false);</p>
<p> comboBoxFocus();</p>
<p> break;</p>
<p> case 'Escape':</p>
<p> if(isPanelOpen === true){</p>
<p> setIsPanelOpen(false);</p>
<p> }</p>
<p> break;</p>
<p> case 'Home':</p>
<p> event.preventDefault();</p>
<p> setIsPanelOpen(true);</p>
<p> setTimeout(() => {</p>
<p> (optionRef.current[0].parentElement as HTMLDivElement)?.focus();</p>
<p> }, 100);</p>
<p> break;</p>
<p> case 'End':</p>
<p> setIsPanelOpen(true);</p>
<p> setTimeout(() => {</p>
<p> (optionRef.current[options?.length - 1].parentElement as HTMLDivElement)?.focus();</p>
<p> }, 100);</p>
<p> break;</p>
<p> default:</p>
<p> break;</p>
<p> }</p>
<p> }; </p>
<p> </p>
<p> //Outside click handler</p>
<p> const handleClickOutside = (event: MouseEvent) => {</p>
<p> if (</p>
<p> dropdownEl.current &&</p>
<p> !dropdownEl.current.contains(event.target as Node)</p>
<p> ) {</p>
<p> setIsPanelOpen(false);</p>
<p> }</p>
<p> };</p>
<p> useEffect(() => { </p>
<p> document.addEventListener('mousedown', handleClickOutside);</p>
<p> return () => {</p>
<p> document.removeEventListener('mousedown', handleClickOutside);</p>
<p> };</p>
<p> }, []);</p>
<p> </p>
<p> //handle toggle behaviour of the panel</p>
<p> const handlePanelToggle = (newVal: boolean) => {</p>
<p> setIsPanelOpen(newVal);</p>
<p> isDropdownPanelOpen(newVal);</p>
<p> }</p>
<p> </p>
<p> const onValueChange = (e:any) => {</p>
<p> onChange && onChange(e)</p>
<p> setSelectedValue(e);</p>
<p> }</p>
<p> </p>
<p> useEffect(() => {</p>
<p> if(mount)</p>
<p> onValueChange(selectedValue);</p>
<p> // setSelectedValue(selectedValue);</p>
<p> // onChange && onChange(selectedValue) </p>
<p> }, [selectedValue])</p>
<p> </p>
<p> useEffect(() => {</p>
<p> setMount(true); </p>
<p> }, [])</p>
<p> </p>
<p> //combobox wrapper logic</p>
<p> const ButtonWrapper = () => {</p>
<p> return (</p>
<p> <></p>
<p> <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}></p>
<p> <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}/></p>
<p> </StyledButtonWrapper></p>
<p> </></p>
<p> )</p>
<p> }</p>
<p> </p>
<p> //listbox wrapper</p>
<p> const PanelWrapper = () => {</p>
<p> return (</p>
<p> <StyledPanelWrapper role={roleListbox ? roleListbox : "listbox"} onKeyDown={onOptionsKeyDown} ref={optionWrapperRef}></p>
<p> {options?.map((option: string, index: any) => (</p>
<p> <StyledInputDropdownOptionWrapper tabIndex={0} key={index}></p>
<p> {option === selectedValue ?</p>
<p> <> </p>
<p> <StyledInputDropdownOptionIcon></p>
<p> {<Icon iconName="Check"/>}</p>
<p> </StyledInputDropdownOptionIcon></p>
<p> </></p>
<p> :</p>
<p> ""}</p>
<p> <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"}></p>
<p> {option}</p>
<p> </StyledInputDropdownOption></p>
<p> </StyledInputDropdownOptionWrapper></p>
<p> ))}</p>
<p> </StyledPanelWrapper></p>
<p> )</p>
<p> }</p>
<p> </p>
<p> const onOptionClicked = (value:any) => () => {</p>
<p> setSelectedValue(value);</p>
<p> setIsPanelOpen(false); </p>
<p> };</p>
<p> </p>
<p> //options wrapper keyboard navigation</p>
<p> const onOptionsKeyDown = (event: any) => {</p>
<p> if(event.key === "Escape"){</p>
<p> setIsPanelOpen(false);</p>
<p> comboBoxFocus();</p>
<p> }</p>
<p> if(event.key === "Enter" || event.key === "NumpadEnter" || event.code === "Space"){</p>
<p> event.preventDefault();</p>
<p> const focusedOption = document.activeElement;</p>
<p> const val = focusedOption?.childNodes[0].textContent;</p>
<p> setSelectedValue(val); </p>
<p> setTimeout(() => {</p>
<p> setIsPanelOpen(false);</p>
<p> }, 100);</p>
<p> comboBoxFocus();</p>
<p> }</p>
<p> if(event.key === "Home"){</p>
<p> (optionRef.current[0].parentElement as HTMLDivElement)?.focus();</p>
<p> }</p>
<p> if(event.key === "End"){</p>
<p> (optionRef.current[optionRef.current.length - 1].parentElement as HTMLDivElement)?.focus();</p>
<p> }</p>
<p> if(event.code === "ArrowDown" || event.code === "ArrowUp"){</p>
<p> event.preventDefault();</p>
<p> // debugger</p>
<p> </p>
<p> // Determine our current position</p>
<p> let currentIndex = optionRef?.current.indexOf(event.target);</p>
<p> </p>
<p> // Do nothing if somehow we couldn't find this element.</p>
<p> // This shouldn't be possible so warn us devs</p>
<p> if (currentIndex === -1) {</p>
<p> console.warn("Unexpected Arrow key on Options menu", event);</p>
<p> return;</p>
<p> }</p>
<p> </p>
<p> // Up means we're going down one index</p>
<p> currentIndex += event.code === "ArrowUp" ? -1 : 1;</p>
<p> </p>
<p> // Focus the new element</p>
<p> (optionRef?.current[currentIndex]?.parentElement as HTMLDivElement).focus();</p>
<p> }</p>
<p> }</p>
<p> </p>
<p> return (</p>
<p> <StyledInputDropdown aria-controls={dropdownId} aria-expanded={isPanelOpen} ref={ref} disabled={disabled} onFocus={onFocus} onBlur={onBlur} {...rest}></p>
<p> <Label label={label ? label : ""} htmlFor={htmlFor ? htmlFor : ID} disabled={disabled} hidden={hideLabel} onClick={comboBoxFocus}/></p>
<p> <Disclosure ref={dropdownEl} open={isPanelOpen} buttonContent={<ButtonWrapper />} panelContent={<PanelWrapper />} disabled={disabled} isClickedValueChange={handlePanelToggle} variant={variant} {...rest}/></p>
<p> <HelperText content={hideHelperText ? "" : helperText} disabled={disabled} variant={variant} /></p>
<p> </StyledInputDropdown></p>
<p> );</p>
<p> }</p>
<p> );</p>
<p> </p>
<p>export default InputDropdown as InputDropdownComponent</p>
<p> </p>