README
React Accordion
@volvo-cars/react-accordion
An Accordion is a content area which can be collapsed and expanded. It can be used to group or hide complex regions to keep the page clean. This Accordion component is a lightweight container that may either stand alone or be connected to other Accordions.
Installation
💡 This package includes Typescript definitions
Simple Accordion
<Block extend={{ maxWidth: 400 }}>
<Accordion>
<AccordionSummary>
<Text>Accordion 1</Text>
</AccordionSummary>
<AccordionDetails>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</Text>
</AccordionDetails>
</Accordion>
<Accordion>
<AccordionSummary>
<Text>Accordion 2</Text>
</AccordionSummary>
<AccordionDetails>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</Text>
</AccordionDetails>
</Accordion>
<Accordion>
<AccordionSummary>
<Text>Accordion 3</Text>
</AccordionSummary>
<AccordionDetails>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</Text>
</AccordionDetails>
</Accordion>
</Block>
The above example shows independent accordions using
AccordionSummary
which controls the expanded state andAccordionDetails
that houses the content of theAccordion
.
Connected Accordions
By wrapping a set of Accordions with an AccordionController
we can control how accordions respond to other accordions expanded or collapse state.
<Block extend={{ maxWidth: 400 }}>
<AccordionController>
<Accordion>
<AccordionSummary>
<Text>Accordion 1</Text>
</AccordionSummary>
<AccordionDetails>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</Text>
</AccordionDetails>
</Accordion>
<Accordion>
<AccordionSummary>
<Text>Accordion 2</Text>
</AccordionSummary>
<AccordionDetails>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</Text>
</AccordionDetails>
</Accordion>
<Accordion>
<AccordionSummary>
<Text>Accordion 3</Text>
</AccordionSummary>
<AccordionDetails>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</Text>
</AccordionDetails>
</Accordion>
</AccordionController>
</Block>
Notice how only one Accordion is open at a time in the above example using
AccordionController
, themultiple
prop allows to override this behaviour.
Default Expanded
Specify which Accordions to be expanded by default.
<Block extend={{ maxWidth: 400 }}>
<AccordionController>
<Accordion defaultExpanded>
<AccordionSummary>
<Text>Accordion 1</Text>
</AccordionSummary>
<AccordionDetails>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</Text>
</AccordionDetails>
</Accordion>
<Accordion>
<AccordionSummary>
<Text>Accordion 2</Text>
</AccordionSummary>
<AccordionDetails>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</Text>
</AccordionDetails>
</Accordion>
<Accordion>
<AccordionSummary>
<Text>Accordion 3</Text>
</AccordionSummary>
<AccordionDetails>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</Text>
</AccordionDetails>
</Accordion>
</AccordionController>
</Block>
Controlled Accordion
Extend the default behaviour and control expanded and collapsed state yourself.
() => {
const [accordion, setAccordion] = React.useState('accordion1');
return (
<Block extend={{ maxWidth: 400 }}>
<AccordionController>
<Accordion
expanded={accordion === 'accordion1'}
onChange={(isExpanded) =>
setAccordion(isExpanded ? 'accordion1' : '')
}
>
<AccordionSummary>
<Text>Accordion 1</Text>
</AccordionSummary>
<AccordionDetails>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Suspendisse malesuada lacus ex, sit amet blandit leo lobortis
eget.
</Text>
</AccordionDetails>
</Accordion>
<Accordion
expanded={accordion === 'accordion2'}
onChange={(isExpanded) =>
setAccordion(isExpanded ? 'accordion2' : '')
}
>
<AccordionSummary>
<Text>Accordion 2</Text>
</AccordionSummary>
<AccordionDetails>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Suspendisse malesuada lacus ex, sit amet blandit leo lobortis
eget.
</Text>
</AccordionDetails>
</Accordion>
<Accordion
expanded={accordion === 'accordion3'}
onChange={(isExpanded) =>
setAccordion(isExpanded ? 'accordion3' : '')
}
>
<AccordionSummary>
<Text>Accordion 3</Text>
</AccordionSummary>
<AccordionDetails>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Suspendisse malesuada lacus ex, sit amet blandit leo lobortis
eget.
</Text>
</AccordionDetails>
</Accordion>
</AccordionController>
</Block>
);
};
Additional actions
In order to put an action such as a Checkbox or a button inside of the AccordionSummary
, you can use the AccordionAction
component that stops propagation of click and Enter/Spacebar events to the parent AccordionSummary
.
⚠️ Note that when using the
AccordionAction
, some accessibility issues arise mainly due to nested interactive elements, as those are ignored by screen readers. You are free to change therole
prop on theAccordionSummary
to decided which elements are favored by screen readers.
<Block extend={{ maxWidth: 400 }}>
<AccordionController>
<Accordion>
<AccordionSummary role={undefined}>
<AccordionAction
extend={({ theme }) => ({
border: `1px solid ${theme.color.foreground.primary}`,
display: 'inline-block',
})}
>
<Checkbox onChange={() => {}} label="Check me" />
</AccordionAction>
</AccordionSummary>
<AccordionDetails>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</Text>
</AccordionDetails>
</Accordion>
<Accordion>
<AccordionSummary>
<Text>Accordion 2</Text>
</AccordionSummary>
<AccordionDetails>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</Text>
</AccordionDetails>
</Accordion>
</AccordionController>
</Block>
onAnimationEnd
Apart from onChange
which is called when the Accordion is about to change state, there is a onAnimationEnd
callback function similar to onChange
but will only be called upon animation finish.
() => {
const [changeState, setChangeState] = React.useState(false);
const [animationEndState, setAnimationEndState] = React.useState(false);
return (
<Block extend={{ maxWidth: 400 }}>
<Text subStyle="emphasis">
onChange: Expanded: {changeState.toString()}
<br />
onAnimationEnd: Expanded: {animationEndState.toString()}
</Text>
<br />
<AccordionController>
<Accordion
onChange={(isExpanded) => setChangeState(isExpanded)}
onAnimationEnd={(isExpanded) => {
setAnimationEndState(isExpanded);
}}
>
<AccordionSummary>
<Text>Accordion 1</Text>
</AccordionSummary>
<AccordionDetails>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Suspendisse malesuada lacus ex, sit amet blandit leo lobortis
eget.
</Text>
</AccordionDetails>
</Accordion>
</AccordionController>
</Block>
);
};
Performance
The content of the Accordion is rendered by default even when collapsed, which is useful if it's required for SEO purposes. But if the content of the Accordion is expensive to render, it's possible to unmount it when collapsed using the lazyRenderDetails
prop on the Accordion component.
<Accordion lazyRenderDetails>
<AccordionSummary>
<Text>Accordion</Text>
</AccordionSummary>
<AccordionDetails>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</Text>
</AccordionDetails>
</Accordion>
If you have child elements that are expensive to render, and want to decrease the number of unnecessary re-renders in the Accordion component, it's recommended to wrap your callback function with a useCallback
instead of passing a new function on every render.
const [state,setState]= React.useState('');
const handleChange = React.useCallback((isExpanded)=>{
setState(isExpanded ? 'state1': 'state2')
},[])
() => {
return (
<Accordion onChange={handleChange}>
<AccordionSummary>
<Text>Accordion 1</Text>
</AccordionSummary>
<AccordionDetails>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</Text>
</AccordionDetails>
</Accordion>
);
};
Accessibility
The Accordion components are accessible by default, they add necessary roles, aria labels, keyboard interaction with Enter and Space as well as tab focus following WAI:ARIA
API
Accordion
Props - Name | Description | Type | Default Value |
---|---|---|---|
defaultExpanded |
Accordion should be expanded by default | boolean |
undefined |
onChange |
Fires when expanded state changes | (isExpanded:boolean) => void |
undefined |
lazyRenderDetails |
Unmounts AccordionDetails content when collapsed |
boolean |
undefined |
expanded |
Manually control expanded state | boolean |
undefined |
hideDivider |
Force hides divider that appears when more that one Accordion are rendered |
boolean |
undefined |
onAnimationEnd |
Fires when expanded state changes and animation has finished | (isVisible?: boolean) => void |
undefined |
className |
Class name to be passed to Accordion wrapper |
string |
undefined |
id |
Id to be passed to Accordion wrapper |
string |
undefined |
extend |
Extends Accordion wrapper styles |
{} ()=>{} [] |
undefined |
AccordionController
Props - Name | Description | Type | Default Value |
---|---|---|---|
multiple |
When false , only one Accordion opens at a time |
boolean |
false |
AccordionSummary
Props - Name | Description | Type | Default Value |
---|---|---|---|
onInteraction |
Fires when the user interacts with the AccordionSummary , either Click, Enter or Space |
({event?: SyntheticEvent, expanded?:boolean}) => void |
undefined |
role |
Changes the role of the AccordionSummary wrapper |
string |
button |
className |
Class name to be passed to AccordionSummary wrapper |
string |
undefined |
extend |
Extends AccordionSummary wrapper styles |
{} ()=>{} [] |
undefined |
iconAlignment |
Vertical alignment of the AccordionSummary icon |
top \| center |
top |
AccordionDetails
Props - Name | Description | Type | Default Value |
---|---|---|---|
className |
Class name to be passed to AccordionDetails wrapper |
string |
undefined |
extend |
Extends AccordionDetails wrapper styles |
{} ()=>{} [] |
undefined |
AccordionAction
Props - Name | Description | Type | Default Value |
---|---|---|---|
className |
Class name to be passed to AccordionAction wrapper |
string |
undefined |
extend |
Extends AccordionAction wrapper styles |
{} ()=>{} [] |
undefined |
id |
Id to be passed to AccordionAction wrapper |
string |
undefined |