Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 8759232

Browse files
committedJun 19, 2021
Added search handle field value selection on Enter and other behavior.
1 parent e3adca3 commit 8759232

File tree

4 files changed

+154
-49
lines changed

4 files changed

+154
-49
lines changed
 

‎src/components/SearchHandleField/index.jsx

Lines changed: 138 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,57 @@
1-
import React, { useCallback } from "react";
1+
import React, { useCallback, useState } from "react";
22
import PT from "prop-types";
33
import cn from "classnames";
4-
import AsyncSelect from "react-select/async";
4+
import throttle from "lodash/throttle";
5+
import Select, { components } from "react-select";
56
import { getMemberSuggestions } from "services/teams";
7+
import { useUpdateEffect } from "utils/hooks";
68
import styles from "./styles.module.scss";
79

10+
const loadingMessage = () => "Loading...";
11+
12+
const noOptionsMessage = () => "No suggestions";
13+
14+
function MenuList(props) {
15+
let focusedOption = props.focusedOption;
16+
focusedOption = props.selectProps.isMenuFocused
17+
? focusedOption
18+
: props.getValue()[0];
19+
20+
return <components.MenuList {...props} focusedOption={focusedOption} />;
21+
}
22+
MenuList.propTypes = {
23+
focusedOption: PT.object,
24+
getValue: PT.func,
25+
selectProps: PT.shape({
26+
isMenuFocused: PT.oneOfType([PT.bool, PT.number]),
27+
}),
28+
};
29+
30+
function Option(props) {
31+
return (
32+
<components.Option
33+
{...props}
34+
isFocused={props.selectProps.isMenuFocused && props.isFocused}
35+
isSelected={!props.selectProps.isMenuFocused && props.isSelected}
36+
/>
37+
);
38+
}
39+
Option.propTypes = {
40+
isFocused: PT.bool,
41+
isSelected: PT.bool,
42+
selectProps: PT.shape({
43+
isMenuFocused: PT.oneOfType([PT.bool, PT.number]),
44+
}),
45+
};
46+
847
const selectComponents = {
948
DropdownIndicator: () => null,
49+
ClearIndicator: () => null,
1050
IndicatorSeparator: () => null,
51+
MenuList,
52+
Option,
1153
};
1254

