UNPKG

@base-ui-components/react

Version:

Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.

144 lines (143 loc) 4.28 kB
'use client'; import * as React from 'react'; import { mergeReactProps } from '../../utils/mergeReactProps.js'; import { useControlled } from '../../utils/useControlled.js'; import { ARROW_DOWN, ARROW_UP, ARROW_RIGHT, ARROW_LEFT } from '../../composite/composite.js'; const SUPPORTED_KEYS = [ARROW_DOWN, ARROW_UP, ARROW_RIGHT, ARROW_LEFT, 'Home', 'End']; function getActiveTriggers(accordionItemRefs) { const { current: accordionItemElements } = accordionItemRefs; const output = []; for (let i = 0; i < accordionItemElements.length; i += 1) { const section = accordionItemElements[i]; if (!isDisabled(section)) { const trigger = section?.querySelector('[type="button"]'); if (!isDisabled(trigger)) { output.push(trigger); } } } return output; } function isDisabled(element) { return element === null || element.hasAttribute('disabled') || element.getAttribute('data-disabled') === 'true'; } export function useAccordionRoot(parameters) { const { disabled, direction, loop, onValueChange, orientation, openMultiple, value: valueParam, defaultValue } = parameters; const accordionItemRefs = React.useRef([]); const [value, setValue] = useControlled({ controlled: valueParam, default: defaultValue, name: 'Accordion', state: 'value' }); const handleValueChange = React.useCallback((newValue, nextOpen) => { if (!openMultiple) { const nextValue = value[0] === newValue ? [] : [newValue]; setValue(nextValue); onValueChange?.(nextValue); } else if (nextOpen) { const nextOpenValues = value.slice(); nextOpenValues.push(newValue); setValue(nextOpenValues); onValueChange?.(nextOpenValues); } else { const nextOpenValues = value.filter(v => v !== newValue); setValue(nextOpenValues); onValueChange?.(nextOpenValues); } }, [onValueChange, openMultiple, setValue, value]); const getRootProps = React.useCallback((externalProps = {}) => { const isRtl = direction === 'rtl'; const isHorizontal = orientation === 'horizontal'; return mergeReactProps(externalProps, { dir: direction, role: 'region', onKeyDown(event) { if (!SUPPORTED_KEYS.includes(event.key)) { return; } event.preventDefault(); const triggers = getActiveTriggers(accordionItemRefs); const numOfEnabledTriggers = triggers.length; const lastIndex = numOfEnabledTriggers - 1; let nextIndex = -1; const thisIndex = triggers.indexOf(event.target); function toNext() { if (loop) { nextIndex = thisIndex + 1 > lastIndex ? 0 : thisIndex + 1; } else { nextIndex = Math.min(thisIndex + 1, lastIndex); } } function toPrev() { if (loop) { nextIndex = thisIndex === 0 ? lastIndex : thisIndex - 1; } else { nextIndex = thisIndex - 1; } } switch (event.key) { case ARROW_DOWN: if (!isHorizontal) { toNext(); } break; case ARROW_UP: if (!isHorizontal) { toPrev(); } break; case ARROW_RIGHT: if (isHorizontal) { if (isRtl) { toPrev(); } else { toNext(); } } break; case ARROW_LEFT: if (isHorizontal) { if (isRtl) { toNext(); } else { toPrev(); } } break; case 'Home': nextIndex = 0; break; case 'End': nextIndex = lastIndex; break; default: break; } if (nextIndex > -1) { triggers[nextIndex].focus(); } } }); }, [direction, loop, orientation]); return React.useMemo(() => ({ getRootProps, accordionItemRefs, direction, disabled, handleValueChange, orientation, value }), [getRootProps, accordionItemRefs, direction, disabled, handleValueChange, orientation, value]); }