import React, { useCallback, useEffect, useId, useRef, useState } from "react"; export type CustomSelectOption = { value: string; label: string; }; export interface CustomSelectProps { ariaLabel?: string; value: string; options: ReadonlyArray; onChange: (value: string) => void; disabled?: boolean; placeholder?: string; className?: string; triggerClassName?: string; menuClassName?: string; } export function CustomSelect({ ariaLabel, value, options, onChange, disabled = false, placeholder = "Select…", className = "", triggerClassName = "", menuClassName = "", }: CustomSelectProps): React.ReactElement { const [isOpen, setIsOpen] = useState(false); const [focusIndex, setFocusIndex] = useState(-1); const containerRef = useRef(null); const listRef = useRef(null); const id = useId(); const selectedOption = options.find((option) => option.value === value); const close = useCallback(() => { setIsOpen(false); setFocusIndex(-1); }, []); useEffect(() => { if (!isOpen) { return undefined; } function handleClickOutside(event: MouseEvent) { if ( containerRef.current && !containerRef.current.contains(event.target as Node) ) { close(); } } document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [close, isOpen]); useEffect(() => { if (!isOpen || focusIndex < 0) { return; } const items = listRef.current?.querySelectorAll('[role="option"]'); items?.[focusIndex]?.scrollIntoView({ block: "nearest" }); }, [focusIndex, isOpen]); function openWithCurrentValue() { setIsOpen(true); setFocusIndex(options.findIndex((option) => option.value === value)); } function handleKeyDown(event: React.KeyboardEvent) { if (disabled) { return; } switch (event.key) { case "Enter": case " ": event.preventDefault(); if (isOpen && focusIndex >= 0) { onChange(options[focusIndex].value); close(); } else { openWithCurrentValue(); } break; case "ArrowDown": event.preventDefault(); if (!isOpen) { openWithCurrentValue(); } else { setFocusIndex((prev) => prev < options.length - 1 ? prev + 1 : prev, ); } break; case "ArrowUp": event.preventDefault(); if (!isOpen) { openWithCurrentValue(); } else { setFocusIndex((prev) => (prev > 0 ? prev - 1 : prev)); } break; case "Home": event.preventDefault(); if (isOpen && options.length > 0) { setFocusIndex(0); } break; case "End": event.preventDefault(); if (isOpen && options.length > 0) { setFocusIndex(options.length - 1); } break; case "Escape": event.preventDefault(); close(); break; case "Tab": close(); break; default: break; } } return (
{isOpen ? (
    = 0 ? `${id}-option-${focusIndex}` : undefined } className={`b3-wvs-custom-select-menu ${menuClassName}`.trim()} > {options.map((option, index) => { const isSelected = option.value === value; const isFocused = index === focusIndex; return (
  • setFocusIndex(index)} onMouseDown={(event) => { event.preventDefault(); onChange(option.value); close(); }} > {option.label} {isSelected ? ( ) : null}
  • ); })}
) : null}
); }