@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
JavaScript
'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]);
}