Skip to content

Commit

Permalink
Merge pull request #79 from MetroStar/CSG-1032
Browse files Browse the repository at this point in the history
CSG-1032: Comet USWDS Components with children should allow rendering of React children or a list of items
  • Loading branch information
jbouder committed Sep 27, 2023
2 parents e83274c + 659dd37 commit d7410dc
Show file tree
Hide file tree
Showing 29 changed files with 488 additions and 192 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { StoryFn, Meta } from '@storybook/react';
import { Accordion } from '../../index';
import { Accordion, AccordionItem } from '../../index';
import { AccordionProps } from './accordion';

const meta: Meta<typeof Accordion> = {
Expand All @@ -22,13 +22,13 @@ Default.args = {
{
id: 'item-1',
label: 'Item 1',
child: <span>Hello</span>,
children: <span>Hello</span>,
expanded: true,
},
{
id: 'item-2',
label: 'Item 2',
child: <span>World</span>,
children: <span>World</span>,
expanded: false,
},
],
Expand All @@ -42,14 +42,31 @@ MultiSelect.args = {
{
id: 'item-1',
label: 'Item 1',
child: <span>Hello</span>,
children: <span>Hello</span>,
expanded: false,
},
{
id: 'item-2',
label: 'Item 2',
child: <span>World</span>,
children: <span>World</span>,
expanded: false,
},
],
};

const ChildrenTemplate: StoryFn<typeof Accordion> = (args: AccordionProps) => (
<Accordion {...args}>
<AccordionItem id="item-1" label="Item 1" expanded={true}>
<span>Hello</span>
</AccordionItem>
<AccordionItem id="item-2" label="Item 2" expanded={false}>
<span>World</span>
</AccordionItem>
</Accordion>
);

export const WithChildren = ChildrenTemplate.bind({});
WithChildren.args = {
id: 'accordion-3',
allowMultiSelect: false,
};
13 changes: 9 additions & 4 deletions packages/comet-uswds/src/components/accordion/accordion.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('Accordion', () => {
id: 'item-1',
label: 'foo',
expanded: false,
child: <span>bar</span>,
children: <span>bar</span>,
},
];
const { container } = render(<Accordion id="accordion" items={items} />);
Expand All @@ -24,7 +24,7 @@ describe('Accordion', () => {
id: 'item-1',
label: 'foo',
expanded: false,
child: <span>bar</span>,
children: <span>bar</span>,
},
];
render(<Accordion id="accordion" items={items} />);
Expand All @@ -43,7 +43,7 @@ describe('Accordion', () => {
id: 'item-1',
label: 'foo',
expanded: true,
child: <span>bar</span>,
children: <span>bar</span>,
},
];
render(<Accordion id="accordion" items={items} />);
Expand All @@ -62,7 +62,7 @@ describe('Accordion', () => {
id: 'item-1',
label: 'foo',
expanded: false,
child: <span>bar</span>,
children: <span>bar</span>,
},
];
render(<Accordion id="accordion" items={items} allowMultiSelect={true} />);
Expand All @@ -74,4 +74,9 @@ describe('Accordion', () => {

expect(screen.getByText('bar')).toBeVisible();
});

test('should not render when no items or children are provided', () => {
const { container } = render(<Accordion id="accordion" />);
expect(container.querySelector('#accordion')).toBeFalsy();
});
});
87 changes: 58 additions & 29 deletions packages/comet-uswds/src/components/accordion/accordion.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { ReactNode, useEffect, useRef } from 'react';
import clasnames from 'classnames';
import React, { ReactElement, ReactNode, useEffect, useRef } from 'react';
import classnames from 'classnames';
import accordion from '@uswds/uswds/js/usa-accordion';
import './accordion.style.css';

