diff --git a/packages/store-ui/src/molecules/Accordion/Accordion.test.tsx b/packages/store-ui/src/molecules/Accordion/Accordion.test.tsx index 203b99cf43..c59bcc5200 100644 --- a/packages/store-ui/src/molecules/Accordion/Accordion.test.tsx +++ b/packages/store-ui/src/molecules/Accordion/Accordion.test.tsx @@ -1,4 +1,4 @@ -import { render, fireEvent } from '@testing-library/react' +import { render, fireEvent, cleanup } from '@testing-library/react' import { axe } from 'jest-axe' import React, { useState } from 'react' @@ -56,23 +56,137 @@ const TestAccordion = () => { } describe('Accordion', () => { - it('should have `data-store-accordion` attribute', () => { - const { getByTestId } = render() + let accordion: HTMLElement + let items: HTMLElement[] + let buttons: HTMLElement[] + let panels: HTMLElement[] - expect(getByTestId('store-accordion')).toHaveAttribute( - 'data-store-accordion' + beforeEach(() => { + const { getByTestId, getAllByTestId } = render() + + accordion = getByTestId('store-accordion') + items = getAllByTestId('store-accordion-item') + buttons = getAllByTestId('store-accordion-button') + panels = getAllByTestId('store-accordion-panel') + }) + + afterEach(cleanup) + + it('should show panel specified by `indices`', () => { + const { getAllByTestId } = render( + {}}> + + + + + + + + + ) + + const panelsMock = getAllByTestId('store-accordion-panel-mock') + + expect(panelsMock[0]).not.toBeVisible() + expect(panelsMock[1]).toBeVisible() }) - it('should not have ARIA violations', async () => { - const { queryAllByTestId } = render() + describe('Data attributes', () => { + it('`Accordion` component should have `data-store-accordion` attribute', () => { + expect(accordion).toHaveAttribute('data-store-accordion') + }) + + it('`AccordionItem` component should have `data-store-accordion-item` attribute', () => { + for (const item of items) { + expect(item).toHaveAttribute('data-store-accordion-item') + } + }) + + it('`AccordionButton` component should have `data-store-accordion-button` attribute', () => { + for (const button of buttons) { + expect(button).toHaveAttribute('data-store-accordion-button') + } + }) + + it('`AccordionPanel` component should have `data-store-accordion-panel` attribute', () => { + for (const panel of panels) { + expect(panel).toHaveAttribute('data-store-accordion-panel') + } + }) + }) + + describe('User actions', () => { + it('clicking item should call `onChange` function', () => { + const mockOnChange = jest.fn() + const { getByTestId } = render( + + + + + + ) + + const button = getByTestId('store-accordion-button-mock') + + fireEvent.click(button) + expect(mockOnChange).toHaveBeenCalledTimes(1) + }) + + it('should move focus to the next focusable button on `ArrowDown` press', () => { + buttons[1].focus() + expect(buttons[1]).toHaveFocus() + fireEvent.keyDown(document.activeElement!, { key: 'ArrowDown' }) + expect(buttons[0]).toHaveFocus() + }) + + it('should move focus to the previous focusable button on `ArrowUp` press', () => { + buttons[1].focus() + expect(buttons[1]).toHaveFocus() + fireEvent.keyDown(document.activeElement!, { key: 'ArrowUp' }) + expect(buttons[0]).toHaveFocus() + }) + }) + + describe('Acessibility', () => { + // WAI-ARIA tests + // https://www.w3.org/TR/wai-aria-practices-1.2/#accordion + it('should not have violations', async () => { + expect(await axe(document.body)).toHaveNoViolations() + + // Open a panel and check again + fireEvent.click(buttons[0]) + expect(await axe(document.body)).toHaveNoViolations() + }) + + it('`role` should be set to `region` for panel elements', () => { + for (const panel of panels) { + expect(panel).toHaveAttribute('role', 'region') + } + }) - expect(await axe(document.body)).toHaveNoViolations() + it('`aria-labelledby` for panel elements should point to the corresponding button', () => { + panels.forEach((panel, index) => { + expect(panel).toHaveAttribute( + 'aria-labelledby', + buttons[index].getAttribute('id') + ) + }) + }) - // Open a panel and check again - const buttons = queryAllByTestId('store-accordion-button') + it('`aria-controls` for button elements should point to the corresponding panel', () => { + buttons.forEach((button, index) => { + expect(button).toHaveAttribute( + 'aria-controls', + panels[index].getAttribute('id') + ) + }) + }) - fireEvent.click(buttons[1]) - expect(await axe(document.body)).toHaveNoViolations() + it('`aria-expanded` should be true only for active button', () => { + expect(buttons[0]).toHaveAttribute('aria-expanded', 'false') + fireEvent.click(buttons[0]) + expect(buttons[0]).toHaveAttribute('aria-expanded', 'true') + }) }) }) diff --git a/packages/store-ui/src/molecules/Accordion/Accordion.tsx b/packages/store-ui/src/molecules/Accordion/Accordion.tsx index 80bedaf5c5..be3f83700a 100644 --- a/packages/store-ui/src/molecules/Accordion/Accordion.tsx +++ b/packages/store-ui/src/molecules/Accordion/Accordion.tsx @@ -9,11 +9,11 @@ export interface AccordionProps */ testId?: string /** - * Indices that indicate which accordion items are opened + * Indices that indicate which accordion items are opened. */ indices: Iterable /** - * Function that is triggered when an accordion item is opened/closed + * Function that is triggered when an accordion item is opened/closed. */ onChange: (index: number) => void } @@ -21,6 +21,7 @@ export interface AccordionProps interface AccordionContext { indices: Set onChange: (index: number) => void + numberOfItems: number } const AccordionContext = createContext(undefined) @@ -29,13 +30,17 @@ const Accordion = forwardRef(function Accordion( { testId = 'store-accordion', indices, onChange, children, ...props }, ref ) { - const context = { indices: new Set(indices), onChange } - const childrenWithIndex = React.Children.map( children as ReactElement, (child, index) => cloneElement(child, { index: child.props.index ?? index }) ) + const context = { + indices: new Set(indices), + onChange, + numberOfItems: childrenWithIndex.length, + } + return (
{ + if (!['ArrowDown', 'ArrowUp'].includes(event.key)) { + return + } + + const getNext = () => { + const next = Number(index) + 1 === numberOfItems ? 0 : Number(index) + 1 + + return document.getElementById(`button--${next}`) + } + + const getPrevious = () => { + const previous = + Number(index) - 1 < 0 ? numberOfItems - 1 : Number(index) - 1 + + return document.getElementById(`button--${previous}`) + } + + switch (event.key) { + case 'ArrowDown': + event.preventDefault() + getNext()?.focus() + break + + case 'ArrowUp': + event.preventDefault() + getPrevious()?.focus() + break + + default: + } + } + return (