Skip to content

Commit

Permalink
[WNMGDS-2415] Refactor AccordionItem (#2567)
Browse files Browse the repository at this point in the history
* Refactor AccordionItem from class to function component.

* Delete Accordion hgov stories file - redundant as core's story is themed correctly.

* Add tests for controlled AccordionItem.

* Clean up state and add TODO comment for  prop.
  • Loading branch information
zarahzachz authored and pwolfert committed Jul 5, 2023
1 parent 0387756 commit 76b428a
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 169 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,30 @@ describe('Controlled accordion item', function () {
userEvent.click(buttonEl);
expect(onClick).toHaveBeenCalled();
});
it('uses isControlledOpen as source of truth', () => {
const onChange = jest.fn();
const baseProps = { ...defaultProps, children: defaultChildren, onChange };
const { rerender } = renderAccordionItem({ ...baseProps, isControlledOpen: false });

const buttonEl = screen.getByRole('button');
expect(buttonEl).toHaveAttribute('aria-expanded', 'false');

rerender(
<AccordionItem {...baseProps} isControlledOpen={true}>
{defaultChildren}
</AccordionItem>
);
expect(buttonEl).toHaveAttribute('aria-expanded', 'true');

rerender(
<AccordionItem {...baseProps} isControlledOpen={false}>
{defaultChildren}
</AccordionItem>
);
expect(buttonEl).toHaveAttribute('aria-expanded', 'false');

// Even clicking the button shouldn't expand it if it's controlled
userEvent.click(buttonEl);
expect(buttonEl).toHaveAttribute('aria-expanded', 'false');
});
});
151 changes: 69 additions & 82 deletions packages/design-system/src/components/Accordion/AccordionItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { AddIcon, RemoveIcon } from '../Icons';
import classNames from 'classnames';
import uniqueId from 'lodash/uniqueId';
Expand Down Expand Up @@ -49,98 +49,85 @@ export interface AccordionItemProps {
*/
openIcon?: React.ReactNode;
}

export interface AccordionItemState {
isOpen?: boolean;
}
export class AccordionItem extends React.Component<AccordionItemProps, AccordionItemState> {
static defaultProps = {
headingLevel: '2',
closeIcon: (
<RemoveIcon
className="ds-c-accordion__button-icon"
title={t('accordion.close')}
ariaHidden={false}
/>
),
openIcon: (
<AddIcon
className="ds-c-accordion__button-icon"
title={t('accordion.open')}
ariaHidden={false}
/>
),
};

buttonId: string;
contentId: string;
isControlled: boolean;

constructor(props: AccordionItemProps) {
super(props);

this.isControlled = !!props.onChange;
this.state = this.isControlled ? {} : { isOpen: !!props.defaultOpen };
this.handleClick = this.handleClick.bind(this);
this.contentId = props.id || uniqueId('accordionItem_');
this.buttonId = `${this.contentId}-button`;
}
export const AccordionItem: React.FC<AccordionItemProps> = ({
buttonClassName,
children,
contentClassName,
defaultOpen = false,
heading,
headingLevel = '2',
id,
// TODO: Explore deprecating `isControlledOpen` in favor of `isOpen`
isControlledOpen,
onChange,
closeIcon = (
<RemoveIcon
className="ds-c-accordion__button-icon"
title={t('accordion.close')}
ariaHidden={false}
/>
),
openIcon = (
<AddIcon
className="ds-c-accordion__button-icon"
title={t('accordion.open')}
ariaHidden={false}
/>
),
}) => {
const contentClasses = classNames('ds-c-accordion__content', contentClassName);
const buttonClasses = classNames('ds-c-accordion__button', buttonClassName);
const HeadingTag = `h${headingLevel}` as const;
const CloseIcon = closeIcon as React.ReactNode;
const OpenIcon = openIcon as React.ReactNode;
const isControlled = !!onChange;
const contentId = id || uniqueId('accordionItem_');
const buttonId = `${contentId}-button`;
const [isOpen, setIsOpen] = useState(isControlled ? isControlledOpen : defaultOpen);

// Set the state for opening and closing an accordion item
handleClick(): void {
if (this.isControlled) {
this.props.onChange();
const handleClick = () => {
if (isControlled) {
onChange();
} else {
this.setState({ isOpen: !this.state.isOpen });
setIsOpen(!isOpen);
}
}

render() {
const {
buttonClassName,
children,
contentClassName,
heading,
headingLevel = '2',
isControlledOpen,
closeIcon,
openIcon,
} = this.props;
};

const contentClasses = classNames('ds-c-accordion__content', contentClassName);
const buttonClasses = classNames('ds-c-accordion__button', buttonClassName);
const HeadingTag = `h${headingLevel}` as const;
const isItemOpen = this.isControlled ? isControlledOpen : this.state.isOpen;
const CloseIcon = closeIcon as React.ReactNode;
const OpenIcon = openIcon as React.ReactNode;
const isItemOpen = isControlled ? isControlledOpen : isOpen;

if (heading) {
return (
<>
<HeadingTag className="ds-c-accordion__heading">
<button
className={buttonClasses}
aria-expanded={isItemOpen}
aria-controls={this.contentId}
id={this.buttonId}
onClick={this.handleClick}
type="button"
>
{heading}
{isItemOpen ? CloseIcon : OpenIcon}
</button>
</HeadingTag>
<div
className={contentClasses}
aria-labelledby={this.buttonId}
id={this.contentId}
hidden={this.isControlled ? !isControlledOpen : !this.state.isOpen}
if (heading) {
return (
<>
<HeadingTag className="ds-c-accordion__heading">
<button
className={buttonClasses}
aria-expanded={isItemOpen}
aria-controls={contentId}
id={buttonId}
onClick={handleClick}
type="button"
>
{children}
</div>
</>
);
}
{heading}
{isItemOpen ? CloseIcon : OpenIcon}
</button>
</HeadingTag>
<div
className={contentClasses}
aria-labelledby={buttonId}
id={contentId}
hidden={isControlled ? !isControlledOpen : !isOpen}
>
{children}
</div>
</>
);
}
}
};

export default AccordionItem;

This file was deleted.

0 comments on commit 76b428a

Please sign in to comment.