Commit ef4575c5 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Add FilterSelect component

parent 02d71ce4
import { createElement } from 'preact';
import propTypes from 'prop-types';
import Menu from './menu';
import MenuItem from './menu-item';
import SvgIcon from '../../shared/components/svg-icon';
/**
* @typedef {import('../store/modules/filters').FilterOption} FilterOption
*/
/**
* @typedef FilterSelectProps
* @prop {FilterOption} defaultOption
* @prop {string} [icon]
* @prop {(selectedFilter: FilterOption) => any} onSelect
* @prop {FilterOption[]} options
* @prop {FilterOption} [selectedOption]
* @prop {string} title
*/
/**
* A select-element-like control for selecting one of a defined set of
* options.
*
* @param {FilterSelectProps} props
*/
function FilterSelect({
defaultOption,
icon,
onSelect,
options,
selectedOption,
title,
}) {
const filterOptions = [defaultOption, ...options];
const selected = selectedOption ?? defaultOption;
const menuLabel = (
<span className="filter-select__menu-label">
{icon && <SvgIcon name={icon} className="filter-select__menu-icon" />}
{selected.display}
</span>
);
return (
<Menu label={menuLabel} title={title}>
{filterOptions.map(filterOption => (
<MenuItem
onClick={() => onSelect(filterOption)}
key={filterOption.value}
isSelected={filterOption.value === selected.value}
label={filterOption.display}
/>
))}
</Menu>
);
}
FilterSelect.propTypes = {
defaultOption: propTypes.object,
icon: propTypes.string,
onSelect: propTypes.func,
options: propTypes.array,
selectedOption: propTypes.object,
title: propTypes.string,
};
export default FilterSelect;
import { mount } from 'enzyme';
import { createElement } from 'preact';
import FilterSelect from '../filter-select';
import { $imports } from '../filter-select';
import mockImportedComponents from '../../../test-util/mock-imported-components';
describe('FilterSelect', () => {
let someOptions;
const createComponent = props => {
return mount(
<FilterSelect
defaultOption={{ value: '', display: 'all' }}
onSelect={() => null}
options={someOptions}
title="Select one"
{...props}
/>
);
};
beforeEach(() => {
someOptions = [
{ value: 'onevalue', display: 'One Value' },
{ value: 'twovalue', display: 'Two Value' },
];
$imports.$mock(mockImportedComponents());
});
afterEach(() => {
$imports.$restore();
});
it('should render option display values', () => {
const wrapper = createComponent();
const selectItems = wrapper.find('MenuItem');
assert.equal(selectItems.length, 3);
// First, the default option
assert.deepEqual(selectItems.at(0).props().label, 'all');
// Then the other options
assert.deepEqual(selectItems.at(1).props().label, 'One Value');
assert.deepEqual(selectItems.at(2).props().label, 'Two Value');
});
it('should invoke `onSelect` callback when an option is selected', () => {
const fakeOnSelect = sinon.stub();
const wrapper = createComponent({ onSelect: fakeOnSelect });
const secondOption = wrapper.find('MenuItem').at(1);
secondOption.props().onClick();
assert.calledOnce(fakeOnSelect);
assert.calledWith(
fakeOnSelect,
sinon.match({ value: 'onevalue', display: 'One Value' })
);
});
it('should render provided icon and selected option in label', () => {
const wrapper = createComponent({ icon: 'profile' });
const label = mount(wrapper.find('Menu').props().label);
const icon = label.find('SvgIcon');
assert.isTrue(icon.exists());
assert.equal(icon.props().name, 'profile');
// Default option should be selected as we didn't indicate a selected option
assert.equal(label.text(), 'all');
});
it('should render provided title', () => {
const wrapper = createComponent({ title: 'Select something' });
assert.equal(wrapper.find('Menu').props().title, 'Select something');
});
it('should denote the selected option as selected', () => {
const wrapper = createComponent({
selectedOption: { value: 'twovalue', display: 'Two Value' },
});
const label = mount(wrapper.find('Menu').props().label);
assert.equal(label.text(), 'Two Value');
assert.isFalse(wrapper.find('MenuItem').at(1).props().isSelected);
assert.isTrue(wrapper.find('MenuItem').at(2).props().isSelected);
});
});
@use "../../mixins/utils";
@use "../../variables" as var;
.filter-select {
&__menu-label {
@include utils.font--large;
align-items: center;
color: var.$color-text;
display: flex;
// Prevent label from wrapping if top bar is too narrow to fit all of its
// items.
flex-shrink: 0;
font-weight: bold;
}
&__menu-icon {
@include utils.icon--medium;
margin-right: var.$layout-space--xsmall;
}
}
......@@ -26,6 +26,7 @@
@use './components/autocomplete-list';
@use './components/button';
@use './components/excerpt';
@use './components/filter-select';
@use './components/filter-status';
@use './components/group-list';
@use './components/group-list-item';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment