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
-[](#contributors-)
+[](#contributors-)
[](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 💻 |
+  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'