UNPKG

@mantine/core

Version:

React components library focused on usability, accessibility and developer experience

1 lines 15.3 kB
{"version":3,"file":"use-combobox.mjs","names":[],"sources":["../../../../src/components/Combobox/use-combobox/use-combobox.ts"],"sourcesContent":["import { useCallback, useEffect, useRef } from 'react';\nimport { useUncontrolled } from '@mantine/hooks';\nimport { findElementBySelector, findElementsBySelector, getRootElement } from '../../../core/utils';\nimport { getFirstIndex, getNextIndex, getPreviousIndex } from './get-index/get-index';\n\nexport type ComboboxDropdownEventSource = 'keyboard' | 'mouse' | 'unknown';\n\nexport interface ComboboxStore {\n /** Current dropdown opened state */\n dropdownOpened: boolean;\n\n /** Opens dropdown */\n openDropdown: (eventSource?: ComboboxDropdownEventSource) => void;\n\n /** Closes dropdown */\n closeDropdown: (eventSource?: ComboboxDropdownEventSource) => void;\n\n /** Toggles dropdown opened state */\n toggleDropdown: (eventSource?: ComboboxDropdownEventSource) => void;\n\n /** Selected option index ref */\n selectedOptionIndex: number;\n\n /** Returns currently selected option index or `-1` if none of the options is selected */\n getSelectedOptionIndex: () => number;\n\n /** Selects `Combobox.Option` by index */\n selectOption: (index: number) => void;\n\n /** Selects first `Combobox.Option` with `active` prop.\n * If there are no such options, the function does nothing.\n */\n selectActiveOption: () => string | null;\n\n /** Selects first `Combobox.Option` that is not disabled.\n * If there are no such options, the function does nothing.\n * */\n selectFirstOption: () => string | null;\n\n /** Selects next `Combobox.Option` that is not disabled.\n * If the current option is the last one, the function selects first option, if `loop` is true.\n */\n selectNextOption: () => string | null;\n\n /** Selects previous `Combobox.Option` that is not disabled.\n * If the current option is the first one, the function selects last option, if `loop` is true.\n * */\n selectPreviousOption: () => string | null;\n\n /** Resets selected option index to -1, removes `data-combobox-selected` from selected option */\n resetSelectedOption: () => void;\n\n /** Triggers `onClick` event of selected option.\n * If there is no selected option, the function does nothing.\n */\n clickSelectedOption: () => void;\n\n /** Updates selected option index to currently selected or active option.\n * The function is required to be used with searchable components to update selected option index\n * when options list changes based on search query.\n */\n updateSelectedOptionIndex: (\n target?: 'active' | 'selected' | number,\n options?: { scrollIntoView?: boolean }\n ) => void;\n\n /** List id, used for `aria-*` attributes */\n listId: string | null;\n\n /** Sets list id */\n setListId: (id: string) => void;\n\n /** Ref of `Combobox.Search` input */\n searchRef: React.RefObject<HTMLInputElement | null>;\n\n /** Moves focus to `Combobox.Search` input */\n focusSearchInput: () => void;\n\n /** Ref of the target element */\n targetRef: React.RefObject<HTMLElement | null>;\n\n /** Moves focus to the target element */\n focusTarget: () => void;\n}\n\nexport interface UseComboboxOptions {\n /** Default value for `dropdownOpened`, `false` by default. Used when the component is uncontrolled */\n defaultOpened?: boolean;\n\n /** Controlled `dropdownOpened` state. When set, the dropdown opened state is controlled by the parent component */\n opened?: boolean;\n\n /** Called when `dropdownOpened` state changes. Required for controlled mode */\n onOpenedChange?: (opened: boolean) => void;\n\n /** Called when dropdown closes with event source: keyboard, mouse or unknown. Useful for analytics or side effects on dropdown closure */\n onDropdownClose?: (eventSource: ComboboxDropdownEventSource) => void;\n\n /** Called when dropdown opens with event source: keyboard, mouse or unknown. Useful for analytics or side effects on dropdown opening */\n onDropdownOpen?: (eventSource: ComboboxDropdownEventSource) => void;\n\n /** Determines whether arrow key presses should loop through items (first to last and last to first). Defaults to `true` */\n loop?: boolean;\n\n /** `behavior` passed down to `element.scrollIntoView`. Controls the scrolling animation when options are scrolled into view. Defaults to `'instant'` */\n scrollBehavior?: ScrollBehavior;\n}\n\nexport function useCombobox({\n defaultOpened,\n opened,\n onOpenedChange,\n onDropdownClose,\n onDropdownOpen,\n loop = true,\n scrollBehavior = 'instant',\n}: UseComboboxOptions = {}): ComboboxStore {\n const [dropdownOpened, setDropdownOpened] = useUncontrolled({\n value: opened,\n defaultValue: defaultOpened,\n finalValue: false,\n onChange: onOpenedChange,\n });\n\n const listId = useRef<string | null>(null);\n const selectedOptionIndex = useRef<number>(-1);\n const searchRef = useRef<HTMLInputElement | null>(null);\n const targetRef = useRef<HTMLElement | null>(null);\n const focusSearchTimeout = useRef<number>(-1);\n const focusTargetTimeout = useRef<number>(-1);\n const selectedIndexUpdateTimeout = useRef<number>(-1);\n\n const openDropdown: ComboboxStore['openDropdown'] = useCallback(\n (eventSource = 'unknown') => {\n if (!dropdownOpened) {\n setDropdownOpened(true);\n onDropdownOpen?.(eventSource);\n }\n },\n [setDropdownOpened, onDropdownOpen, dropdownOpened]\n );\n\n const closeDropdown: ComboboxStore['closeDropdown'] = useCallback(\n (eventSource = 'unknown') => {\n if (dropdownOpened) {\n setDropdownOpened(false);\n onDropdownClose?.(eventSource);\n }\n },\n [setDropdownOpened, onDropdownClose, dropdownOpened]\n );\n\n const toggleDropdown: ComboboxStore['toggleDropdown'] = useCallback(\n (eventSource = 'unknown') => {\n if (dropdownOpened) {\n closeDropdown(eventSource);\n } else {\n openDropdown(eventSource);\n }\n },\n [closeDropdown, openDropdown, dropdownOpened]\n );\n\n const clearSelectedItem = useCallback(() => {\n const root = getRootElement(targetRef.current);\n const selected = findElementBySelector(`#${listId.current} [data-combobox-selected]`, root);\n selected?.removeAttribute('data-combobox-selected');\n selected?.removeAttribute('aria-selected');\n }, []);\n\n const selectOption = useCallback(\n (index: number) => {\n const root = getRootElement(targetRef.current);\n const list = findElementBySelector(`#${listId.current!}`, root);\n const items = list\n ? findElementsBySelector<HTMLDivElement>('[data-combobox-option]', list)\n : null;\n\n if (!items) {\n return null;\n }\n\n const nextIndex = index >= items!.length ? 0 : index < 0 ? items!.length - 1 : index;\n selectedOptionIndex.current = nextIndex;\n\n if (items?.[nextIndex] && !items[nextIndex].hasAttribute('data-combobox-disabled')) {\n clearSelectedItem();\n items[nextIndex].setAttribute('data-combobox-selected', 'true');\n items[nextIndex].setAttribute('aria-selected', 'true');\n items[nextIndex].scrollIntoView({ block: 'nearest', behavior: scrollBehavior });\n return items[nextIndex].id;\n }\n\n return null;\n },\n [scrollBehavior, clearSelectedItem]\n );\n\n const selectActiveOption = useCallback(() => {\n const root = getRootElement(targetRef.current);\n const activeOption = findElementBySelector<HTMLDivElement>(\n `#${listId.current} [data-combobox-active]`,\n root\n );\n\n if (activeOption) {\n const items = findElementsBySelector<HTMLDivElement>(\n `#${listId.current} [data-combobox-option]`,\n root\n );\n const index = items.findIndex((option) => option === activeOption);\n return selectOption(index);\n }\n\n return selectOption(0);\n }, [selectOption]);\n\n const selectNextOption = useCallback(() => {\n const root = getRootElement(targetRef.current);\n const items = findElementsBySelector<HTMLDivElement>(\n `#${listId.current} [data-combobox-option]`,\n root\n );\n return selectOption(getNextIndex(selectedOptionIndex.current, items, loop));\n }, [selectOption, loop]);\n\n const selectPreviousOption = useCallback(() => {\n const root = getRootElement(targetRef.current);\n const items = findElementsBySelector<HTMLDivElement>(\n `#${listId.current} [data-combobox-option]`,\n root\n );\n return selectOption(getPreviousIndex(selectedOptionIndex.current, items, loop));\n }, [selectOption, loop]);\n\n const selectFirstOption = useCallback(() => {\n const root = getRootElement(targetRef.current);\n const items = findElementsBySelector<HTMLDivElement>(\n `#${listId.current} [data-combobox-option]`,\n root\n );\n return selectOption(getFirstIndex(items));\n }, [selectOption]);\n\n const updateSelectedOptionIndex: ComboboxStore['updateSelectedOptionIndex'] = useCallback(\n (target = 'selected', options) => {\n if (typeof target === 'number') {\n selectedOptionIndex.current = target;\n const root = getRootElement(targetRef.current);\n\n const items = findElementsBySelector<HTMLDivElement>(\n `#${listId.current} [data-combobox-option]`,\n root\n );\n\n if (options?.scrollIntoView) {\n items[target]?.scrollIntoView({ block: 'nearest', behavior: scrollBehavior });\n }\n return;\n }\n\n selectedIndexUpdateTimeout.current = window.setTimeout(() => {\n const root = getRootElement(targetRef.current);\n const items = findElementsBySelector<HTMLDivElement>(\n `#${listId.current} [data-combobox-option]`,\n root\n );\n const index = items.findIndex((option) => option.hasAttribute(`data-combobox-${target}`));\n\n selectedOptionIndex.current = index;\n\n if (options?.scrollIntoView) {\n items[index]?.scrollIntoView({ block: 'nearest', behavior: scrollBehavior });\n }\n }, 0);\n },\n []\n );\n\n const resetSelectedOption = useCallback(() => {\n selectedOptionIndex.current = -1;\n clearSelectedItem();\n }, [clearSelectedItem]);\n\n const clickSelectedOption = useCallback(() => {\n const root = getRootElement(targetRef.current);\n const items = findElementsBySelector<HTMLDivElement>(\n `#${listId.current} [data-combobox-option]`,\n root\n );\n const item = items?.[selectedOptionIndex.current];\n item?.click();\n }, []);\n\n const setListId = useCallback((id: string) => {\n listId.current = id;\n }, []);\n\n const focusSearchInput = useCallback(() => {\n focusSearchTimeout.current = window.setTimeout(() => searchRef.current?.focus(), 0);\n }, []);\n\n const focusTarget = useCallback(() => {\n focusTargetTimeout.current = window.setTimeout(() => targetRef.current?.focus(), 0);\n }, []);\n\n const getSelectedOptionIndex = useCallback(() => selectedOptionIndex.current, []);\n\n useEffect(\n () => () => {\n window.clearTimeout(focusSearchTimeout.current);\n window.clearTimeout(focusTargetTimeout.current);\n window.clearTimeout(selectedIndexUpdateTimeout.current);\n },\n []\n );\n\n return {\n dropdownOpened,\n openDropdown,\n closeDropdown,\n toggleDropdown,\n\n selectedOptionIndex: selectedOptionIndex.current,\n getSelectedOptionIndex,\n selectOption,\n selectFirstOption,\n selectActiveOption,\n selectNextOption,\n selectPreviousOption,\n resetSelectedOption,\n updateSelectedOptionIndex,\n\n listId: listId.current,\n setListId,\n clickSelectedOption,\n\n searchRef,\n focusSearchInput,\n\n targetRef,\n focusTarget,\n };\n}\n"],"mappings":"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"}