From 9bd91379722aa010ed5a9ee2c90daa852ce4c922 Mon Sep 17 00:00:00 2001 From: pearl-truss <67110378+pearl-truss@users.noreply.github.com> Date: Thu, 26 Jan 2023 08:48:08 -0500 Subject: [PATCH] feat: Make search component extendable (#2230) * add base SearchField component * update SearchField to have correct props, structure * incorporated SearchField into Search component * create base storybook story for SearchField component * add unit test for SearchField * moved SearchField into new shared Search directory * remove inline Label addition in Search component * created SearchButton component * incorporate SearchButton into Search component * keep track of button size in SearchButton component * add stories for SearchButton * add test for SearchButton * pass size fields and styling into SearchField and SearchButton components * Add test for passing usa-search--big class when isBig is set on SearchField * Add addtional test to Search * update import for Search in index.ts * fix additional import errors for Search * update import for Search in Header story * add test for passing input props to searchField component * revert changes to inputref * Add @pearl-truss as a contributor * push with updated ssh key --- .all-contributorsrc | 9 +++ README.md | 3 +- .../Search/{ => Search}/Search.stories.tsx | 2 +- .../Search/{ => Search}/Search.test.tsx | 24 ++++---- src/components/Search/{ => Search}/Search.tsx | 43 +++++--------- .../SearchButton/SearchButton.stories.tsx | 46 +++++++++++++++ .../Search/SearchButton/SearchButton.test.tsx | 46 +++++++++++++++ .../Search/SearchButton/SearchButton.tsx | 50 ++++++++++++++++ .../SearchField/SearchField.stories.tsx | 27 +++++++++ .../Search/SearchField/SearchField.test.tsx | 58 +++++++++++++++++++ .../Search/SearchField/SearchField.tsx | 53 +++++++++++++++++ src/components/forms/TextInput/TextInput.tsx | 2 + .../header/Header/Header.stories.tsx | 2 +- src/index.ts | 2 +- .../templates/documentation.stories.tsx | 2 +- src/stories/templates/landing.stories.tsx | 2 +- 16 files changed, 322 insertions(+), 49 deletions(-) rename src/components/Search/{ => Search}/Search.stories.tsx (97%) rename src/components/Search/{ => Search}/Search.test.tsx (76%) rename src/components/Search/{ => Search}/Search.tsx (51%) create mode 100644 src/components/Search/SearchButton/SearchButton.stories.tsx create mode 100644 src/components/Search/SearchButton/SearchButton.test.tsx create mode 100644 src/components/Search/SearchButton/SearchButton.tsx create mode 100644 src/components/Search/SearchField/SearchField.stories.tsx create mode 100644 src/components/Search/SearchField/SearchField.test.tsx create mode 100644 src/components/Search/SearchField/SearchField.tsx diff --git a/.all-contributorsrc b/.all-contributorsrc index 1a14991e1c..f568ce292a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -213,6 +213,15 @@ "contributions": [ "code" ] + }, + { + "login": "pearl-truss", + "name": "pearl-truss", + "avatar_url": "https://avatars.githubusercontent.com/u/67110378?v=4", + "profile": "https://github.com/pearl-truss", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7 diff --git a/README.md b/README.md index 4574320f12..c62586df15 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # @trussworks/react-uswds -[![All Contributors](https://img.shields.io/badge/all_contributors-22-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-23-orange.svg?style=flat-square)](#contributors-) [![npm version](https://img.shields.io/npm/v/@trussworks/react-uswds)](https://www.npmjs.com/package/@trussworks/react-uswds) @@ -156,6 +156,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Jacob Capps
Jacob Capps

💻 + pearl-truss
pearl-truss

💻 diff --git a/src/components/Search/Search.stories.tsx b/src/components/Search/Search/Search.stories.tsx similarity index 97% rename from src/components/Search/Search.stories.tsx rename to src/components/Search/Search/Search.stories.tsx index 4c900dbc55..1eddaf2065 100644 --- a/src/components/Search/Search.stories.tsx +++ b/src/components/Search/Search/Search.stories.tsx @@ -2,7 +2,7 @@ import React from 'react' import { Search } from './Search' export default { - title: 'Components/Search', + title: 'Components/Search/Search', component: Search, parameters: { docs: { diff --git a/src/components/Search/Search.test.tsx b/src/components/Search/Search/Search.test.tsx similarity index 76% rename from src/components/Search/Search.test.tsx rename to src/components/Search/Search/Search.test.tsx index 55d341b468..fa4e396d7e 100644 --- a/src/components/Search/Search.test.tsx +++ b/src/components/Search/Search/Search.test.tsx @@ -69,18 +69,16 @@ describe('Search component', () => { expect(queryByRole('button')).not.toHaveTextContent('Search') }) - describe('renders size classes', () => { - beforeEach(() => { - jest.clearAllMocks() - }) - it.each([ - ['big', 'usa-search--big'], - ['small', 'usa-search--small'], - ])('when size is %s should include class %s', (sizeString, uswdsClass) => { - const size = sizeString as 'big' | 'small' - const mockSubmit = jest.fn() - const { container } = render() - expect(container.querySelector('form')).toHaveClass(uswdsClass) - }) + it('adds small class when size prop is small', () => { + const mockSubmit = jest.fn() + const { container } = render() + expect(container.querySelector('div.usa-search--small button')).toBeInTheDocument() + }) + + it('adds big class when size prop is big', () => { + const mockSubmit = jest.fn() + const { container } = render() + expect(container.querySelector('div.usa-search--big button')).toBeInTheDocument() + expect(container.querySelector('div.usa-search--big input')).toBeInTheDocument() }) }) diff --git a/src/components/Search/Search.tsx b/src/components/Search/Search/Search.tsx similarity index 51% rename from src/components/Search/Search.tsx rename to src/components/Search/Search/Search.tsx index b82f890293..410e28cbb0 100644 --- a/src/components/Search/Search.tsx +++ b/src/components/Search/Search/Search.tsx @@ -1,12 +1,10 @@ import React from 'react' import classnames from 'classnames' -import searchImg from '@uswds/uswds/src/img/usa-icons-bg/search--white.svg' - -import { Button } from '../Button/Button' -import { Form, OptionalFormProps } from '../forms/Form/Form' -import { Label } from '../forms/Label/Label' -import { TextInput } from '../forms/TextInput/TextInput' +import { Form, OptionalFormProps } from '../../forms/Form/Form' +import { SearchField } from '../SearchField/SearchField' +import { SearchButton } from '../SearchButton/SearchButton' +import { OptionalTextInputProps } from '../../forms/TextInput/TextInput' type SearchLocalization = { buttonText: string @@ -21,6 +19,7 @@ type SearchInputProps = { placeholder?: string label?: React.ReactNode i18n?: SearchLocalization + inputProps?: OptionalTextInputProps } export const Search = ({ @@ -32,18 +31,12 @@ export const Search = ({ label = 'Search', inputId = 'search-field', i18n, + inputProps, ...formProps }: SearchInputProps & OptionalFormProps): React.ReactElement => { - const buttonText = i18n?.buttonText || 'Search' - const isBig = size === 'big' - const isSmall = size === 'small' const classes = classnames( 'usa-search', - { - 'usa-search--small': isSmall, - 'usa-search--big': isBig, - }, className ) @@ -54,26 +47,16 @@ export const Search = ({ role="search" search={true} {...formProps}> - - - + ) } diff --git a/src/components/Search/SearchButton/SearchButton.stories.tsx b/src/components/Search/SearchButton/SearchButton.stories.tsx new file mode 100644 index 0000000000..0be5a5a2c7 --- /dev/null +++ b/src/components/Search/SearchButton/SearchButton.stories.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { SearchButton } from './SearchButton' + +export default { + title: 'Components/Search/SearchButton', + component: SearchButton, + parameters: { + docs: { + description: { + component: ` +### USWDS 2.0 Search component + +Source: https://designsystem.digital.gov/components/search/ +`, + }, + }, + }, +} + +const sampleLocalization = { + buttonText: 'Buscar', +} + +export const defaultSearchButton = (): React.ReactElement => ( + +) + +export const bigSearchButton = (): React.ReactElement => ( + +) + +export const smallSearch = (): React.ReactElement => ( + +) + +export const defaultSpanishSearchButton = (): React.ReactElement => ( + +) + +export const bigSpanishSearchButton = (): React.ReactElement => ( + +) + +export const smallSpanishSearch = (): React.ReactElement => ( + +) diff --git a/src/components/Search/SearchButton/SearchButton.test.tsx b/src/components/Search/SearchButton/SearchButton.test.tsx new file mode 100644 index 0000000000..bcce588f1c --- /dev/null +++ b/src/components/Search/SearchButton/SearchButton.test.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { SearchButton } from './SearchButton' + +const sampleLocalization = { + buttonText: 'Buscar', +} + +describe('SearchButton component', () => { + it('renders without errors', () => { + const { queryByRole } = render( + + ) + expect(queryByRole('button')).toHaveTextContent('Search') + }) + + it('does not render button text when small', () => { + const { queryByRole } = render( + + ) + + expect(queryByRole('button')).not.toHaveTextContent('Search') + }) + + it('internationalization', () => { + const { queryByText } = render( + + ) + + expect(queryByText('Buscar')).toBeInTheDocument() + }) + + describe('renders size classes', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + it.each([ + ['big', 'usa-search--big'], + ['small', 'usa-search--small'], + ])('when size is %s should include class %s', (sizeString, uswdsClass) => { + const size = sizeString as 'big' | 'small' + const { container } = render() + expect(container.querySelector('div')).toHaveClass(uswdsClass) + }) + }) +}) diff --git a/src/components/Search/SearchButton/SearchButton.tsx b/src/components/Search/SearchButton/SearchButton.tsx new file mode 100644 index 0000000000..612b8105cf --- /dev/null +++ b/src/components/Search/SearchButton/SearchButton.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import classnames from 'classnames' + +import searchImg from '@uswds/uswds/src/img/usa-icons-bg/search--white.svg' + +import { Button } from '../../Button/Button' + +type SearchLocalization = { + buttonText: string +} + +type SearchButtonProps = { + size?: 'big' | 'small' + className?: string + i18n?: SearchLocalization +} + +export const SearchButton = ({ + size, + className, + i18n +}: SearchButtonProps): React.ReactElement => { + const buttonText = i18n?.buttonText || 'Search' + const isSmall = size === 'small' + const isBig = size === 'big' + + const classes = classnames( + { + 'usa-search--small': isSmall, + 'usa-search--big': isBig, + }, + className + ) + return ( +
+ +
+ ) +} + +export default SearchButton diff --git a/src/components/Search/SearchField/SearchField.stories.tsx b/src/components/Search/SearchField/SearchField.stories.tsx new file mode 100644 index 0000000000..1145d02435 --- /dev/null +++ b/src/components/Search/SearchField/SearchField.stories.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import { SearchField } from './SearchField' + +export default { + title: 'Components/Search/SearchField', + component: SearchField, + parameters: { + docs: { + description: { + component: ` +### USWDS 2.0 Search Field component + +Source: https://designsystem.digital.gov/components/search/ +`, + }, + }, + }, +} + + +export const defaultSearchField = (): React.ReactElement => ( + +) + +export const bigSearchField = (): React.ReactElement => ( + +) diff --git a/src/components/Search/SearchField/SearchField.test.tsx b/src/components/Search/SearchField/SearchField.test.tsx new file mode 100644 index 0000000000..cc10138311 --- /dev/null +++ b/src/components/Search/SearchField/SearchField.test.tsx @@ -0,0 +1,58 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { SearchField } from './SearchField' + +describe('SearchField component', () => { + it('renders without errors', () => { + const { queryByTestId } = render( + + ) + expect(queryByTestId('textInput')).toBeInTheDocument() + }) + + it('renders a placeholder', () => { + const placeholder = 'SearchFieldhere' + const { queryByTestId } = render( + + ) + expect(queryByTestId('textInput')).toHaveAttribute( + 'placeholder', + placeholder + ) + }) + + it('renders a default value', () => { + const defaultValue = 'SearchFieldhere' + const { queryByTestId } = render( + + ) + expect(queryByTestId('textInput')).toHaveAttribute( + 'value', + defaultValue + ) + }) + + it('passes input props', () => { + const { getByTestId } = render( + + ) + const input = getByTestId('textInput') + + expect(input).toHaveAttribute('required') + expect(input).toHaveAttribute('minLength', "6") + }) + + it('renders a label', () => { + const { queryByLabelText } = render( + + ) + + expect(queryByLabelText('Buscar')).toBeInTheDocument() + }) + + it('adds big class when isBig is true', () => { + const uswdsClass = 'usa-search--big' + const { container } = render() + expect(container.querySelector('div')).toHaveClass(uswdsClass) + }) +}) diff --git a/src/components/Search/SearchField/SearchField.tsx b/src/components/Search/SearchField/SearchField.tsx new file mode 100644 index 0000000000..4d3741e090 --- /dev/null +++ b/src/components/Search/SearchField/SearchField.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import classnames from 'classnames' + +import { Label } from '../../forms/Label/Label' +import { TextInput, OptionalTextInputProps } from '../../forms/TextInput/TextInput' + + +type SearchFieldProps = { + isBig?: boolean + className?: string + inputName?: string + inputId?: string + placeholder?: string + defaultValue?: React.ReactNode + label?: React.ReactNode + inputProps?: JSX.IntrinsicElements['input'] +} + +export const SearchField = ({ + isBig, + className, + placeholder, + defaultValue, + inputName = 'search', + label = 'Search', + inputId = 'search-field', + inputProps +}: SearchFieldProps & OptionalTextInputProps): React.ReactElement => { + const classes = classnames( + { + 'usa-search--big': isBig, + }, + className + ) + + return ( +
+ + +
+ ) +} + +export default SearchField diff --git a/src/components/forms/TextInput/TextInput.tsx b/src/components/forms/TextInput/TextInput.tsx index 2e87db3077..0da6ba844c 100644 --- a/src/components/forms/TextInput/TextInput.tsx +++ b/src/components/forms/TextInput/TextInput.tsx @@ -19,6 +19,7 @@ type CustomTextInputProps = { validationStatus?: 'error' | 'success' inputSize?: 'small' | 'medium' inputRef?: TextInputRef + inputProps?: JSX.IntrinsicElements['input'] } export type OptionalTextInputProps = CustomTextInputProps & @@ -40,6 +41,7 @@ export const TextInput = ({ const isSuccess = validationStatus === 'success' const isSmall = inputSize === 'small' const isMedium = inputSize === 'medium' + const classes = classnames( 'usa-input', { diff --git a/src/components/header/Header/Header.stories.tsx b/src/components/header/Header/Header.stories.tsx index 35e476e634..546850ea82 100644 --- a/src/components/header/Header/Header.stories.tsx +++ b/src/components/header/Header/Header.stories.tsx @@ -3,7 +3,7 @@ import { Header } from './Header' import { Title } from '../Title/Title' import { PrimaryNav } from '../PrimaryNav/PrimaryNav' -import { Search } from '../../Search/Search' +import { Search } from '../../Search/Search/Search' import { Menu } from '../Menu/Menu' import { MegaMenu } from '../MegaMenu/MegaMenu' import { NavMenuButton } from '../NavMenuButton/NavMenuButton' diff --git a/src/index.ts b/src/index.ts index ecc6e3284b..3650efe281 100644 --- a/src/index.ts +++ b/src/index.ts @@ -139,7 +139,7 @@ export { BreadcrumbLink } from './components/breadcrumb/BreadcrumbLink/Breadcrum export { StepIndicator } from './components/stepindicator/StepIndicator/StepIndicator' export { StepIndicatorStep } from './components/stepindicator/StepIndicatorStep/StepIndicatorStep' -export { Search } from './components/Search/Search' +export { Search } from './components/Search/Search/Search' export { SummaryBox } from './components/SummaryBox/SummaryBox/SummaryBox' export { SummaryBoxHeading } from './components/SummaryBox/SummaryBoxHeading/SummaryBoxHeading' diff --git a/src/stories/templates/documentation.stories.tsx b/src/stories/templates/documentation.stories.tsx index c607a72796..c5f1e8a143 100644 --- a/src/stories/templates/documentation.stories.tsx +++ b/src/stories/templates/documentation.stories.tsx @@ -7,7 +7,7 @@ import { GovBanner, GridContainer, Grid } from '../../index' /** HEADER */ import { Header } from '../../components/header/Header/Header' import { Title } from '../../components/header/Title/Title' -import { Search } from '../../components/header/../Search/Search' +import { Search } from '../../components/header/../Search/Search/Search' import { Menu } from '../../components/header/Menu/Menu' import { NavMenuButton } from '../../components/header/NavMenuButton/NavMenuButton' import { NavDropDownButton } from '../../components/header/NavDropDownButton/NavDropDownButton' diff --git a/src/stories/templates/landing.stories.tsx b/src/stories/templates/landing.stories.tsx index 7dcbd9186c..57d00bfdb6 100644 --- a/src/stories/templates/landing.stories.tsx +++ b/src/stories/templates/landing.stories.tsx @@ -8,7 +8,7 @@ import { GovBanner, GridContainer, Grid, MediaBlockBody } from '../../index' /** HEADER */ import { Header } from '../../components/header/Header/Header' import { Title } from '../../components/header/Title/Title' -import { Search } from '../../components/header/../Search/Search' +import { Search } from '../../components/header/../Search/Search/Search' import { Menu } from '../../components/header/Menu/Menu' import { NavMenuButton } from '../../components/header/NavMenuButton/NavMenuButton' import { NavDropDownButton } from '../../components/header/NavDropDownButton/NavDropDownButton'