diff --git a/src/components/SearchBar/SearchBar.module.css b/src/components/SearchBar/SearchBar.module.css new file mode 100644 index 000000000..924c31471 --- /dev/null +++ b/src/components/SearchBar/SearchBar.module.css @@ -0,0 +1,11 @@ +/*------------------------------------*\ + # SEARCH FIELD +\*------------------------------------*/ + +/** + * Search bar wrapper for the SearchField and SearchButton + */ +.search-bar { + display: flex; + gap: var(--eds-size-1); +} diff --git a/src/components/SearchBar/SearchBar.stories.tsx b/src/components/SearchBar/SearchBar.stories.tsx new file mode 100644 index 000000000..a10443cae --- /dev/null +++ b/src/components/SearchBar/SearchBar.stories.tsx @@ -0,0 +1,78 @@ +import { BADGE } from '@geometricpanda/storybook-addon-badges'; +import { StoryObj, Meta } from '@storybook/react'; +import React from 'react'; + +import { SearchBar } from '../SearchBar/SearchBar'; + +export default { + title: 'Organisms/Interactive/SearchBar', + component: SearchBar, + parameters: { + badges: [BADGE.BETA], + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} as Meta; + +type Args = React.ComponentProps; + +export const Default: StoryObj = { + args: { + children: ( + <> + + + + ), + }, +}; + +export const Disabled: StoryObj = { + args: { + children: ( + <> + + + + ), + }, + parameters: { + axe: { + disabledRules: ['color-contrast'], + }, + }, +}; + +export const Custom: StoryObj = { + render: () => ( + <> +
+ + +
+
+ + +
+ + ), +}; + +export const SearchField: StoryObj< + React.ComponentProps +> = { + argTypes: { onChange: { action: 'onChange' } }, + render: (args) => , +}; + +export const SearchButton: StoryObj< + React.ComponentProps +> = { + argTypes: { onClick: { action: 'onClick' } }, + render: (args) => , +}; diff --git a/src/components/SearchBar/SearchBar.test.tsx b/src/components/SearchBar/SearchBar.test.tsx new file mode 100644 index 000000000..0e056ecf2 --- /dev/null +++ b/src/components/SearchBar/SearchBar.test.tsx @@ -0,0 +1,6 @@ +import { generateSnapshots } from '@chanzuckerberg/story-utils'; +import * as stories from './SearchBar.stories'; + +describe('', () => { + generateSnapshots(stories); +}); diff --git a/src/components/SearchBar/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx new file mode 100644 index 000000000..efaddc9a7 --- /dev/null +++ b/src/components/SearchBar/SearchBar.tsx @@ -0,0 +1,34 @@ +import clsx from 'clsx'; +import React, { ReactNode } from 'react'; +import styles from './SearchBar.module.css'; +import SearchButton from '../SearchButton'; +import SearchField from '../SearchField'; + +export type Props = { + /** + * SearchBar subcomponents to be wrapped. + */ + children: ReactNode; + /** + * CSS class names that can be appended to the component. + */ + className?: string; +}; + +/** + * BETA: This component is still a work in progress and is subject to change. + * + * ```ts + * import {SearchBar} from "@chanzuckerberg/eds"; + * ``` + * + * Input field and button used for searching through various data fields. + */ +export const SearchBar = ({ children, className }: Props) => { + const componentClassName = clsx(styles['search-bar'], className); + + return
{children}
; +}; + +SearchBar.InputField = SearchField; +SearchBar.Button = SearchButton; diff --git a/src/components/SearchBar/__snapshots__/SearchBar.test.tsx.snap b/src/components/SearchBar/__snapshots__/SearchBar.test.tsx.snap new file mode 100644 index 000000000..3d0807393 --- /dev/null +++ b/src/components/SearchBar/__snapshots__/SearchBar.test.tsx.snap @@ -0,0 +1,223 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` Custom story renders snapshot 1`] = ` +
+
+ +
+ + + + search + + + +
+
+
+
+ + + + search + + + +
+ +
+
+`; + +exports[` Default story renders snapshot 1`] = ` +
+ +
+`; + +exports[` Disabled story renders snapshot 1`] = ` +
+ +
+`; + +exports[` SearchButton story renders snapshot 1`] = ` +
+ +
+`; + +exports[` SearchField story renders snapshot 1`] = ` +
+
+ + + + search + + + +
+
+`; diff --git a/src/components/SearchBar/index.ts b/src/components/SearchBar/index.ts new file mode 100644 index 000000000..ffe57e791 --- /dev/null +++ b/src/components/SearchBar/index.ts @@ -0,0 +1 @@ +export { SearchBar as default } from './SearchBar'; diff --git a/src/components/SearchButton/SearchButton.module.css b/src/components/SearchButton/SearchButton.module.css new file mode 100644 index 000000000..c0c31e9ef --- /dev/null +++ b/src/components/SearchButton/SearchButton.module.css @@ -0,0 +1,14 @@ +/*------------------------------------*\ + # SEARCH BUTTON +\*------------------------------------*/ + +/** + * Button to trigger search in a Search Bar. + */ +.search-button { + padding-top: 1px; + padding-bottom: 1px; + height: var(--eds-size-5); + box-sizing: content-box; + border: 0; +} diff --git a/src/components/SearchButton/SearchButton.tsx b/src/components/SearchButton/SearchButton.tsx new file mode 100644 index 000000000..5f9347c87 --- /dev/null +++ b/src/components/SearchButton/SearchButton.tsx @@ -0,0 +1,38 @@ +import clsx from 'clsx'; +import React, { MouseEventHandler } from 'react'; +import styles from './SearchButton.module.css'; +import Button from '../Button'; + +export type SearchButtonProps = { + /** + * CSS class names that can be appended to the component. + */ + className?: string; + /** + * Disables the button and prevents button use. + */ + disabled?: boolean; + /** + * On click handler for component + */ + onClick?: MouseEventHandler; +}; + +/** + * BETA: This component is still a work in progress and is subject to change. + * + * ```ts + * import {SearchButton} from "@chanzuckerberg/eds"; + * ``` + * + * A button styled for use with the SearchBar. + */ +export const SearchButton = ({ className, ...other }: SearchButtonProps) => { + const componentClassName = clsx(styles['search-button'], className); + + return ( + + ); +}; diff --git a/src/components/SearchButton/index.ts b/src/components/SearchButton/index.ts new file mode 100644 index 000000000..d853af5d0 --- /dev/null +++ b/src/components/SearchButton/index.ts @@ -0,0 +1 @@ +export { SearchButton as default } from './SearchButton'; diff --git a/src/components/SearchField/SearchField.module.css b/src/components/SearchField/SearchField.module.css new file mode 100644 index 000000000..d82c2f2ec --- /dev/null +++ b/src/components/SearchField/SearchField.module.css @@ -0,0 +1,45 @@ +/*------------------------------------*\ + # SEARCH FIELD +\*------------------------------------*/ + +/** + * Search Input Field for gathering input text. + */ +.search-field { + flex: 1; + position: relative; +} + +/** + * The InputField component. + */ +.search-field__input { + padding-left: 2.25rem; +} +.search-field__input::-webkit-search-cancel-button { + display: none; +} + +/** + * The search icon that overlays the InputField. + */ +.search-field__icon { + color: var(--eds-theme-color-icon-neutral-default); + position: absolute; + top: 0.6875rem; + left: 0.625rem; +} + +/** + * Focused variant of the Search Icon. + */ +.search-field__input:focus + .search-field__icon { + color: var(--eds-theme-color-icon-brand-primary); +} + +/** + * Disabled variant of the Search Icon. + */ +.search-field__icon--disabled { + color: var(--eds-theme-color-icon-disabled); +} diff --git a/src/components/SearchField/SearchField.tsx b/src/components/SearchField/SearchField.tsx new file mode 100644 index 000000000..0c6967105 --- /dev/null +++ b/src/components/SearchField/SearchField.tsx @@ -0,0 +1,45 @@ +import clsx from 'clsx'; +import React from 'react'; +import styles from './SearchField.module.css'; +import Icon from '../Icon'; +import TextInput, { TextInputProps } from '../TextInput'; + +/** + * BETA: This component is still a work in progress and is subject to change. + * + * ```ts + * import {SearchField} from "@chanzuckerberg/eds"; + * ``` + * + * A search TextInput component styled for use with the SearchBar. + */ +export const SearchField = ({ + className, + disabled, + ...other +}: TextInputProps) => { + const inputClassName = clsx(styles['search-field__input'], className); + const iconClassName = clsx( + styles['search-field__icon'], + disabled && styles['search-field__icon--disabled'], + ); + + return ( +
+ + +
+ ); +}; diff --git a/src/components/SearchField/index.ts b/src/components/SearchField/index.ts new file mode 100644 index 000000000..8e5e54550 --- /dev/null +++ b/src/components/SearchField/index.ts @@ -0,0 +1 @@ +export { SearchField as default } from './SearchField'; diff --git a/src/components/TextInput/TextInput.module.css b/src/components/TextInput/TextInput.module.css index 9c3605f4c..4fe78ee8e 100644 --- a/src/components/TextInput/TextInput.module.css +++ b/src/components/TextInput/TextInput.module.css @@ -7,7 +7,7 @@ /** * Default text input styles */ -.text-input { +:where(.text-input) { @mixin inputStyles; padding-left: var(--eds-size-2); } diff --git a/src/components/TextInput/TextInput.tsx b/src/components/TextInput/TextInput.tsx index f8b22c0d4..400bc659d 100644 --- a/src/components/TextInput/TextInput.tsx +++ b/src/components/TextInput/TextInput.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx'; import React, { ChangeEventHandler } from 'react'; import styles from './TextInput.module.css'; -export type Props = React.InputHTMLAttributes & { +export type TextInputProps = React.InputHTMLAttributes & { /** * String that describes a type of file that may be selected by the user * https://developer.mozilla.org/en-US/docs/Web/*HTML/Element/input/file#Unique_file_type_specifiers @@ -127,7 +127,7 @@ export const TextInput = ({ id, isError, ...other -}: Props) => { +}: TextInputProps) => { const componentClassName = clsx( styles['text-input'], isError && styles['error'], diff --git a/src/components/TextInput/index.ts b/src/components/TextInput/index.ts index 8f098ddec..0da9ef983 100644 --- a/src/components/TextInput/index.ts +++ b/src/components/TextInput/index.ts @@ -1 +1,2 @@ export { TextInput as default } from './TextInput'; +export type { TextInputProps } from './TextInput'; diff --git a/src/index.ts b/src/index.ts index b48d8ee41..1d83ba758 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,6 +65,9 @@ export { default as Radio } from './components/Radio'; export { default as RadioInput } from './components/RadioInput'; export { default as RadioLabel } from './components/RadioLabel'; export { default as Score } from './components/Score'; +export { default as SearchBar } from './components/SearchBar'; +export { default as SearchButton } from './components/SearchButton'; +export { default as SearchField } from './components/SearchField'; export { default as Section } from './components/Section'; export { default as StackedBlock } from './components/StackedBlock'; export { default as Tab } from './components/Tab';