import { useReducer, useRef, useEffect, useMemo } from 'react'; import { Option, SelectAction, SelectProps, SelectState } from './types'; import { checkEmpty } from '../../utils/checkEmpty/checkEmpty'; import { createPortal } from 'react-dom'; import classNames from 'classnames'; import Dropdown from './components/Dropdown/Dropdown'; import Icon from '../Icon'; import './Select.scss'; import usePortalPosition from '../../hooks/usePortalPosition'; import Typography from '../Typography'; const selectReducer = ( state: SelectState, action: SelectAction ): SelectState => { switch (action.type) { case 'FOCUS_INPUT': return { ...state, isInputFocused: true, iconColor: 'var(--ff-select-brand-color)', isIconClick: true, showOptions: true, }; case 'BLUR_INPUT': return { ...state, isInputFocused: false, options: action.payload.optionsList, option: action.payload.option, // todo color need to be add in style guide iconColor: 'var(--ff-select-default-color)', isIconClick: false, showOptions: false, dropdownPosition: { positionX: 0, positionY: 0, width: 0, fromBottom: 0, }, }; case 'MOUSE_ENTER': return state.isInputFocused ? state : { ...state, iconColor: 'var(--ff-select-text-hover-color)' }; case 'MOUSE_LEAVE': return state.isInputFocused ? state : // todo color need to be add in style guide { ...state, iconColor: 'var(--ff-select-default-color)', isIconClick: false, }; case 'UPDATE_DROPDOWN_POSITION': const { positionX, positionY, width, fromBottom } = action.payload || {}; return { ...state, dropdownPosition: { positionX, positionY, width, fromBottom, }, }; case 'UPDATE_OPTION': return { ...state, option: action.payload, }; case 'UPDATE_OPTION_LIST': return { ...state, options: action.payload, }; case 'SHOW_ERROR': return { ...state, isInputFocused: true, isIconClick: false, showOptions: false, options: action.payload.optionsList, option: action.payload.option, // todo color need to be add in style guide iconColor: 'var(--ff-select-default-color)', dropdownPosition: { positionX: 0, positionY: 0, width: 0, fromBottom: 0, }, }; default: return state; } }; const Select = ({ label = '', showLabel = true, optionsList = [], selectedOption = { label: '', value: '' }, onChange = () => {}, errorMsg = '', className = '', optionZIndex = 100, disabled = false, borderRadius = true, showBorder = true, required = false, }: SelectProps) => { const initialState: SelectState = useMemo( () => ({ isInputFocused: false, // todo color need to be added in style guide iconColor: 'var(--ff-select-default-color)', isIconClick: false, showOptions: false, options: optionsList, option: checkEmpty(selectedOption) ? '' : selectedOption?.value, dropdownPosition: { positionX: 0, positionY: 0, width: 0, fromBottom: 0 }, }), [optionsList, selectedOption] ); const [selectControlState, dispatch] = useReducer( selectReducer, initialState ); const DropDownRef = useRef(null); const InputRef = useRef(null); const { isInputFocused, iconColor, isIconClick, showOptions, dropdownPosition, options, option, } = selectControlState; const calculatePosition = usePortalPosition(DropDownRef, showOptions); const handleSelectAction = ( actionType: | 'FOCUS_INPUT' | 'MOUSE_ENTER' | 'MOUSE_LEAVE' | 'SHOW_ERROR' | 'BLUR_INPUT' ) => { if (!disabled) { if (actionType === 'SHOW_ERROR' || actionType === 'BLUR_INPUT') { dispatch({ type: actionType, payload: { optionsList, option: selectedOption.value }, }); } else { dispatch({ type: actionType }); } } }; const onSelectInputChange = (e: React.ChangeEvent) => { if (disabled) return; const { value } = e.target; const filteredOptions = optionsList.filter((option) => { return typeof option.value === 'string' ? option.value.toLowerCase().includes(value.toLowerCase().trim()) : option.value === Number(value); }); dispatch({ type: 'UPDATE_OPTION_LIST', payload: filteredOptions }); dispatch({ type: 'UPDATE_OPTION', payload: value }); }; const onSelectBlur = () => { if (disabled) return; if (errorMsg) { handleSelectAction('SHOW_ERROR'); } else { handleSelectAction('BLUR_INPUT'); } }; const onSelectOptionSelector = (option: Option) => { if (!disabled) { onSelectBlur(); dispatch({ type: 'UPDATE_OPTION', payload: option.value }); if (onChange) { onChange(option); } } }; const onSelectUpdatePosition = () => { if (!showOptions || !DropDownRef?.current || disabled) return; const { positionX, positionY, width, fromBottom } = calculatePosition(DropDownRef); dispatch({ type: 'UPDATE_DROPDOWN_POSITION', payload: { positionX, positionY, width, fromBottom }, }); }; const onSelectToggleScroll = (isEnabled: boolean) => { const bodyScrollWidth = window.innerWidth - document.body.clientWidth; document.body.style.paddingRight = isEnabled ? '' : `${bodyScrollWidth}px`; document.body.style.overflow = isEnabled ? '' : 'hidden'; }; useEffect(() => { if (disabled) return; if (showOptions) { onSelectToggleScroll(!showOptions); } onSelectUpdatePosition(); const handleResizeOrScroll = () => onSelectUpdatePosition(); window.addEventListener('resize', handleResizeOrScroll); window.addEventListener('scroll', handleResizeOrScroll); return () => { onSelectToggleScroll(true); window.removeEventListener('resize', handleResizeOrScroll); window.removeEventListener('scroll', handleResizeOrScroll); }; }, [showOptions]); useEffect(() => { if (errorMsg) { handleSelectAction('SHOW_ERROR'); } }, []); const applyActiveOptionClasses = !isInputFocused && Boolean(option); return (
handleSelectAction('FOCUS_INPUT')} onMouseEnter={() => handleSelectAction('MOUSE_ENTER')} onMouseLeave={() => handleSelectAction('MOUSE_LEAVE')} onChange={onSelectInputChange} value={option} disabled={disabled} autoComplete="off" spellCheck="false" ref={InputRef} /> {showLabel && (
{required && ( * )} {label}
)}
{showLabel && ( {required && ( * )} {label} )}
{errorMsg && ( {errorMsg} )}
{showOptions && createPortal( , document.body )}
); }; export default Select;