Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .storybook/preview.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@
@forward 'usa-pagination';
@forward 'usa-icon';
@forward 'usa-collection';
@forward 'usa-select';
@forward 'usa-combo-box';
57 changes: 57 additions & 0 deletions src/components/ncids/Dropdown/Dropdown.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import React from 'react';

import { Dropdown } from './Dropdown';

const meta: Meta<typeof Dropdown> = {
title: 'NCIDS/Dropdown',
component: Dropdown,
tags: ['autodocs'],
argTypes: {
className: {
control: 'text',
description: 'Additional CSS classes on the list',
},
},
};

export default meta;
type Story = StoryObj<typeof Dropdown>;

export const Default: Story = {
args: {
id: 'default-dropdown',
name: 'default-dropdown',
options: [
{ label: '20', value: 20 },
{ label: '50', value: 50 },
{ label: '100', value: 100 },
],
onChange: fn(),
},
};

export const WithResultsPerPageText: Story = {
name: 'With Results Per Page Text',
render: (args) => {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span>Show</span>
<Dropdown {...args} style={{ width: '70px' }} />
<span>results per page</span>
</div>
);
},
args: {
id: 'results-per-page',
name: 'results-per-page',
ariaLabel: 'Select option',
options: [
{ label: '20', value: 20 },
{ label: '50', value: 50 },
{ label: '100', value: 100 },
],
onChange: fn(),
},
};
91 changes: 91 additions & 0 deletions src/components/ncids/Dropdown/Dropdown.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React from 'react';
import { cleanup, render, screen } from '@testing-library/react';
import { axe } from 'vitest-axe';
import { afterEach, describe, expect, it, vi } from 'vitest';
import userEvent from '@testing-library/user-event';

import { Dropdown } from './Dropdown';

describe('<Dropdown />', () => {
afterEach(() => {
cleanup();
});

it('should render options', () => {
render(
<Dropdown
id="test"
name="test"
ariaLabel="Select option"
options={[
{ label: '20', value: 20 },
{ label: '30', value: 30 },
{ label: '50', value: 50 },
{ label: '100', value: 100 },
]}
/>
);

expect(screen.getByRole('option', { name: '20' })).toBeInTheDocument();
expect(screen.getByRole('option', { name: '30' })).toBeInTheDocument();
expect(screen.getByRole('option', { name: '50' })).toBeInTheDocument();
expect(screen.getByRole('option', { name: '100' })).toBeInTheDocument();
});

it('should render a select with usa-select class', () => {
const { container } = render(
<Dropdown
id="test"
name="test"
ariaLabel="Select option"
options={[
{ label: '20', value: '20' },
{ label: '30', value: '30' },
]}
/>
);
const select = container.querySelector('select');
expect(select).toHaveClass('usa-select');
});

it('should call onChange when selection changes', async () => {
const user = userEvent.setup();
const handleChange = vi.fn();

render(
<Dropdown
id="test"
name="test"
onChange={handleChange}
ariaLabel="Select option"
options={[
{ label: 'Twenty', value: 20 },
{ label: 'Thirty', value: 30 },
]}
/>
);

const select = screen.getByRole('combobox');
await user.selectOptions(select, '20');

expect(handleChange).toHaveBeenCalledTimes(1);
});

it('should have no accessibility violations', async () => {
const handleChange = vi.fn();
const { container } = render(
<Dropdown
id="test"
name="test"
ariaLabel="Select option"
onChange={handleChange}
options={[
{ label: 'Twenty', value: 20 },
{ label: 'Thirty', value: 30 },
]}
/>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
67 changes: 67 additions & 0 deletions src/components/ncids/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';

export interface DropdownOptionProps {
/** Option label */
label: string;
/** Additional CSS classes on the <option>. */
className?: string;
/** Value of the option */
value: string | number;
}

export const DropdownOption: React.FC<DropdownOptionProps> = ({
label,
className,
value,
}) => {
return (
<option key={`option_${value}`} value={value} className={className}>
{label}
</option>
);
};

export interface DropdownProps
extends React.SelectHTMLAttributes<HTMLSelectElement> {
/** Dropdown list ID */
id: string;
/** Name for the dropdown list */
name: string;
/** Additional CSS classes on the <select>. */
className?: string;
/** Array of options */
options: DropdownOptionProps[];
/** Aria label on the <select> */
ariaLabel: string;
/** Callback fired when an option is selected */
onChange?: (event: React.ChangeEvent<HTMLSelectElement>) => void;
}

export const Dropdown: React.FC<DropdownProps> = ({
id,
name,
options,
className,
ariaLabel,
onChange,
...rest
}) => {
const classes = ['usa-select', className || ''].filter(Boolean).join(' ');

return (
<select
className={classes}
name={name}
id={id}
aria-label={ariaLabel}
onChange={onChange}
{...rest}
>
{options.map((opt) => (
<DropdownOption key={`option_${opt.value}`} {...opt} />
))}
</select>
);
};

export default Dropdown;
2 changes: 2 additions & 0 deletions src/components/ncids/Dropdown/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Dropdown, default } from './Dropdown';
export type { DropdownProps } from './Dropdown';
2 changes: 2 additions & 0 deletions src/components/ncids/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export { Pager } from './Pager';
export type { PagerProps, PageChangeData } from './Pager';
export { Collection, CollectionItem } from './Collection';
export type { CollectionProps, CollectionItemProps } from './Collection';
export { Dropdown } from './Dropdown';
export type { DropdownProps } from './Dropdown';
Loading