From 382fc9358751f660040228efbb44a088c2e03466 Mon Sep 17 00:00:00 2001 From: Olson Date: Mon, 13 Apr 2026 17:00:07 -0400 Subject: [PATCH] (#28) Implement Dropdown/Select component * Adds Dropdown and Dropdown Option components with NCIDS usa-select classes * Support configurable options * Includes unit tests * Adds Storybook documentation Closes #28 --- .storybook/preview.scss | 2 + .../ncids/Dropdown/Dropdown.stories.tsx | 57 ++++++++++++ .../ncids/Dropdown/Dropdown.test.tsx | 91 +++++++++++++++++++ src/components/ncids/Dropdown/Dropdown.tsx | 67 ++++++++++++++ src/components/ncids/Dropdown/index.ts | 2 + src/components/ncids/index.ts | 2 + 6 files changed, 221 insertions(+) create mode 100644 src/components/ncids/Dropdown/Dropdown.stories.tsx create mode 100644 src/components/ncids/Dropdown/Dropdown.test.tsx create mode 100644 src/components/ncids/Dropdown/Dropdown.tsx create mode 100644 src/components/ncids/Dropdown/index.ts diff --git a/.storybook/preview.scss b/.storybook/preview.scss index 92e0b24..af60bd1 100644 --- a/.storybook/preview.scss +++ b/.storybook/preview.scss @@ -8,3 +8,5 @@ @forward 'usa-pagination'; @forward 'usa-icon'; @forward 'usa-collection'; +@forward 'usa-select'; +@forward 'usa-combo-box'; diff --git a/src/components/ncids/Dropdown/Dropdown.stories.tsx b/src/components/ncids/Dropdown/Dropdown.stories.tsx new file mode 100644 index 0000000..30d0a2f --- /dev/null +++ b/src/components/ncids/Dropdown/Dropdown.stories.tsx @@ -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 = { + title: 'NCIDS/Dropdown', + component: Dropdown, + tags: ['autodocs'], + argTypes: { + className: { + control: 'text', + description: 'Additional CSS classes on the list', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +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 ( +
+ Show + + results per page +
+ ); + }, + 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(), + }, +}; diff --git a/src/components/ncids/Dropdown/Dropdown.test.tsx b/src/components/ncids/Dropdown/Dropdown.test.tsx new file mode 100644 index 0000000..8c122f7 --- /dev/null +++ b/src/components/ncids/Dropdown/Dropdown.test.tsx @@ -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('', () => { + afterEach(() => { + cleanup(); + }); + + it('should render options', () => { + render( + + ); + + 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( + + ); + 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( + + ); + + 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( + + ); + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/src/components/ncids/Dropdown/Dropdown.tsx b/src/components/ncids/Dropdown/Dropdown.tsx new file mode 100644 index 0000000..1764223 --- /dev/null +++ b/src/components/ncids/Dropdown/Dropdown.tsx @@ -0,0 +1,67 @@ +import React from 'react'; + +export interface DropdownOptionProps { + /** Option label */ + label: string; + /** Additional CSS classes on the