Skip to content

Commit 49a4df6

Browse files
committed
add searchable select input field
1 parent fd8efc8 commit 49a4df6

File tree

6 files changed

+175
-128
lines changed

6 files changed

+175
-128
lines changed

.env

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
API_URL_A=https://forautobackend.herokuapp.com
2-
API_URL=http://localhost:3030
1+
API_URL=https://forautobackend.herokuapp.com
2+
API_URL_A=http://localhost:3030

components/Forms/parts/BaseVehicleFields.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const BaseVehicleFields: React.FunctionComponent<Props> = ({
6464
<Select
6565
label={t('label.make')}
6666
fluid
67+
searchable
6768
loading={loadingMakes}
6869
placeholder={t('placeholder.make')}
6970
isRequired={includes(requiredFields, fieldTypes.make)}
@@ -75,6 +76,7 @@ const BaseVehicleFields: React.FunctionComponent<Props> = ({
7576
<Select
7677
label={t('label.model')}
7778
fluid
79+
searchable
7880
loading={loadingModels}
7981
placeholder={t('placeholder.model')}
8082
name={fieldTypes.model}

components/Input/Select.js

Lines changed: 123 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable jsx-a11y/no-static-element-interactions */
22
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
3-
import React, { useState, useRef } from 'react'
3+
import React, { useState, useRef, useEffect } from 'react'
44
import PropTypes from 'prop-types'
55
import { Field } from 'redux-form'
66
import {
@@ -22,6 +22,67 @@ import InputErrorText from './parts/InputErrorText'
2222
import InputLabel from './parts/InputLabel'
2323
import styles from './Select.module.scss'
2424

25+
const isEmpty = (val) => isNil(val) || val === '' || val.length === 0
26+
27+
const Content = React.memo(
28+
({
29+
options,
30+
handleOnChange,
31+
searchInputRef,
32+
searchValue,
33+
setSearchValue,
34+
searchable,
35+
placeholder,
36+
loading,
37+
multiple,
38+
value,
39+
}) => {
40+
const Title = () => {
41+
const title =
42+
get(
43+
find(options, (o) => o.value === value),
44+
'label'
45+
) || placeholder
46+
if (!multiple) return <span>{title}</span>
47+
if (!isEmpty(value)) {
48+
return map(options, ({ label: l, value: key }) => {
49+
if (includes(value, key)) {
50+
return (
51+
<BaseButton
52+
key={key}
53+
className={styles.chip}
54+
onClick={() => handleOnChange(filter(value, (v) => v !== key))}
55+
>
56+
<span className={styles.title}>{l}</span>
57+
<CloseIcon />
58+
</BaseButton>
59+
)
60+
}
61+
})
62+
}
63+
return <span>{title}</span>
64+
}
65+
66+
if (loading) return <Loader loading className={styles.loading} />
67+
if (searchable)
68+
return (
69+
<input
70+
ref={searchInputRef}
71+
value={searchValue}
72+
placeholder={placeholder}
73+
className={styles.searchInput}
74+
onChange={(e) => setSearchValue(e.target.value)}
75+
onBlur={() => {
76+
if (value === '') {
77+
setSearchValue('')
78+
}
79+
}}
80+
/>
81+
)
82+
return <Title />
83+
}
84+
)
85+
2586
const SelectComponent = ({
2687
disabled,
2788
loading,
@@ -38,45 +99,32 @@ const SelectComponent = ({
3899
multiple,
39100
isCustom,
40101
onReset,
102+
searchable,
41103
}) => {
42104
const { value } = input
43105
const { error, active } = meta
44106
const [open, setOpen] = useState(false)
107+
const [searchValue, setSearchValue] = useState('')
45108
const ref = useRef(null)
46-
const isEmpty = (val) => isNil(val) || val === '' || val.length === 0
109+
const searchInputRef = useRef(null)
110+
47111
const isActive = (key) => (multiple ? includes(value, key) : value === key)
48112
const handleOnChange = (val) => {
49113
if (input.onChange) input.onChange(val)
50114
if (onChange) onChange(val)
51115
}
52-
const Title = () => {
53-
const title =
54-
get(
55-
find(options, (o) => o.value === value),
56-
'label'
57-
) || placeholder
58-
if (!multiple) return <span>{title}</span>
59-
if (!isEmpty(value)) {
60-
return map(options, ({ label: l, value: key }) => {
61-
if (includes(value, key)) {
62-
return (
63-
<BaseButton
64-
key={key}
65-
className={styles.chip}
66-
onClick={() => handleOnChange(filter(value, (v) => v !== key))}
67-
>
68-
<span className={styles.title}>{l}</span>
69-
<CloseIcon />
70-
</BaseButton>
71-
)
72-
}
73-
})
116+
117+
useEffect(() => {
118+
if (value === '' && searchable) {
119+
setSearchValue('')
74120
}
75-
return <span>{title}</span>
76-
}
121+
if (searchable && searchValue === '' && value && value !== '') {
122+
handleOnChange('')
123+
}
124+
}, [value, searchable])
77125

78126
useOutsideClick({ ref, isOpen: open, setOpen })
79-
const handleOptionClick = (key) => {
127+
const handleOptionClick = (key, label) => {
80128
if (multiple) {
81129
if (includes(value, key)) {
82130
handleOnChange(filter(value, (v) => v !== key))
@@ -87,11 +135,22 @@ const SelectComponent = ({
87135
handleOnChange(key)
88136
}
89137
if (!multiple) setOpen(false)
138+
if (searchable) {
139+
setTimeout(() => {
140+
setSearchValue(label)
141+
setOpen(false)
142+
}, 300)
143+
}
90144
}
91145

92146
const handleOpen = () => {
93147
if (loading || disabled || lodashEmpty(options)) return
94148
setOpen(!open)
149+
if (searchInputRef.current) {
150+
setTimeout(() => {
151+
searchInputRef.current.focus()
152+
}, 600)
153+
}
95154
}
96155

97156
const handleKeyPress = (e) => {
@@ -100,10 +159,11 @@ const SelectComponent = ({
100159
}
101160
}
102161

103-
const Content = () => {
104-
if (loading) return <Loader loading className={styles.loading} />
105-
return <Title />
106-
}
162+
const filteredOptions = !searchable
163+
? options
164+
: filter(options, (option) =>
165+
option.label.toLowerCase().includes(searchValue.toLowerCase())
166+
)
107167

108168
return (
109169
<div
@@ -154,7 +214,18 @@ const SelectComponent = ({
154214
)}
155215
tabIndex="0"
156216
>
157-
<Content />
217+
<Content
218+
options={options}
219+
handleOnChange={handleOnChange}
220+
searchInputRef={searchInputRef}
221+
searchValue={searchValue}
222+
setSearchValue={setSearchValue}
223+
searchable={searchable}
224+
loading={loading}
225+
placeholder={placeholder}
226+
multiple={multiple}
227+
value={value}
228+
/>
158229
<ArrowDownIcon
159230
className={classNames(styles.arrowIcon, open && styles.active)}
160231
/>
@@ -166,18 +237,22 @@ const SelectComponent = ({
166237
<div className={styles.wrapper}>
167238
{open && !isCustom && (
168239
<div className={styles.options}>
169-
{map(options, (option, index) => (
170-
<BaseButton
171-
key={index}
172-
className={classNames(
173-
styles.option,
174-
isActive(option.value) && styles.active
175-
)}
176-
onClick={() => handleOptionClick(option.value)}
177-
>
178-
<span>{option.label}</span>
179-
</BaseButton>
180-
))}
240+
{isEmpty(filteredOptions) ? (
241+
<span className={styles.option}>😢</span>
242+
) : (
243+
map(filteredOptions, (option, index) => (
244+
<BaseButton
245+
key={index}
246+
className={classNames(
247+
styles.option,
248+
isActive(option.value) && styles.active
249+
)}
250+
onClick={() => handleOptionClick(option.value, option.label)}
251+
>
252+
<span>{option.label}</span>
253+
</BaseButton>
254+
))
255+
)}
181256
</div>
182257
)}
183258
</div>
@@ -206,6 +281,7 @@ SelectComponent.propTypes = {
206281
active: PropTypes.bool,
207282
}),
208283
isCustom: PropTypes.bool,
284+
searchable: PropTypes.bool,
209285
placeholder: PropTypes.string,
210286
}
211287

@@ -225,6 +301,7 @@ SelectComponent.defaultProps = {
225301
fluid: false,
226302
loading: false,
227303
name: null,
304+
searchable: false,
228305
disabled: false,
229306
options: [],
230307
multiple: false,
@@ -290,6 +367,7 @@ Select.propTypes = {
290367
onChange: PropTypes.func,
291368
normalize: PropTypes.func,
292369
fluid: PropTypes.bool,
370+
searchable: PropTypes.bool,
293371
label: PropTypes.string,
294372
multiple: PropTypes.bool,
295373
onReset: PropTypes.any,
@@ -302,6 +380,7 @@ Select.defaultProps = {
302380
label: '',
303381
className: null,
304382
visible: true,
383+
searchable: false,
305384
fluid: false,
306385
loading: false,
307386
name: null,

components/Input/Select.module.scss

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@
1212
}
1313
}
1414

15+
.searchInput {
16+
position: absolute;
17+
left: spacing(1.5);
18+
overflow-y: hidden;
19+
overflow-x: auto;
20+
width: calc(100% - 48px);
21+
&::placeholder {
22+
white-space: nowrap;
23+
color: $color-gray;
24+
overflow: hidden;
25+
text-overflow: ellipsis;
26+
}
27+
}
28+
1529
.container {
1630
display: inline-flex;
1731
flex-direction: column;
@@ -27,7 +41,7 @@
2741
border: $border-thick-light-gray;
2842
border-radius: $border-radius-input;
2943
padding: spacing(1.5) spacing(6) spacing(1.5) spacing(1.5);
30-
min-height: 44px;
44+
min-height: 48px;
3145
min-width: 120px;
3246
position: relative;
3347
transition: $transition;

0 commit comments

Comments
 (0)