ag-ui-kit
Version:
Custom select box component (single + multiple) - no dependencies
1 lines • 11.5 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.tsx"],"sourcesContent":["import React, {\r\n useState,\r\n useRef,\r\n useEffect,\r\n ReactNode,\r\n HTMLAttributes,\r\n ForwardedRef,\r\n KeyboardEvent,\r\n} from \"react\";\r\n\r\n// Props for the Select component\r\ninterface SelectProps\r\n extends Omit<HTMLAttributes<HTMLDivElement>, \"onChange\"> {\r\n className?: string;\r\n value?: string | string[];\r\n onChange?: (value: string | string[]) => void;\r\n multiple?: boolean;\r\n children: ReactNode;\r\n arrowOpen?: ReactNode;\r\n arrowClosed?: ReactNode;\r\n}\r\n\r\n// Props for the Option component\r\ninterface OptionProps extends HTMLAttributes<HTMLDivElement> {\r\n value: string;\r\n children: ReactNode;\r\n className?: string;\r\n disabled?: boolean;\r\n}\r\n\r\n// CSS styles\r\nconst STYLE_ID = \"cs-select-styles\";\r\nconst STYLES = `\r\n.cs-select { position: relative; width: 200px; font-family: system-ui, sans-serif; }\r\n.cs-control { border: 1px solid #ccc; padding: 8px 12px; border-radius: 4px; background: #fff; cursor: pointer; display:flex; justify-content:space-between; align-items:center; }\r\n.cs-arrow { margin-left: 8px; }\r\n.cs-dropdown { position: absolute; top: 100%; left: 0; right: 0; border: 1px solid #ccc; border-radius: 4px; margin-top: 4px; background: #fff; z-index: 1000; max-height: 200px; overflow-y: auto; }\r\n.cs-option { padding: 8px 12px; cursor: pointer; display:flex; align-items:center; gap:8px; border-bottom: 1px solid #eee; }\r\n.cs-option:last-child { border-bottom: none; }\r\n.cs-option--selected { background: #f0f0f0; }\r\n.cs-option input[type=\"checkbox\"] { pointer-events: none; }\r\n.cs-option--disabled { opacity: 0.5; cursor: not-allowed; }\r\n.cs-option--highlight { background: #e6f7ff; }\r\n`;\r\n\r\nfunction ensureStyles() {\r\n if (typeof document === \"undefined\") return;\r\n if (document.getElementById(STYLE_ID)) return;\r\n const style = document.createElement(\"style\");\r\n style.id = STYLE_ID;\r\n style.textContent = STYLES;\r\n document.head.appendChild(style);\r\n}\r\n\r\n// Option Component\r\nexport const Option: React.FC<OptionProps> = ({\r\n value,\r\n children,\r\n className,\r\n disabled,\r\n ...rest\r\n}) => (\r\n <div\r\n data-value={value}\r\n aria-disabled={disabled}\r\n className={`cs-option ${disabled ? \"cs-option--disabled\" : \"\"} ${className || \"\"}`}\r\n {...rest}\r\n >\r\n {children}\r\n </div>\r\n);\r\n\r\n// Select Component with keyboard navigation\r\nexport const Select = React.forwardRef<HTMLDivElement, SelectProps>(\r\n (\r\n { className, value, onChange, multiple, children, arrowOpen, arrowClosed, ...rest },\r\n ref: ForwardedRef<HTMLDivElement>\r\n ) => {\r\n const [open, setOpen] = useState(false);\r\n const [selected, setSelected] = useState<string[]>(\r\n Array.isArray(value) ? value : value ? [value] : []\r\n );\r\n const [highlightIndex, setHighlightIndex] = useState<number>(-1);\r\n\r\n const innerRef = useRef<HTMLDivElement>(null);\r\n const optionsRef = useRef<HTMLDivElement[]>([]);\r\n\r\n useEffect(() => {\r\n ensureStyles();\r\n }, []);\r\n\r\n useEffect(() => {\r\n if (value !== undefined) {\r\n setSelected(Array.isArray(value) ? value : [value]);\r\n }\r\n }, [value]);\r\n\r\n useEffect(() => {\r\n const handleClickOutside = (event: MouseEvent) => {\r\n if (innerRef.current && !innerRef.current.contains(event.target as Node)) {\r\n setOpen(false);\r\n setHighlightIndex(-1);\r\n }\r\n };\r\n document.addEventListener(\"mousedown\", handleClickOutside);\r\n return () => document.removeEventListener(\"mousedown\", handleClickOutside);\r\n }, []);\r\n\r\n const toggleOption = (val: string, disabled?: boolean) => {\r\n if (disabled) return;\r\n\r\n let newValues: string[];\r\n if (multiple) {\r\n if (selected.includes(val)) {\r\n newValues = selected.filter((v) => v !== val);\r\n } else {\r\n newValues = [...selected, val];\r\n }\r\n } else {\r\n newValues = [val];\r\n setOpen(false);\r\n setHighlightIndex(-1);\r\n }\r\n\r\n if (value === undefined) setSelected(newValues);\r\n onChange?.(multiple ? newValues : newValues[0]);\r\n };\r\n\r\n const renderOptions = () =>\r\n React.Children.map(children, (child: any, index) => {\r\n if (!child) return null;\r\n const { value, disabled, className, children, ...otherProps } = child.props;\r\n const isSelected = selected.includes(value);\r\n\r\n return (\r\n <div\r\n ref={(el) => (optionsRef.current[index] = el!)}\r\n onClick={() => toggleOption(value, disabled)}\r\n className={`cs-option ${\r\n isSelected ? \"cs-option--selected\" : \"\"\r\n } ${disabled ? \"cs-option--disabled\" : \"\"} ${\r\n index === highlightIndex ? \"cs-option--highlight\" : \"\"\r\n } ${className || \"\"}`}\r\n {...otherProps}\r\n >\r\n {multiple && <input type=\"checkbox\" checked={isSelected} readOnly />}\r\n {children}\r\n </div>\r\n );\r\n });\r\n\r\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\r\n const optionsCount = React.Children.count(children);\r\n if (!open && (e.key === \"ArrowDown\" || e.key === \"ArrowUp\")) {\r\n setOpen(true);\r\n setHighlightIndex(0);\r\n e.preventDefault();\r\n return;\r\n }\r\n if (!open) return;\r\n\r\n if (e.key === \"ArrowDown\") {\r\n let next = highlightIndex + 1;\r\n while (\r\n next < optionsCount &&\r\n (React.Children.toArray(children)[next] as any).props.disabled\r\n ) {\r\n next++;\r\n }\r\n if (next < optionsCount) setHighlightIndex(next);\r\n e.preventDefault();\r\n } else if (e.key === \"ArrowUp\") {\r\n let prev = highlightIndex - 1;\r\n while (\r\n prev >= 0 &&\r\n (React.Children.toArray(children)[prev] as any).props.disabled\r\n ) {\r\n prev--;\r\n }\r\n if (prev >= 0) setHighlightIndex(prev);\r\n e.preventDefault();\r\n } else if (e.key === \"Enter\" && highlightIndex >= 0) {\r\n const child = React.Children.toArray(children)[highlightIndex] as any;\r\n toggleOption(child.props.value, child.props.disabled);\r\n e.preventDefault();\r\n } else if (e.key === \"Escape\") {\r\n setOpen(false);\r\n setHighlightIndex(-1);\r\n e.preventDefault();\r\n }\r\n };\r\n\r\n return (\r\n <div\r\n ref={(node) => {\r\n innerRef.current = node;\r\n if (ref) {\r\n if (typeof ref === \"function\") ref(node);\r\n else ref.current = node;\r\n }\r\n }}\r\n className={`cs-select ${className || \"\"}`}\r\n onKeyDown={handleKeyDown}\r\n tabIndex={0}\r\n {...rest}\r\n >\r\n <div onClick={() => setOpen((o) => !o)} className=\"cs-control\">\r\n <span>\r\n {selected.length > 0 ? selected.join(\", \") : \"Select an option\"}\r\n </span>\r\n <span className=\"cs-arrow\">{open ? arrowOpen || \"▲\" : arrowClosed || \"▼\"}</span>\r\n </div>\r\n\r\n {open && <div className=\"cs-dropdown\">{renderOptions()}</div>}\r\n </div>\r\n );\r\n }\r\n);\r\n\r\nSelect.displayName = \"Select\";\r\n"],"mappings":"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","names":["React","useState","useRef","useEffect","jsx","jsxs","STYLE_ID","STYLES","ensureStyles","style","Option","value","children","className","disabled","rest","Select","onChange","multiple","arrowOpen","arrowClosed","ref","open","setOpen","selected","setSelected","highlightIndex","setHighlightIndex","innerRef","optionsRef","handleClickOutside","event","toggleOption","val","newValues","v","renderOptions","child","index","otherProps","isSelected","el","handleKeyDown","optionsCount","next","prev","node","o"]}