Skip to content

Commit

Permalink
feat(Search): make search buttons configurable (carbon-design-system#817
Browse files Browse the repository at this point in the history
  • Loading branch information
asudoh authored and tw15egan committed May 3, 2018
1 parent 7065843 commit 6c0206f
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 70 deletions.
21 changes: 21 additions & 0 deletions src/components/Search/Search-story.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import Search from '../Search';
import SearchFilterButton from '../SearchFilterButton';

const searchProps = {
className: 'some-class',
Expand Down Expand Up @@ -87,4 +88,24 @@ storiesOf('Search', module)

return <ControlledSearch />;
}
)
.addWithInfo(
'Custom set of buttons',
`
You can control what set of buttons you want.
`,
() => (
<Search
{...searchProps}
className="some-class"
id="search-1"
labelText="Search"
placeHolderText="Search"
onChange={() => {
console.log('onChange');
action('onChange');
}}>
<SearchFilterButton />
</Search>
)
);
6 changes: 0 additions & 6 deletions src/components/Search/Search-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,6 @@ describe('Search', () => {
const icon = wrapper.find(Icon).at(3);
expect(icon.props().name).toEqual('list');
});

it('should use "grid" icon when format state is not "list"', () => {
wrapper.setState({ format: 'not-list' });
const icon = wrapper.find(Icon).at(3);
expect(icon.props().name).toEqual('grid');
});
});
});

Expand Down
78 changes: 14 additions & 64 deletions src/components/Search/Search.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import classNames from 'classnames';
import Icon from '../Icon';
import SearchFilterButton from '../SearchFilterButton';
import SearchLayoutButton from '../SearchLayoutButton';

