Skip to content

Commit

Permalink
feat(search-field): new search field component
Browse files Browse the repository at this point in the history
  • Loading branch information
adamstankiewicz committed May 21, 2018
1 parent 44f3733 commit 138bb67
Show file tree
Hide file tree
Showing 10 changed files with 4,942 additions and 566 deletions.
4,906 changes: 4,350 additions & 556 deletions .storybook/__snapshots__/Storyshots.test.js.snap

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/InputText/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ function Text(props) {
placeholder={props.placeholder}
aria-describedby={props.describedBy}
onChange={props.onChange}
onKeyPress={props.onKeyPress}
onBlur={props.onBlur}
aria-invalid={!props.isValid}
autoComplete={props.autoComplete}
Expand Down
45 changes: 45 additions & 0 deletions src/SearchField/README.md
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.
77 changes: 77 additions & 0 deletions src/SearchField/SearchField.scss
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;
}
50 changes: 50 additions & 0 deletions src/SearchField/SearchField.stories.jsx
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',
}}
/>
));
193 changes: 193 additions & 0 deletions src/SearchField/SearchField.test.jsx
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);
});
});
});
Loading

0 comments on commit 138bb67

Please sign in to comment.