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