export default class Search extends Component {
static propTypes = {
Expand All @@ -24,7 +26,6 @@ export default class Search extends Component {
};

state = {
format: 'list',
hasContent: this.props.value || this.props.defaultValue || false,
};

Expand All @@ -44,18 +45,6 @@ export default class Search extends Component {
this.setState({ hasContent: false }, () => this.input.focus());
};

toggleLayout = () => {
if (this.state.format === 'list') {
this.setState({
format: 'grid',
});
} else {
this.setState({
format: 'list',
});
}
};

handleChange = evt => {
this.setState({
hasContent: evt.target.value !== '',
Expand All @@ -64,55 +53,6 @@ export default class Search extends Component {
this.props.onChange(evt);
};

// eslint-disable-next-line consistent-return
searchFilterBtn = () => {
if (!this.props.small) {
return (
<button
className="bx--search-button"
type="button"
aria-label={this.props.searchButtonLabelText}>
<Icon
name="filter--glyph"
description="filter"
className="bx--search-filter"
/>
</button>
);
}
};

// eslint-disable-next-line consistent-return
searchLayoutBtn = () => {
if (!this.props.small) {
return (
<button
className="bx--search-button"
type="button"
onClick={this.toggleLayout}
aria-label={this.props.layoutButtonLabelText}>
{this.state.format === 'list' ? (
<div className="bx--search__toggle-layout__container">
<Icon
name="list"
description="list"
className="bx--search-view"
/>
</div>
) : (
<div className="bx--search__toggle-layout__container">
<Icon
name="grid"
description="toggle-layout"
className="bx--search-view"
/>
</div>
)}
</button>
);
}
};

render() {
const {
className,
Expand All @@ -124,7 +64,10 @@ export default class Search extends Component {
.substr(2)}`),
placeHolderText,
labelText,
searchButtonLabelText,
layoutButtonLabelText,
small,
children,
...other
} = this.props;

Expand All @@ -142,6 +85,8 @@ export default class Search extends Component {
'bx--search-close--hidden': !hasContent,
});

const renderButtons = !children && !small;

return (
<div className={searchClasses} role="search">
<Icon
Expand Down Expand Up @@ -169,8 +114,13 @@ export default class Search extends Component {
className={clearClasses}
onClick={this.clearInput}
/>
{this.searchFilterBtn()}
{this.searchLayoutBtn()}
{children}
{renderButtons && (
<SearchFilterButton labelText={searchButtonLabelText} />
)}
{renderButtons && (
<SearchLayoutButton labelText={layoutButtonLabelText} />
)}
</div>
);
}
Expand Down
28 changes: 28 additions & 0 deletions src/components/SearchFilterButton/SearchFilterButton-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import Icon from '../Icon';
import SearchFilterButton from '../SearchFilterButton';
import { mount } from 'enzyme';

describe('SearchFilterButton', () => {
const wrapper = mount(<SearchFilterButton labelText="testlabel" />);

describe('buttons', () => {
const btn = wrapper.find('button');

it('should have type="button"', () => {
const type = btn.instance().getAttribute('type');
expect(type).toEqual('button');
});

it('has expected class', () => {
expect(btn.hasClass('bx--search-button')).toEqual(true);
});
});

describe('icons', () => {
it('should use "filter--glyph" icon', () => {
const icon = wrapper.find(Icon);
expect(icon.props().name).toEqual('filter--glyph');
});
});
});
29 changes: 29 additions & 0 deletions src/components/SearchFilterButton/SearchFilterButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from '../Icon';

/**
* The filter button for `<Search>`.
*/
const SearchFilterButton = ({ labelText }) => (
<button className="bx--search-button" type="button" aria-label={labelText}>
<Icon
name="filter--glyph"
description="filter"
className="bx--search-filter"
/>
</button>
);

SearchFilterButton.propTypes = {
/**
* The a11y label text.
*/
labelText: PropTypes.string,
};

SearchFilterButton.defaultProps = {
labelText: 'Search',
};

export default SearchFilterButton;
1 change: 1 addition & 0 deletions src/components/SearchFilterButton/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default from './SearchFilterButton';
56 changes: 56 additions & 0 deletions src/components/SearchLayoutButton/SearchLayoutButton-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';
import Icon from '../Icon';
import SearchLayoutButton from '../SearchLayoutButton';
import { mount } from 'enzyme';

describe('SearchLayoutButton', () => {
const wrapper = mount(<SearchLayoutButton labelText="testlabel" />);

describe('buttons', () => {
const btn = wrapper.find('button');

it('should have type="button"', () => {
const type = btn.instance().getAttribute('type');
expect(type).toEqual('button');
});

it('has expected class for sort button', () => {
expect(btn.hasClass('bx--search-button')).toEqual(true);
});
});

describe('icons', () => {
it('should use "list" icon for toggle button', () => {
const icon = wrapper.find(Icon);
expect(icon.props().name).toEqual('list');
});

it('should use "grid" icon when format state is not "list"', () => {
wrapper.setState({ format: 'not-list' });
const icon = wrapper.find(Icon);
expect(icon.props().name).toEqual('grid');
});

it('should support specifying the layout via props', () => {
const wrapperWithFormatProps = mount(
<SearchLayoutButton format="grid" />
);
expect(wrapperWithFormatProps.find(Icon).props().name).toEqual('grid');
wrapperWithFormatProps.setProps({ format: 'list' });
expect(wrapperWithFormatProps.find(Icon).props().name).toEqual('list');
});

it('should support being notified of change in layout', () => {
const onChangeFormat = jest.fn();
const wrapperWithFormatProps = mount(
<SearchLayoutButton format="grid" onChangeFormat={onChangeFormat} />
);
wrapperWithFormatProps.find('button').simulate('click');
wrapperWithFormatProps.find('button').simulate('click');
expect(onChangeFormat.mock.calls).toEqual([
[{ format: 'list' }],
[{ format: 'grid' }],
]);
});
});
});
84 changes: 84 additions & 0 deletions src/components/SearchLayoutButton/SearchLayoutButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Icon from '../Icon';

/**
* The layout button for `<Search>`.
*/
class SearchLayoutButton extends Component {
static propTypes = {
/**
* The layout.
*/
format: PropTypes.oneOf(['list', 'grid']),

/**
* The a11y label text.
*/
labelText: PropTypes.string,

/**
* The callback called when layout switches.
*/
onChangeFormat: PropTypes.func,
};

static defaultProps = {
labelText: 'Filter',
};

state = {
/**
* The current layout.
* @type {string}
*/
format: this.props.format || 'list',
};

componentWillReceiveProps({ format }) {
const { format: prevFormat } = this.props;
if (prevFormat !== format) {
this.setState({ format: format || 'list' });
}
}

/**
* Toggles the button state upon user-initiated event.
*/
toggleLayout = () => {
const format = this.state.format === 'list' ? 'grid' : 'list';
this.setState({ format }, () => {
const { onChangeFormat } = this.props;
if (typeof onChangeFormat === 'function') {
onChangeFormat({ format });
}
});
};

render() {
const { labelText } = this.props;
return (
<button
className="bx--search-button"
type="button"
onClick={this.toggleLayout}
aria-label={labelText}>
{this.state.format === 'list' ? (
<div className="bx--search__toggle-layout__container">
<Icon name="list" description="list" className="bx--search-view" />
</div>
) : (
<div className="bx--search__toggle-layout__container">
<Icon
name="grid"
description="toggle-layout"
className="bx--search-view"
/>
</div>
)}
</button>
);
}
}

export default SearchLayoutButton;
1 change: 1 addition & 0 deletions src/components/SearchLayoutButton/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default from './SearchLayoutButton';
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ export {
export RadioButton from './components/RadioButton';
export RadioButtonGroup from './components/RadioButtonGroup';
export Search from './components/Search';
export SearchFilterButton from './components/SearchFilterButton';
export SearchLayoutButton from './components/SearchLayoutButton';
export SecondaryButton from './components/SecondaryButton';
export Select from './components/Select';
export SelectItem from './components/SelectItem';
Expand Down

0 comments on commit 6c0206f

Please sign in to comment.