@zendeskgarden/container-accordion
Version:
Containers relating to accordions in the Garden Design System
128 lines (123 loc) • 4.12 kB
JavaScript
/**
* Copyright Zendesk, Inc.
*
* Use of this source code is governed under the Apache License, Version 2.0
* found at http://www.apache.org/licenses/LICENSE-2.0.
*/
import React, { useState, useCallback, useMemo } from 'react';
import { useId, getControlledValue, composeEventHandlers, KEYS } from '@zendeskgarden/container-utilities';
import PropTypes from 'prop-types';
function useAccordion({
idPrefix,
sections = [],
expandedSections,
defaultExpandedSections,
onChange = () => undefined,
expandable = true,
collapsible = true
}) {
const prefix = useId(idPrefix);
const TRIGGER_ID = `${prefix}--trigger`;
const PANEL_ID = `${prefix}--panel`;
const isControlled = expandedSections !== null && expandedSections !== undefined;
const [expandedState, setExpandedState] = useState(defaultExpandedSections || sections.slice(0, 1));
const [disabledState, setDisabledState] = useState(collapsible ? [] : expandedState);
const internalExpandedState = getControlledValue(expandedSections, expandedState);
const toggle = useCallback(value => {
const expanded = [];
const disabled = [];
sections.forEach(sectionValue => {
let isExpanded = false;
if (sectionValue === value) {
isExpanded = collapsible ? internalExpandedState.includes(sectionValue) === false : true;
} else if (expandable) {
isExpanded = internalExpandedState.includes(sectionValue);
}
if (isExpanded) {
expanded.push(sectionValue);
if (!collapsible) {
disabled.push(sectionValue);
}
}
});
onChange(value);
if (isControlled === false) {
setExpandedState(expanded);
}
setDisabledState(disabled);
}, [sections, internalExpandedState, collapsible, expandable, isControlled, onChange]);
const getHeaderProps = useCallback(({
role = 'heading',
'aria-level': ariaLevel,
...props
}) => ({
role: role === null ? undefined : role,
'aria-level': ariaLevel,
'data-garden-container-id': 'containers.accordion',
'data-garden-container-version': '3.0.15',
...props
}), []);
const getTriggerProps = useCallback(({
value,
role = 'button',
tabIndex = 0,
...props
}) => ({
id: `${TRIGGER_ID}:${value}`,
role: role === null ? undefined : role,
tabIndex,
'aria-controls': `${PANEL_ID}:${value}`,
'aria-disabled': disabledState.includes(value) || undefined,
'aria-expanded': internalExpandedState.includes(value),
onClick: composeEventHandlers(props.onClick, () => toggle(value)),
onKeyDown: composeEventHandlers(props.onKeyDown, event => {
if (event.key === KEYS.SPACE || event.key === KEYS.ENTER) {
toggle(value);
event.preventDefault();
}
}),
...props
}), [PANEL_ID, TRIGGER_ID, internalExpandedState, disabledState, toggle]);
const getPanelProps = useCallback(({
value,
role = 'region',
...props
}) => ({
id: `${PANEL_ID}:${value}`,
role: role === null ? undefined : role,
'aria-hidden': !internalExpandedState.includes(value),
'aria-labelledby': `${TRIGGER_ID}:${value}`,
...props
}), [PANEL_ID, TRIGGER_ID, internalExpandedState]);
return useMemo(() => ({
getHeaderProps,
getTriggerProps,
getPanelProps,
expandedSections: internalExpandedState,
disabledSections: disabledState
}), [getHeaderProps, getTriggerProps, getPanelProps, internalExpandedState, disabledState]);
}
const AccordionContainer = props => {
const {
children,
render = children,
...options
} = props;
return React.createElement(React.Fragment, null, render(useAccordion(options)));
};
AccordionContainer.defaultProps = {
expandable: true,
collapsible: true
};
AccordionContainer.propTypes = {
children: PropTypes.func,
render: PropTypes.func,
sections: PropTypes.array.isRequired,
expandedSections: PropTypes.array,
defaultExpandedSections: PropTypes.array,
expandable: PropTypes.bool,
collapsible: PropTypes.bool,
idPrefix: PropTypes.string,
onChange: PropTypes.func
};
export { AccordionContainer, useAccordion };