export interface AccordionItem {
export interface AccordionItemProps {
/**
* The unique identifier for the accordion item
*/
Expand All @@ -19,7 +19,7 @@ export interface AccordionItem {
/**
* The body of the accordion item
*/
child: ReactNode;
children: ReactNode;
}

export interface AccordionProps {
Expand All @@ -34,7 +34,11 @@ export interface AccordionProps {
/**
* An array of AccordionItem objects, used to build the accordion
*/
items: AccordionItem[];
items?: AccordionItemProps[];
/**
* AccordionItem components to display as children
*/
children?: ReactElement<AccordionItemProps> | Array<ReactElement<AccordionItemProps>>;
}

/**
Expand All @@ -44,7 +48,13 @@ export const Accordion = ({
id,
allowMultiSelect = false,
items,
}: AccordionProps): React.ReactElement => {
children,
}: AccordionProps): ReactElement => {
// If no children and items provided, render partial
if (!children && !items) {
return <></>;
}

// Ensure accordion JS is loaded
const accordionRef = useRef<HTMLDivElement>(null);
useEffect(() => {
Expand All @@ -71,34 +81,53 @@ export const Accordion = ({
<div
id={id}
ref={accordionRef}
className={clasnames('usa-accordion', {
className={classnames('usa-accordion', {
'usa-accordion--multiselectable': allowMultiSelect,
})}
data-allow-multiple={allowMultiSelect ? true : undefined}
>
{items.map((e, i) => (
<div className="accordion-item" data-testid="accordion-item" key={`accordion-item-${i}`}>
<h4 className="usa-accordion__heading">
<button
type="button"
className="usa-accordion__button"
data-testid="accordion-button"
aria-expanded={items[i].expanded}
aria-controls={items[i].id}
>
{e.label}
</button>
</h4>
<div
id={items[i].id}
className="usa-accordion__content usa-prose text-left"
data-testid="accordion-content"
hidden={!items[i].expanded}
{children ??
items?.map((e, i) => (
<AccordionItem
id={e.id}
key={`accordion-item-${i}`}
label={e.label}
expanded={e.expanded}
>
{e.child}
</div>
</div>
))}
{e.children}
</AccordionItem>
))}
</div>
);
};

export const AccordionItem = ({
id,
label,
expanded,
children,
}: AccordionItemProps): ReactElement => {
return (
<div className="accordion-item" data-testid="accordion-item">
<h4 className="usa-accordion__heading">
<button
type="button"
className="usa-accordion__button"
data-testid="accordion-button"
aria-expanded={expanded}
aria-controls={id}
>
{label}
</button>
</h4>
<div
id={id}
className="usa-accordion__content usa-prose text-left"
data-testid="accordion-content"
hidden={!expanded}
>
{children}
</div>
</div>
);
};
Expand Down
2 changes: 1 addition & 1 deletion packages/comet-uswds/src/components/accordion/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { default, AccordionItem } from './accordion';
export { default, AccordionItem, AccordionItemProps } from './accordion';
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { StoryFn, Meta } from '@storybook/react';
import { Breadcrumb } from '../../index';
import { BreadcrumbProps, Crumb } from './breadcrumb';
import { Breadcrumb, BreadcrumbItem } from '../../index';
import { BreadcrumbItemProps, BreadcrumbProps } from './breadcrumb';

const meta: Meta<typeof Breadcrumb> = {
title: 'USWDS/Breadcrumb',
Expand All @@ -11,10 +11,12 @@ export default meta;

const Template: StoryFn<typeof Breadcrumb> = (args: BreadcrumbProps) => <Breadcrumb {...args} />;

const action = (c: BreadcrumbItemProps): void => alert('Called with: ' + JSON.stringify(c));

export const Default = Template.bind({});
Default.args = {
id: 'breadcrumb-1',
crumbs: [
items: [
{
name: 'Rome',
path: '/rome',
Expand All @@ -25,5 +27,19 @@ Default.args = {
},
],
current: 'Italy',
action: (c: Crumb) => alert('Called with: ' + JSON.stringify(c)),
action,
};

const ChildrenTemplate: StoryFn<typeof Breadcrumb> = (args: BreadcrumbProps) => (
<Breadcrumb {...args}>
<BreadcrumbItem name="Rome" path="/rome" action={args.action} />
<BreadcrumbItem name="Greece" path="/greece" action={args.action} />
</Breadcrumb>
);

export const WithChildren = ChildrenTemplate.bind({});
WithChildren.args = {
id: 'breadcrumb-2',
current: 'Italy',
action,
};
26 changes: 21 additions & 5 deletions packages/comet-uswds/src/components/breadcrumb/breadcrumb.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@ import React from 'react';
import '@testing-library/jest-dom';
import { render, screen, fireEvent } from '@testing-library/react';
import { axe } from 'jest-axe';
import Breadcrumb from './breadcrumb';
import Breadcrumb, { BreadcrumbItem } from './breadcrumb';

describe('Breadcrumb', () => {
test('should render with no accessibility violations', async () => {
const spy = jest.fn();
const crumbs = [{ path: '/test', name: 'test' }];
const { container } = render(
<Breadcrumb id="breadcrumb" crumbs={crumbs} current="foo" action={spy} />,
<Breadcrumb id="breadcrumb" items={crumbs} current="foo" action={spy} />,
);
expect(await axe(container)).toHaveNoViolations();
});

test('should render with given props and is callable', () => {
const spy = jest.fn();
const crumbs = [{ path: '/test', name: 'test' }];
render(<Breadcrumb id="breadcrumb" crumbs={crumbs} current="foo" action={spy} />);
const crumbs = [{ path: '/test', name: 'test', action: spy }];
render(<Breadcrumb id="breadcrumb" items={crumbs} current="foo" action={spy} />);

expect(screen.getByText('test')).toBeVisible();
expect(screen.getByText('foo')).toBeVisible();
Expand All @@ -30,6 +30,22 @@ describe('Breadcrumb', () => {
test('should render when current is optional', () => {
const spy = jest.fn();
const crumbs = [{ path: '/test', name: 'test' }];
render(<Breadcrumb id="breadcrumb" crumbs={crumbs} action={spy} />);
render(<Breadcrumb id="breadcrumb" items={crumbs} action={spy} />);
});

test('should not render when no children or items are provided', () => {
const spy = jest.fn();
const { container } = render(<Breadcrumb id="breadcrumb" action={spy} />);
expect(container.querySelector('#breadcrumb')).toBeFalsy();
});

test('should render with children', () => {
const spy = jest.fn();
const { container } = render(
<Breadcrumb id="breadcrumb" action={spy}>
<BreadcrumbItem path="/test" name="test" />
</Breadcrumb>,
);
expect(container.querySelector('#breadcrumb')).toBeTruthy();
});
});
Loading

0 comments on commit d7410dc

Please sign in to comment.