UNPKG

@kubit-ui-web/react-components

Version:

Kubit React Components is a customizable, accessible library of React web components, designed to enhance your application's user experience

271 lines 12 kB
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react'; import { useCustomHeightFromChildrens } from '../../../hooks/useCustomHeightFromChildrens/useCustomHeightFromChildren'; import { useInput } from '../../../hooks/useInput/useInput'; import { useMediaDevice } from '../../../hooks/useMediaDevice/useMediaDevice'; import { ROLES } from '../../../types/role/role'; import { isArrowDownPressed, isKeyEnterPressed, isKeyEscapePressed, isKeySpacePressed, } from '../../../utils/keyboard/keyboard.utility'; import { syntheticSelect } from '../../../utils/syntheticComponents/syntheticSelect/syntheticSelect'; import { useInternalValidations } from '../../input/hooks/useInternalValidations'; import { InternalErrorType } from '../../input/types/internalErrors'; // helpers import { filterOptions, findOptionByLabel, findOptionByValue } from '../helpers/filterOptions'; export const useInputDropdown = (props) => { // States const [openOptions, setOpenOptions] = useState(props.open); const [valueSearchSelected, setValueSearchSelected] = useState(props.value); // if the input loses focus and there is no valid option in the input value, show an error message const [labelSearchSelected, setLabelSearchSelected] = useState(props.value); // It is used to control that if an option is selected and the dropdown is opened, all the options are shown again const [showAllOptions, setShowAllOptions] = useState(true); const [searchText, setSearchText] = useState(findOptionByValue(props.value, props.optionList.options)?.label ?? ''); const [inputPopoverText, setInputPopoverText] = useState(findOptionByValue(props.value, props.optionList.options)?.label ?? ''); // ghost prototype const { getRef, clickOption } = useMemo(() => { if (typeof window !== 'undefined' && typeof document !== 'undefined') { return syntheticSelect(props.optionList.options, props.value, props.name); } return { getRef: () => null, clickOption: () => null }; }, [props.optionList.options]); useImperativeHandle(props.ref, () => { return getRef(); }, []); // Variables const device = useMediaDevice(); const { internalErrors, addInternalError, removeInternalError } = useInternalValidations(props.type, undefined, props.onInternalErrors); const handleInputBlur = event => { props.onBlur?.(event); // if the input loses focus and there is no valid option in the input value, show an error message if (!labelSearchSelected) { addInternalError(InternalErrorType.INVALID_OPTION); } }; // Input Basic hook const { state, inputRef, handleFocusInternal, handlePasteInternal } = useInput({ internalErrorExecution: props.internalErrorExecution, disabled: props.disabled, error: props.error || internalErrors.length > 0, // need for update the state currentValue: searchText, informationAssociated: props.informationAssociated, disabledCopyAndPaste: props.disabledCopyAndPaste, onFocus: props.onFocus, onBlur: handleInputBlur, onInternalErrors: props.onInternalErrors, onPaste: props.onPaste, }); const useActionBottomSheet = useMemo(() => props.styles?.[state]?.useActionBottomSheet?.[device], [state, device]); const { optionsListRefCollection, optionsListCollectionRef, height: listOptionsHeight, } = useCustomHeightFromChildrens({ limit: props.elementsToShow, observer: [props.optionList.options, searchText, inputPopoverText], shouldCalculateHeight: !useActionBottomSheet, }); const showAllOptionsTimeout = useRef(null); // Delete showAllOptionsTimeout when the component is unmounted useEffect(() => { return () => { if (showAllOptionsTimeout.current) { clearTimeout(showAllOptionsTimeout.current); } }; }, []); const handleChangeShowAllOptions = ({ value, timeout = 300, }) => { if (showAllOptionsTimeout.current) { clearTimeout(showAllOptionsTimeout.current); } showAllOptionsTimeout.current = setTimeout(() => { setShowAllOptions(value); }, timeout); }; // Auxiliar method // Set the value suggested and call onChange const commonChangeActions = (valueSearchSelected, valueSuggest) => { setValueSearchSelected(valueSearchSelected); if (valueSuggest?.label) { setLabelSearchSelected(valueSuggest.label); handleChangeShowAllOptions({ value: true }); } const event = clickOption(valueSearchSelected); props.onChange?.(event); }; // Methods const handleValueSelected = (value) => { const optionFound = findOptionByValue(value, props.optionList.options); const searchText = optionFound?.customLabel ?? optionFound?.label ?? value; removeInternalError(InternalErrorType.INVALID_OPTION); setSearchText(searchText); setInputPopoverText(searchText); commonChangeActions(value, optionFound ?? { label: value, value }); }; const handleOpenOptions = (open) => { setOpenOptions(open); props.onOpenCloseOptions?.(open); // If closing the options, focus the input if (!open) { inputRef?.current?.focus(); } }; // Input dropdown handlers const handleChangeInputDropdown = event => { handleOpenOptions(true); const searchText = event.target.value; const optionFound = findOptionByLabel(searchText, props.optionList.options); setSearchText(searchText); setInputPopoverText(searchText); removeInternalError(InternalErrorType.INVALID_OPTION); if (labelSearchSelected) { setLabelSearchSelected(''); } handleChangeShowAllOptions({ value: false, timeout: 0 }); if (!optionFound && valueSearchSelected !== undefined) { commonChangeActions(undefined, null); } }; const handleClickInputDropdown = event => { handleOpenOptions(!openOptions); props.onClick?.(event); }; const handleClickIconInputDropdown = event => { handleOpenOptions(!openOptions); props.onIconClick?.(event); props.onRightIconClick?.(event); }; const handleInputKeyDown = event => { if (isKeyEnterPressed(event.key) || (!(props.allowSearch ?? props.styles[state]?.allowSearch) && isKeySpacePressed(event.key))) { handleOpenOptions(!openOptions); props.onClick?.(event); } if (isKeyEscapePressed(event.key)) { handleOpenOptions(false); } // Focus first element of the list if (isArrowDownPressed(event.key)) { optionsListCollectionRef?.current?.[0]?.firstElementChild?.focus(); event.preventDefault(); } }; // Input popover handlers const handleInputPopoverChange = event => { setInputPopoverText(event.target.value); if (labelSearchSelected) { setLabelSearchSelected(''); } handleChangeShowAllOptions({ value: false, timeout: 0 }); }; const handleInputPopoverIconClick = () => { props.onInputPopoverIconClick?.(); if (props.clearTextInputPopoverIconClick) { setInputPopoverText(''); } }; // Uses effects // Update value when value or optionList options changes useEffect(() => { setValueSearchSelected(props.value); const optionFound = findOptionByValue(props.value, props.optionList.options); const searchText = optionFound?.customLabel ?? optionFound?.label ?? ''; handleChangeShowAllOptions({ value: true }); setLabelSearchSelected(searchText); setSearchText(searchText); setInputPopoverText(searchText); }, [props.value, props.optionList.options]); /** * Action bottom sheet keydown handler, when arrow down is pressed over a non option element, it should focus the first option list element */ const handleActionBottomSheetKeydown = useCallback((event) => { // if target is option do nothing, this will handle internally by the OPTIONS if (event.target instanceof Element && event.target.getAttribute('role') === ROLES.OPTION) { return; } // Focus first element of the list if (isArrowDownPressed(event.key)) { optionsListCollectionRef?.current?.[0]?.firstElementChild?.focus(); event.preventDefault(); } }, []); const actionBottomSheetRef = useRef(null); const actionBottomSheetRefCb = useCallback(node => { // Add event listeners to the action bottom sheet when it is mounted if (!node) { actionBottomSheetRef.current?.removeEventListener('keydown', handleActionBottomSheetKeydown); } actionBottomSheetRef.current = node; if (actionBottomSheetRef.current) { actionBottomSheetRef.current.addEventListener('keydown', handleActionBottomSheetKeydown); } // Focus in the input popover if exists const inputInPopover = node?.querySelector('input'); if (inputInPopover) { return inputInPopover.focus(); } // Focus in the first enable option const firstEnabledOption = node?.querySelector('[role="option"]:not([aria-disabled="true"])'); firstEnabledOption?.focus(); }, [handleActionBottomSheetKeydown]); // iconButton skip to list handler const iconRef = useRef(); const handleIconButtonPress = event => { event.stopPropagation(); if (isKeyEnterPressed(event.key) || isKeySpacePressed(event.key)) { return; } if (isKeyEscapePressed(event.key)) { handleOpenOptions(false); } // Focus first element of the list if (isArrowDownPressed(event.key)) { optionsListCollectionRef?.current?.[0]?.firstElementChild?.focus(); event.preventDefault(); } }; useEffect(() => { const iconButton = iconRef.current?.children[0]; if (!iconButton || iconButton.tagName !== 'BUTTON') { return; } iconButton.addEventListener('keydown', handleIconButtonPress); return () => { iconButton?.removeEventListener('keydown', handleIconButtonPress); }; }, []); // references const ref = { refInput: inputRef, refList: optionsListRefCollection, refActionBottomSheet: actionBottomSheetRefCb, refIcon: iconRef, }; // Filter options attending to // 1. Allow search && !showAllOptions due to there is already one selected // 2. If useActionBottomSheet && hasInputInSearchList -> popover input, else normal input let optionsFiltered = { ...props.optionList }; if ((props.allowSearch ?? props.styles[state]?.allowSearch) && !showAllOptions) { const filter = useActionBottomSheet && props.hasInputInSearchList ? inputPopoverText : searchText; optionsFiltered = { ...props.optionList, options: filterOptions(filter, props.optionList.options), }; } return { openOptions, valueSearchSelected, searchText, inputPopoverText, optionsFiltered, handleOpenOptions, handleClickInputDropdown, handleClickIconInputDropdown, handleInputPopoverIconClick, handleValueSelected, handleChangeInputDropdown, handleInputKeyDown, handleInputBlur, state, ref, listOptionsHeight, handleFocusInternal, handleInputPopoverChange, handlePasteInternal, }; }; //# sourceMappingURL=useInputDropdown.js.map