@volvo-cars/react-accordion

Volvo Cars React Accordion component

Usage no npm install needed!

<script type="module">
  import volvoCarsReactAccordion from 'https://cdn.skypack.dev/@volvo-cars/react-accordion';
</script>

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 and AccordionDetails that houses the content of the Accordion.

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, the multipleprop 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 the role prop on the AccordionSummary 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

Props - Accordion

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

Props - AccordionController

Name Description Type Default Value
multiple When false, only one Accordion opens at a time boolean false

Props - AccordionSummary

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

Props - AccordionDetails

Name Description Type Default Value
className Class name to be passed to AccordionDetails wrapper string undefined
extend Extends AccordionDetails wrapper styles {} ()=>{} [] undefined

Props - AccordionAction

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