Skip to content

Commit e3a564c

Browse files
authored
New select input (#49)
* Initial draft - WIP * Refactor SelectList component * Extract refs reducer to a separate fn * Port over missing funtionalities * Bugfixes, add additional tests, CSS cleanup * More CSS updates * Small refactor and add more tests * Extract options into the variable * Final refactor * Make value optional, pass missing prop to select-list and fix css * CSS Updates, clear value from search select on input click etc. * CSS Fixes * Remove `scrollIntoView` functionality * Remove Props export, change method name * Remove constructor from select-list * Change return type of optionsRenderer to `React.ReactNode` * Review #1 * Review: rename `optionsRenderer` -> `renderOptions` * Review: add classname to ul element, introduce `filterBy` * Review: update search-input.tsx and use It in select.tsx * CSS Updates * small css updates
1 parent 938352d commit e3a564c

17 files changed

+1040
-672
lines changed

package-lock.json

+249-292
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/form/form.less

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
}
99
}
1010

11-
input[type='text'], textarea {
11+
.form-control, textarea {
1212
&::placeholder {
1313
color: @gray-darker;
1414
}

src/input/search-input.less

+16-5
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
.search-input {
22
display: flex;
3+
align-items: center;
34
justify-content: space-between;
4-
color: @gray-darker;
55

66
width: 100%;
7+
height: 100%;
8+
padding: 0 10px;
9+
710
background-color: @white;
11+
color: @gray-darker;
812

913
.search-input-icon {
1014
&-filter {
1115
@table-search-bar-icon();
1216
font-size: 14px;
13-
line-height: 30px;
14-
padding: 0 8px;
1517
}
1618

1719
&-clear {
1820
@close-icon();
21+
padding-right: 0;
1922
font-size: 12px;
20-
line-height: 30px;
21-
padding: 0 8px;
2223
background: transparent;
2324
border: 0;
2425
color: @gray-darker;
@@ -35,6 +36,16 @@
3536
box-shadow: none;
3637
font-size: 12px;
3738

39+
&::placeholder {
40+
color: @gray-darkest;
41+
}
42+
43+
&:disabled {
44+
&::placeholder {
45+
color: @gray-light;
46+
}
47+
}
48+
3849
&:focus {
3950
outline: none;
4051
}

src/input/search-input.test.tsx

+106-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe('SearchInput', () => {
1616
const { getByDataQa } = render(
1717
<SearchInput id={id} value={value} onFilterChange={onFilterChange} searchPlaceholder={searchPlaceholder} />
1818
);
19-
const searchInput = getByDataQa(id);
19+
const searchInput = getByDataQa(`search-input-${id}`);
2020

2121
expect(searchInput).toBeTruthy();
2222
});
@@ -30,14 +30,38 @@ describe('SearchInput', () => {
3030
const { getByDataQa } = render(
3131
<SearchInput id={id} value={value} onFilterChange={onFilterChange} searchPlaceholder={searchPlaceholder} />
3232
);
33-
const searchInput = getByDataQa(id);
33+
const searchInput = getByDataQa(`search-input-${id}`);
3434

3535
const newValue = 'foo';
3636
fireEvent.change(searchInput, { target: { value: newValue } });
3737

3838
expect(onFilterChange).toHaveBeenNthCalledWith(1, newValue);
3939
});
4040

41+
it('should NOT invoke `onFilterChange` if disabled', () => {
42+
const id = 'search-input';
43+
const value = '';
44+
const searchPlaceholder = 'search';
45+
const onFilterChange = jest.fn();
46+
const disabled = true;
47+
48+
const { getByDataQa } = render(
49+
<SearchInput
50+
id={id}
51+
value={value}
52+
onFilterChange={onFilterChange}
53+
searchPlaceholder={searchPlaceholder}
54+
disabled={disabled}
55+
/>
56+
);
57+
const searchInput = getByDataQa(`search-input-${id}`);
58+
59+
const newValue = 'foo';
60+
fireEvent.change(searchInput, { target: { value: newValue } });
61+
62+
expect(onFilterChange).not.toHaveBeenCalled();
63+
});
64+
4165
it('should have clear icon disabled if input does not have value', () => {
4266
const id = 'search-input';
4367
const value = '';
@@ -67,4 +91,84 @@ describe('SearchInput', () => {
6791

6892
expect(onFilterChange).toHaveBeenNthCalledWith(1, '');
6993
});
94+
95+
it('should not invoke `onFilterChange` upon clicking clear icon if disabled', () => {
96+
const id = 'search-input';
97+
const value = 'value';
98+
const searchPlaceholder = 'search';
99+
const onFilterChange = jest.fn();
100+
const disabled = true;
101+
102+
const { getByDataQa } = render(
103+
<SearchInput
104+
id={id}
105+
value={value}
106+
onFilterChange={onFilterChange}
107+
searchPlaceholder={searchPlaceholder}
108+
disabled={disabled}
109+
/>
110+
);
111+
const clearIcon = getByDataQa(`${id}-clear`) as HTMLButtonElement;
112+
113+
clearIcon.click();
114+
115+
expect(onFilterChange).not.toHaveBeenCalled();
116+
});
117+
118+
it('should invoke `onClick` handler', () => {
119+
const id = 'search-input';
120+
const value = '';
121+
const searchPlaceholder = 'search';
122+
const onFilterChange = jest.fn();
123+
const onClick = jest.fn();
124+
125+
const { getByDataQa } = render(
126+
<SearchInput
127+
id={id}
128+
value={value}
129+
onFilterChange={onFilterChange}
130+
searchPlaceholder={searchPlaceholder}
131+
onClick={onClick}
132+
/>
133+
);
134+
const searchInput = getByDataQa(`search-input-${id}`);
135+
searchInput.click();
136+
137+
expect(onClick).toHaveBeenCalledTimes(1);
138+
});
139+
140+
it('should have autocomplete turned off by default', () => {
141+
const id = 'search-input';
142+
const value = '';
143+
const searchPlaceholder = 'search';
144+
const onFilterChange = jest.fn();
145+
146+
const { getByDataQa } = render(
147+
<SearchInput id={id} value={value} onFilterChange={onFilterChange} searchPlaceholder={searchPlaceholder} />
148+
);
149+
const searchInput = getByDataQa(`search-input-${id}`) as HTMLInputElement;
150+
151+
expect(searchInput.autocomplete).toEqual('off');
152+
});
153+
154+
it('should contain passed className', () => {
155+
const id = 'search-input';
156+
const value = '';
157+
const searchPlaceholder = 'search';
158+
const onFilterChange = jest.fn();
159+
const className = 'foobar';
160+
161+
const { getByDataQa } = render(
162+
<SearchInput
163+
id={id}
164+
value={value}
165+
onFilterChange={onFilterChange}
166+
searchPlaceholder={searchPlaceholder}
167+
className={className}
168+
/>
169+
);
170+
const searchInput = getByDataQa(`search-input-${id}`);
171+
172+
expect(searchInput.classList.contains(className)).toBeTruthy();
173+
});
70174
});

src/input/search-input.tsx

+22-5
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,49 @@
11
import * as React from 'react';
22

3+
type Autocomplete = 'on' | 'off';
4+
35
interface Props {
46
id: string;
57
value: string;
8+
disabled?: boolean;
9+
className?: string;
610
searchPlaceholder: string;
11+
autoComplete?: Autocomplete;
712
onFilterChange: (filter: string) => void;
13+
onClick?: (event: React.MouseEvent) => void;
814
}
915

1016
const SearchInput: React.FunctionComponent<Props> = (props) => {
11-
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => props.onFilterChange(event.target.value);
12-
const clearSearch = () => props.onFilterChange('');
17+
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
18+
if (!props.disabled) {
19+
props.onFilterChange(event.target.value);
20+
}
21+
};
22+
const clearSearch = () => {
23+
if (!props.disabled) {
24+
props.onFilterChange('');
25+
}
26+
};
1327
return (
1428
<div className="search-input">
1529
<span className="search-input-icon-filter" />
1630
<input
1731
id={props.id}
1832
type="text"
33+
autoComplete={props.autoComplete || 'off'}
1934
value={props.value}
20-
className="search-input-filter"
35+
className={`search-input-filter ${props.className ? props.className : ''}`}
2136
onChange={onChange}
37+
onClick={props.onClick}
2238
placeholder={props.searchPlaceholder}
23-
data-qa={props.id}
39+
data-qa={`search-input-${props.id}`}
40+
disabled={props.disabled}
2441
/>
2542
<button
2643
type="button"
2744
className="search-input-icon-clear"
2845
onClick={clearSearch}
29-
disabled={!props.value.length}
46+
disabled={!props.value.length || props.disabled}
3047
data-qa={`${props.id}-clear`}
3148
/>
3249
</div>

src/multi-level-checkbox-editor/multi-level-checkbox-editor.less

+1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
.multi-level-checkbox-editor-filter {
104104
margin-left: 20px;
105105
width: 350px;
106+
height: 32px;
106107
border: @default-border;
107108
}
108109

src/radio-group/radio-group.test.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { cleanup, render } from '@config/testing';
44

55
import { RadioGroup } from './';
66

7-
const options = [{ label: 'Yes', value: 'yes' }, { label: 'No', value: 'no' }];
7+
const options = [
8+
{ label: 'Yes', value: 'yes' },
9+
{ label: 'No', value: 'no' },
10+
];
811

912
describe('RadioGroup', () => {
1013
afterEach(cleanup);

src/select/index.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
export { Select, StringSelect, NumberSelect } from './select';
1+
export { Select } from './select';
22
export { Option } from '../api/index';
3+
export { RendererProps } from './select-list';

0 commit comments

Comments
 (0)