13-
const loadingMessage = () => "Loading...";
14-
15-
const noOptionsMessage = () => "No suggestions";
16-
1755
/**
1856
* Displays search input field.
1957
*
@@ -23,10 +61,9 @@ const noOptionsMessage = () => "No suggestions";
2361
* @param {string} props.placeholder placeholder text
2462
* @param {string} props.name name for input element
2563
* @param {'medium'|'small'} [props.size] field size
26-
* @param {function} props.onChange function called when input value changes
64+
* @param {function} props.onChange function called when value changes
2765
* @param {function} [props.onInputChange] function called when input value changes
28-
* @param {function} [props.onBlur] function called when input is blurred
29-
* @param {function} [props.onMenuClose] function called when option list is closed
66+
* @param {function} [props.onBlur] function called on input blur
3067
* @param {string} props.value input value
3168
* @returns {JSX.Element}
3269
*/
@@ -38,70 +75,139 @@ const SearchHandleField = ({
3875
onChange,
3976
onInputChange,
4077
onBlur,
41-
onMenuClose,
4278
placeholder,
4379
value,
4480
}) => {
45-
const onValueChange = useCallback(
46-
(option, { action }) => {
47-
if (action === "clear") {
48-
onChange("");
49-
} else {
81+
const [inputValue, setInputValue] = useState(value);
82+
const [isLoading, setIsLoading] = useState(false);
83+
const [isMenuOpen, setIsMenuOpen] = useState(false);
84+
const [isMenuFocused, setIsMenuFocused] = useState(false);
85+
const [options, setOptions] = useState([]);
86+
87+
const loadOptions = useCallback(
88+
throttle(
89+
async (value) => {
90+
setIsLoading(true);
91+
const options = await loadSuggestions(value);
92+
setOptions(options);
93+
setIsLoading(false);
94+
setIsMenuOpen(true);
95+
setIsMenuFocused(options.length && options[0].value === value);
96+
},
97+
300,
98+
{ leading: false }
99+
),
100+
[]
101+
);
102+
103+
const onValueChange = (option, { action }) => {
104+
if (action === "input-change" || action === "select-option") {
105+
setIsMenuFocused(false);
106+
setIsMenuOpen(false);
107+
if (!isMenuFocused) {
108+
onChange(inputValue);
109+
} else if (option) {
50110
onChange(option.value);
51111
}
52-
},
53-
[onChange]
54-
);
112+
} else if (action === "clear") {
113+
setIsMenuFocused(false);
114+
setIsMenuOpen(false);
115+
onChange("");
116+
}
117+
};
55118

56119
const onInputValueChange = useCallback(
57120
(value, { action }) => {
58121
if (action === "input-change") {
59-
onInputChange(value);
122+
setIsMenuFocused(false);
123+
setInputValue(value);
124+
onInputChange && onInputChange(value);
125+
loadOptions(value);
60126
}
61127
},
62-
[onInputChange]
128+
[onInputChange, loadOptions]
63129
);
64130

131+
const onKeyDown = (event) => {
132+
const key = event.key;
133+
if (key === "Enter" || key === "Escape") {
134+
setIsMenuFocused(false);
135+
setIsMenuOpen(false);
136+
if (!inputValue) {
137+
onChange(inputValue);
138+
}
139+
} else if (key === "ArrowDown") {
140+
if (!isMenuFocused) {
141+
event.preventDefault();
142+
event.stopPropagation();
143+
setIsMenuFocused(true);
144+
}
145+
} else if (key === "Backspace") {
146+
if (!inputValue) {
147+
event.preventDefault();
148+
event.stopPropagation();
149+
}
150+
}
151+
};
152+
153+
const onSelectBlur = useCallback(() => {
154+
setIsMenuFocused(false);
155+
setIsMenuOpen(false);
156+
onBlur && onBlur();
157+
}, [onBlur]);
158+
159+
useUpdateEffect(() => {
160+
setInputValue(value);
161+
}, [value]);
162+
65163
return (
66164
<div className={cn(styles.container, styles[size], className)}>
67165
<span className={styles.icon} />
68-
<AsyncSelect
166+
<Select
69167
className={styles.select}
70168
classNamePrefix="custom"
71169
components={selectComponents}
72170
id={id}
73171
name={name}
74172
isClearable={true}
75173
isSearchable={true}
76-
// menuIsOpen={true} // for debugging
174+
isLoading={isLoading}
175+
isMenuFocused={isMenuFocused}
176+
menuIsOpen={isMenuOpen}
77177
value={null}
78-
inputValue={value}
178+
inputValue={inputValue}
179+
options={options}
79180
onChange={onValueChange}
80181
onInputChange={onInputValueChange}
81-
onBlur={onBlur}
82-
onMenuClose={onMenuClose}
83-
openMenuOnClick={false}
182+
onKeyDown={onKeyDown}
183+
onBlur={onSelectBlur}
84184
placeholder={placeholder}
85185
noOptionsMessage={noOptionsMessage}
86186
loadingMessage={loadingMessage}
87-
loadOptions={loadSuggestions}
88-
cacheOptions
89187
/>
90188
</div>
91189
);
92190
};
93191

94-
const loadSuggestions = async (inputVal) => {
192+
const loadSuggestions = async (inputValue) => {
95193
let options = [];
96-
if (inputVal.length < 3) {
194+
if (inputValue.length < 3) {
97195
return options;
98196
}
99197
try {
100-
const res = await getMemberSuggestions(inputVal);
198+
const res = await getMemberSuggestions(inputValue);
101199
const users = res.data.result.content;
200+
let match = null;
102201
for (let i = 0, len = users.length; i < len; i++) {
103202
let value = users[i].handle;
104-
options.push({ value, label: value });
203+
if (value === inputValue) {
204+
match = { value, label: value };
205+
} else {
206+
options.push({ value, label: value });
207+
}
208+
}
209+
if (match) {
210+
options.unshift(match);
105211
}
106212
} catch (error) {
107213
console.error(error);
@@ -118,7 +224,6 @@ SearchHandleField.propTypes = {
118224
onChange: PT.func.isRequired,
119225
onInputChange: PT.func,
120226
onBlur: PT.func,
121-
onMenuClose: PT.func,
122227
placeholder: PT.string,
123228
value: PT.oneOfType([PT.number, PT.string]),
124229
};

‎src/components/SearchHandleField/styles.module.scss

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,13 @@ input.input {
157157
}
158158
}
159159

160-
:global(.custom__option--is-focused) {
160+
:global(.custom__option--is-focused),
161+
:global(.custom__option--is-selected) {
161162
background-color: $primary-text-color !important;
162163
color: #fff;
163164
}
165+
166+
:global(.custom__clear-indicator) {
167+
display: none;
168+
}
164169
}

‎src/routes/WorkPeriods/components/PeriodFilters/index.jsx

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
updateQueryFromState,
2020
} from "store/thunks/workPeriods";
2121
import { useUpdateEffect } from "utils/hooks";
22+
import { preventDefault } from "utils/misc";
2223
import styles from "./styles.module.scss";
2324

2425
/**
@@ -42,17 +43,6 @@ const PeriodFilters = ({ className }) => {
4243
[dispatch]
4344
);
4445

45-
const onUserHandleInputChange = useCallback(
46-
(value) => {
47-
dispatch(setWorkPeriodsUserHandle(value));
48-
},
49-
[dispatch]
50-
);
51-
52-
const updateUrlQuery = useCallback(() => {
53-
dispatch(updateQueryFromState());
54-
}, [dispatch]);
55-
5646
const onPaymentStatusesChange = useCallback(
5747
(statuses) => {
5848
dispatch(setWorkPeriodsPaymentStatuses(statuses));
@@ -81,16 +71,17 @@ const PeriodFilters = ({ className }) => {
8171
useUpdateEffect(loadWorkingPeriodsFirstPage, [filters]);
8272

8373
return (
84-
<form className={cn(styles.container, className)} action="#">
74+
<form
75+
className={cn(styles.container, className)}
76+
action="#"
77+
onSubmit={preventDefault}
78+
>
8579
<div className={styles.handleSection}>
8680
<SearchHandleField
8781
id="topcoder-handle"
8882
name="topcoder_handle"
8983
placeholder="Search Topcoder Handle"
9084
onChange={onUserHandleChange}
91-
onInputChange={onUserHandleInputChange}
92-
onBlur={updateUrlQuery}
93-
// onMenuClose={updateUrlQuery}
9485
value={userHandle}
9586
/>
9687
</div>

‎src/utils/misc.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ export function replaceItems(array, map) {
6767
return result;
6868
}
6969

70+
export function preventDefault(event) {
71+
event.preventDefault();
72+
}
73+
7074
/**
7175
* Stops event propagation.
7276
*

0 commit comments

Comments
 (0)
This repository has been archived.