-
Notifications
You must be signed in to change notification settings - Fork 72
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(search-field): new search field component
- Loading branch information
1 parent
44f3733
commit 138bb67
Showing
10 changed files
with
4,942 additions
and
566 deletions.
There are no files selected for viewing
4,906 changes: 4,350 additions & 556 deletions
4,906
.storybook/__snapshots__/Storyshots.test.js.snap
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# SearchField | ||
|
||
Provides a basic search field component. | ||
|
||
## API | ||
|
||
### `onSubmit` (function; required) | ||
`onSubmit` specifies a callback function for when the `SearchField` is submitted by the user. For example: | ||
|
||
```jsx | ||
<SearchField onSubmit={(value) => { console.log(value); } /> | ||
``` | ||
### `inputLabel` (string, optional) | ||
`inputLabel` specifies the label to use for the input field (e.g., for i18n translations). The default is "Search:". | ||
### `onChange` (function; optional) | ||
`onChange` specifies a callback function for when the value in `SearchField` is changed by the user. The default is an empty function. For example: | ||
```jsx | ||
<SearchField onChange={(value) => { console.log(value); } /> | ||
``` | ||
### `onFocus` (function; optional) | ||
`onFocus` specifies a callback function for when the user focuses in the `SearchField` component. The default is an empty function. For example: | ||
```jsx | ||
<SearchField onFocus={(value) => { console.log(value); } /> | ||
``` | ||
### `onBlur` (function; optional) | ||
`onBlur` specifies a callback function for when the user loses focus in the `SearchField` component. The default is an empty function. For example: | ||
```jsx | ||
<SearchField onBlur={(value) => { console.log(value); }} /> | ||
``` | ||
### `placeholder` (string; optional) | ||
`placeholder` specifies the placeholder text for the input. The default is an empty string. | ||
### `screenReaderText` (object, optional) | ||
`screenReaderText` specifies the screenreader text for both the clear and search buttons (e.g., for i18n translations). The default is `{ clearButton: 'Clear search', searchButton: 'Submit search' }`. | ||
### `value` (string; optional) | ||
`value` specifies the initial value for the input. The default is an empty string. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
@import "~bootstrap/scss/_variables"; | ||
@import "~bootstrap/scss/utilities/_borders"; | ||
@import "~bootstrap/scss/utilities/_spacing"; | ||
@import "~bootstrap/scss/_buttons"; | ||
|
||
$search-field-clear-btn-width: 30px; | ||
$search-field-border-radius: 30px; | ||
|
||
.search-field { | ||
transition: $input-transition; | ||
border-radius: $search-field-border-radius; | ||
box-shadow: $input-box-shadow; | ||
|
||
.form-group { | ||
margin-bottom: 0; | ||
} | ||
|
||
.input-group { | ||
flex: 1 1 0; | ||
} | ||
|
||
label { | ||
margin: 0; | ||
padding-left: 20px; | ||
padding-right: 10px; | ||
} | ||
|
||
button:last-child { | ||
border-radius: $search-field-border-radius; | ||
} | ||
} | ||
|
||
.search-field__focused { | ||
border-color: $blue !important; | ||
box-shadow: $input-focus-box-shadow; | ||
} | ||
|
||
.search-field__input { | ||
background: none; | ||
border: 0; | ||
box-shadow: none !important; | ||
padding-left: 0; | ||
padding-right: 0; | ||
|
||
&:focus { | ||
background: none; | ||
} | ||
} | ||
|
||
.search-field__no-clear-btn { | ||
padding-right: ($search-field-clear-btn-width + 1px + $border-width * 2); | ||
} | ||
|
||
.search-field__search-btn { | ||
border-top: 0; | ||
border-right: 0; | ||
border-bottom: 0; | ||
transition: border-color 0.15s ease-in-out; | ||
padding: 10px 15px 10px 12px; | ||
color: $gray-600; | ||
background: none; | ||
margin-left: 0; | ||
|
||
&:not(:disabled):hover, | ||
&:not(:disabled):focus { | ||
background: #F2F8FB; | ||
color: $blue; | ||
} | ||
} | ||
|
||
.search-field__clear-btn { | ||
width: $search-field-clear-btn-width; | ||
justify-content: center; | ||
color: $gray-600; | ||
border: 0; | ||
background: none; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import React from 'react'; | ||
import { storiesOf } from '@storybook/react'; | ||
import { action } from '@storybook/addon-actions'; | ||
import centered from '@storybook/addon-centered'; | ||
import { checkA11y } from '@storybook/addon-a11y'; | ||
import { withInfo } from '@storybook/addon-info'; | ||
|
||
import SearchField from './index'; | ||
import README from './README.md'; | ||
|
||
storiesOf('SearchField', module) | ||
.addDecorator((story, context) => withInfo({}, README)(story)(context)) | ||
.addDecorator(centered) | ||
.addDecorator(checkA11y) | ||
.add('basic usage', () => ( | ||
<SearchField | ||
onSubmit={action('search-submitted')} | ||
/> | ||
)) | ||
.add('with placeholder', () => ( | ||
<SearchField | ||
onSubmit={action('search-submitted')} | ||
placeholder="Search" | ||
/> | ||
)) | ||
.add('with value', () => ( | ||
<SearchField | ||
onSubmit={action('search-submitted')} | ||
placeholder="Search" | ||
value="foobar" | ||
/> | ||
)) | ||
.add('with callbacks', () => ( | ||
<SearchField | ||
onSubmit={action('search-submitted')} | ||
onChange={action('value-changed')} | ||
onFocus={action('search-focused')} | ||
onBlur={action('search-blurred')} | ||
/> | ||
)) | ||
.add('with custom label and screenreader text', () => ( | ||
<SearchField | ||
onSubmit={action('search-submitted')} | ||
inputLabel="Buscar:" | ||
screenReaderText={{ | ||
clearButton: 'Borrar búsqueda', | ||
searchButton: 'Enviar búsqueda', | ||
}} | ||
/> | ||
)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
/* eslint-disable react/prop-types */ | ||
import React from 'react'; | ||
import { mount } from 'enzyme'; | ||
|
||
import SearchField from './index'; | ||
|
||
const baseProps = { | ||
onSubmit: () => {}, | ||
}; | ||
|
||
describe('<SearchField />', () => { | ||
it('renders', () => { | ||
const props = { | ||
...baseProps, | ||
}; | ||
const wrapper = mount(<SearchField {...props} />); | ||
expect(wrapper.exists()).toEqual(true); | ||
}); | ||
|
||
it('uses passed in value', () => { | ||
const value = 'foofoo'; | ||
const props = { | ||
...baseProps, | ||
value, | ||
}; | ||
const wrapper = mount(<SearchField {...props} />); | ||
expect(wrapper.state('value')).toEqual(value); | ||
expect(wrapper.find('input').prop('value')).toEqual(value); | ||
}); | ||
|
||
it('overrides state value when props value changes', () => { | ||
const initValue = 'foofoo'; | ||
const newValue = 'barbar'; | ||
const props = { | ||
...baseProps, | ||
value: initValue, | ||
}; | ||
const wrapper = mount(<SearchField {...props} />); | ||
expect(wrapper.state('value')).toEqual(initValue); | ||
wrapper.setProps({ | ||
value: newValue, | ||
}); | ||
expect(wrapper.state('value')).toEqual(newValue); | ||
}); | ||
|
||
it('does not override state value when props value changes with existing value', () => { | ||
const value = 'foofoo'; | ||
const props = { | ||
...baseProps, | ||
value, | ||
}; | ||
const wrapper = mount(<SearchField {...props} />); | ||
expect(wrapper.state('value')).toEqual(value); | ||
wrapper.setProps({ | ||
value, | ||
}); | ||
expect(wrapper.state('value')).toEqual(value); | ||
}); | ||
|
||
it('uses passed in placeholder', () => { | ||
const placeholder = 'foofoo'; | ||
const props = { | ||
...baseProps, | ||
placeholder, | ||
}; | ||
const wrapper = mount(<SearchField {...props} />); | ||
expect(wrapper.find('input').prop('placeholder')).toEqual(placeholder); | ||
}); | ||
|
||
it('uses passed in inputLabel text', () => { | ||
const inputLabel = 'Buscar:'; | ||
const props = { | ||
...baseProps, | ||
inputLabel, | ||
}; | ||
const wrapper = mount(<SearchField {...props} />); | ||
expect(wrapper.find('label').text()).toEqual(inputLabel); | ||
}); | ||
|
||
it('uses passed in screenReaderText', () => { | ||
const screenReaderText = { | ||
clearButton: 'Borrar búsqueda', | ||
searchButton: 'Enviar búsqueda', | ||
}; | ||
const props = { | ||
...baseProps, | ||
screenReaderText, | ||
}; | ||
const wrapper = mount(<SearchField {...props} />); | ||
wrapper.find('input').simulate('change', { target: { value: 'foobar' } }); | ||
const searchActionButtons = wrapper.find('Button'); | ||
expect(searchActionButtons.first().find('Icon').prop('screenReaderText')).toEqual(screenReaderText.clearButton); | ||
expect(searchActionButtons.last().find('Icon').prop('screenReaderText')).toEqual(screenReaderText.searchButton); | ||
}); | ||
|
||
describe('fires', () => { | ||
it('focus handler', () => { | ||
const spy = jest.fn(); | ||
const props = { | ||
...baseProps, | ||
onFocus: spy, | ||
}; | ||
const wrapper = mount(<SearchField {...props} />); | ||
wrapper.find('input').simulate('focus'); | ||
expect(spy).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('blur handler', () => { | ||
const spy = jest.fn(); | ||
const props = { | ||
...baseProps, | ||
onBlur: spy, | ||
}; | ||
const wrapper = mount(<SearchField {...props} />); | ||
wrapper.find('input').simulate('blur'); | ||
expect(spy).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('change handler', () => { | ||
const spy = jest.fn(); | ||
const props = { | ||
...baseProps, | ||
onChange: spy, | ||
}; | ||
const wrapper = mount(<SearchField {...props} />); | ||
wrapper.find('input').simulate('change'); | ||
expect(spy).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('submit handler on click', () => { | ||
const spy = jest.fn(); | ||
const props = { | ||
...baseProps, | ||
onSubmit: spy, | ||
}; | ||
const wrapper = mount(<SearchField {...props} />); | ||
wrapper.find('input').simulate('change', { target: { value: 'foobar' } }); | ||
wrapper.find('.input-group-append button').last().simulate('click'); | ||
expect(spy).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('submit handler on enter keypress', () => { | ||
const spy = jest.fn(); | ||
const props = { | ||
...baseProps, | ||
onSubmit: spy, | ||
}; | ||
const wrapper = mount(<SearchField {...props} />); | ||
wrapper.find('input').simulate('change', { target: { value: 'foobar' } }); | ||
wrapper.find('input').simulate('keypress', { key: 'Enter' }); | ||
expect(spy).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
|
||
describe('does not fire', () => { | ||
it('submit handler on non-enter keypress', () => { | ||
const spy = jest.fn(); | ||
const props = { | ||
...baseProps, | ||
onSubmit: spy, | ||
}; | ||
const wrapper = mount(<SearchField {...props} />); | ||
wrapper.find('input').simulate('change', { target: { value: 'foobar' } }); | ||
wrapper.find('input').simulate('keypress', { key: 's' }); | ||
expect(spy).toHaveBeenCalledTimes(0); | ||
}); | ||
}); | ||
|
||
describe('clear button', () => { | ||
it('renders', () => { | ||
const props = { | ||
...baseProps, | ||
}; | ||
const wrapper = mount(<SearchField {...props} />); | ||
wrapper.find('input').simulate('change', { target: { value: 'foobar' } }); | ||
expect(wrapper.find('.input-group-append button').length).toEqual(2); | ||
}); | ||
|
||
it('clears input', () => { | ||
const props = { | ||
...baseProps, | ||
hasClearButton: true, | ||
}; | ||
const wrapper = mount(<SearchField {...props} />); | ||
wrapper.find('input').simulate('change', { target: { value: 'foobar' } }); | ||
expect(wrapper.find('input').prop('value')).toEqual('foobar'); | ||
expect(wrapper.find('.input-group-append button').length).toEqual(2); | ||
wrapper.find('.input-group-append button').first().simulate('click'); | ||
expect(wrapper.find('input').prop('value')).toEqual(''); | ||
expect(wrapper.find('.input-group-append button').length).toEqual(1); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.