homeflowjs 1.0.14 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/search/address-lookup-input/address-lookup-input.component.jsx +205 -0
- package/search/address-lookup-input/clear-button.component.jsx +33 -0
- package/search/address-lookup-input/input.jsx +88 -0
- package/search/index.js +2 -0
- package/shared/close-icon.component.jsx +13 -0
- package/shared/loading-icon.component.jsx +11 -0
- package/shared/search-icon.component.jsx +25 -0
package/package.json
CHANGED
@@ -0,0 +1,205 @@
|
|
1
|
+
import React, {
|
2
|
+
useState, useEffect, useRef, useCallback,
|
3
|
+
} from 'react';
|
4
|
+
import PropTypes from 'prop-types';
|
5
|
+
import debounce from 'lodash.debounce';
|
6
|
+
import Input from './input';
|
7
|
+
|
8
|
+
const DEFAULT_ADDRESS = {
|
9
|
+
value: {},
|
10
|
+
error: null,
|
11
|
+
};
|
12
|
+
|
13
|
+
const AddressLookupInput = ({
|
14
|
+
label,
|
15
|
+
placeholder,
|
16
|
+
className,
|
17
|
+
name,
|
18
|
+
required,
|
19
|
+
setSelectedAddress,
|
20
|
+
}) => {
|
21
|
+
const [locationQuery, setLocationQuery] = useState('');
|
22
|
+
const [suggestions, setSuggestions] = useState([]);
|
23
|
+
const [loading, setLoading] = useState(false);
|
24
|
+
const [address, setAddress] = useState(DEFAULT_ADDRESS);
|
25
|
+
|
26
|
+
const addressLookup = (value) => {
|
27
|
+
setLoading(true);
|
28
|
+
// If no value or is loading just hide suggestions
|
29
|
+
if (!value || loading) setSuggestions([]);
|
30
|
+
|
31
|
+
fetch(`/address_lookup?autocomplete=${value}`)
|
32
|
+
.then(response => response.json())
|
33
|
+
.then((json) => {
|
34
|
+
if (json.suggestions.length === 0) {
|
35
|
+
setAddress({
|
36
|
+
value: address.value,
|
37
|
+
error: 'Sorry we couldn\'t find that address',
|
38
|
+
});
|
39
|
+
}
|
40
|
+
if (json.suggestions.length !== 0) {
|
41
|
+
setAddress({
|
42
|
+
value: address.value,
|
43
|
+
error: null,
|
44
|
+
});
|
45
|
+
setSuggestions(json.suggestions);
|
46
|
+
}
|
47
|
+
setLoading(false);
|
48
|
+
});
|
49
|
+
};
|
50
|
+
|
51
|
+
const debouncedAddressLookup = useCallback(debounce(addressLookup, 500), []);
|
52
|
+
|
53
|
+
const handleQueryChange = (e) => {
|
54
|
+
const { value } = e.target;
|
55
|
+
|
56
|
+
setLocationQuery(value);
|
57
|
+
debouncedAddressLookup(value);
|
58
|
+
};
|
59
|
+
|
60
|
+
const handleAddressSelect = (id, address) => {
|
61
|
+
fetch(`/address_lookup?id=${id}`)
|
62
|
+
.then(response => response.json())
|
63
|
+
.then((json) => {
|
64
|
+
setLocationQuery(address);
|
65
|
+
setAddress({
|
66
|
+
value: json,
|
67
|
+
error: null,
|
68
|
+
});
|
69
|
+
if (setSelectedAddress) {
|
70
|
+
setSelectedAddress(json);
|
71
|
+
}
|
72
|
+
setLoading(false);
|
73
|
+
});
|
74
|
+
};
|
75
|
+
|
76
|
+
// This handles triggering the search
|
77
|
+
const clearSearch = () => {
|
78
|
+
setLocationQuery('');
|
79
|
+
setSuggestions([]);
|
80
|
+
setAddress({
|
81
|
+
value: {},
|
82
|
+
error: null,
|
83
|
+
});
|
84
|
+
};
|
85
|
+
|
86
|
+
// If there is an error remove suggestions
|
87
|
+
useEffect(() => {
|
88
|
+
if (address.error) setSuggestions([]);
|
89
|
+
}, [address.error]);
|
90
|
+
|
91
|
+
|
92
|
+
/**
|
93
|
+
* If loading setSuggestions to an empty array,
|
94
|
+
* this stops the suggestions updating sporadically
|
95
|
+
* as the user is entering a new query
|
96
|
+
*/
|
97
|
+
useEffect(() => {
|
98
|
+
if (loading) setSuggestions([]);
|
99
|
+
}, [loading]);
|
100
|
+
|
101
|
+
/**
|
102
|
+
* This will clear the input and close our dropdown if the user click
|
103
|
+
* outside of the input, this was in its own hook but was causing a
|
104
|
+
* memory leak
|
105
|
+
*/
|
106
|
+
const inputRef = useRef(null);
|
107
|
+
const [clickedOutSide, setClickedOutside] = useState(null);
|
108
|
+
const closeDropdown = () => {
|
109
|
+
if (Object.keys(address.value).length === 0 && !loading) {
|
110
|
+
setLocationQuery('');
|
111
|
+
setSuggestions([]);
|
112
|
+
}
|
113
|
+
};
|
114
|
+
useEffect(() => {
|
115
|
+
// If clicked on outside of element
|
116
|
+
const handleClickOutside = (event) => {
|
117
|
+
/**
|
118
|
+
* Check if the user has clicked outside of the component,
|
119
|
+
* close the dropdown. However,we only ever need to check
|
120
|
+
* this once so setClickedOutside to true after
|
121
|
+
*/
|
122
|
+
if (inputRef.current && !inputRef.current.contains(event.target) && !clickedOutSide) {
|
123
|
+
closeDropdown();
|
124
|
+
setClickedOutside(true);
|
125
|
+
}
|
126
|
+
|
127
|
+
// If the user clicks inside the component just set clickedOutSide to false
|
128
|
+
if (inputRef.current && inputRef.current.contains(event.target) && clickedOutSide) {
|
129
|
+
setClickedOutside(false);
|
130
|
+
}
|
131
|
+
};
|
132
|
+
// Bind the event listener
|
133
|
+
document.addEventListener('click', handleClickOutside);
|
134
|
+
return () => {
|
135
|
+
// Unbind the event listener on clean up
|
136
|
+
document.removeEventListener('click', handleClickOutside);
|
137
|
+
};
|
138
|
+
}, [inputRef, clickedOutSide]);
|
139
|
+
|
140
|
+
return (
|
141
|
+
<div className="address-lookup-input" ref={inputRef}>
|
142
|
+
<Input
|
143
|
+
id="address-lookup-input"
|
144
|
+
name={name}
|
145
|
+
data-key="address_lookup"
|
146
|
+
label={label}
|
147
|
+
placeholder={loading ? 'Loading...' : placeholder}
|
148
|
+
value={locationQuery}
|
149
|
+
onChange={handleQueryChange}
|
150
|
+
onClear={clearSearch}
|
151
|
+
address={address}
|
152
|
+
loading={loading}
|
153
|
+
fullWidth
|
154
|
+
onDark={false}
|
155
|
+
required={required}
|
156
|
+
className={className}
|
157
|
+
// autoComplete false won't in chrome but if we set it to some random value it will work
|
158
|
+
autoComplete="nope"
|
159
|
+
/>
|
160
|
+
{/* Render the search results */}
|
161
|
+
{suggestions.length > 0 && (
|
162
|
+
<ul className="address-lookup-input__results">
|
163
|
+
{suggestions.map(({ address, id }) => (
|
164
|
+
<li
|
165
|
+
className="address-lookup-input__results-item"
|
166
|
+
key={id}
|
167
|
+
>
|
168
|
+
<button
|
169
|
+
type="button"
|
170
|
+
className="address-lookup-input__results-btn btn-unstyled"
|
171
|
+
onClick={() => {
|
172
|
+
handleAddressSelect(id, address);
|
173
|
+
setLoading(true);
|
174
|
+
setSuggestions([]);
|
175
|
+
}}
|
176
|
+
>
|
177
|
+
{address}
|
178
|
+
</button>
|
179
|
+
</li>
|
180
|
+
))}
|
181
|
+
</ul>
|
182
|
+
)}
|
183
|
+
</div>
|
184
|
+
);
|
185
|
+
};
|
186
|
+
|
187
|
+
AddressLookupInput.propTypes = {
|
188
|
+
label: PropTypes.string,
|
189
|
+
placeholder: PropTypes.string,
|
190
|
+
name: PropTypes.string,
|
191
|
+
required: PropTypes.bool,
|
192
|
+
className: PropTypes.string,
|
193
|
+
setSelectedAddress: PropTypes.func,
|
194
|
+
};
|
195
|
+
|
196
|
+
AddressLookupInput.defaultProps = {
|
197
|
+
label: '',
|
198
|
+
placeholder: 'Enter postcode or address',
|
199
|
+
name: 'formatted_address',
|
200
|
+
required: false,
|
201
|
+
className: '',
|
202
|
+
setSelectedAddress: null,
|
203
|
+
};
|
204
|
+
|
205
|
+
export default AddressLookupInput;
|
@@ -0,0 +1,33 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import SearchIcon from '../../shared/search-icon.component';
|
4
|
+
import CloseIcon from '../../shared/close-icon.component';
|
5
|
+
import LoadingIcon from '../../shared/loading-icon.component';
|
6
|
+
|
7
|
+
const ClearButton = ({ address, loading, onClear }) => (
|
8
|
+
<button
|
9
|
+
type="button"
|
10
|
+
className="address-lookup-input__btn btn-unstyled"
|
11
|
+
onClick={onClear}
|
12
|
+
// Disable the button if there is no address
|
13
|
+
disabled={Object.keys(address.value).length === 0}
|
14
|
+
>
|
15
|
+
{Object.keys(address.value).length === 0 && !loading && (
|
16
|
+
<SearchIcon />
|
17
|
+
)}
|
18
|
+
{Object.keys(address.value).length !== 0 && !loading && (
|
19
|
+
<CloseIcon />
|
20
|
+
)}
|
21
|
+
{loading && (
|
22
|
+
<LoadingIcon />
|
23
|
+
)}
|
24
|
+
</button>
|
25
|
+
);
|
26
|
+
|
27
|
+
ClearButton.propTypes = {
|
28
|
+
address: PropTypes.object.isRequired,
|
29
|
+
loading: PropTypes.bool.isRequired,
|
30
|
+
onClear: PropTypes.func.isRequired,
|
31
|
+
};
|
32
|
+
|
33
|
+
export default ClearButton;
|
@@ -0,0 +1,88 @@
|
|
1
|
+
/* eslint-disable jsx-a11y/label-has-for */
|
2
|
+
import React from 'react';
|
3
|
+
import PropTypes from 'prop-types';
|
4
|
+
import ClearButton from './clear-button.component';
|
5
|
+
|
6
|
+
const Input = ({
|
7
|
+
type,
|
8
|
+
id,
|
9
|
+
name,
|
10
|
+
label,
|
11
|
+
value,
|
12
|
+
onChange,
|
13
|
+
onClear,
|
14
|
+
placeholder,
|
15
|
+
address,
|
16
|
+
fullWidth,
|
17
|
+
onDark,
|
18
|
+
iconLeft,
|
19
|
+
iconRight,
|
20
|
+
disabled,
|
21
|
+
className,
|
22
|
+
required,
|
23
|
+
loading,
|
24
|
+
...other
|
25
|
+
}) => (
|
26
|
+
<div className={`input ${address?.error ? 'input--error' : ''} ${fullWidth ? 'input--full-width' : ''} ${onDark ? 'input--on-dark' : ''}`}>
|
27
|
+
{label && (
|
28
|
+
<label className="input__label" htmlFor={id}>{label}</label>
|
29
|
+
)}
|
30
|
+
<div className="input__wrapper">
|
31
|
+
{iconLeft}
|
32
|
+
<input
|
33
|
+
type={type}
|
34
|
+
className={`input__input ${className || ''}`}
|
35
|
+
placeholder={placeholder}
|
36
|
+
id={id}
|
37
|
+
name={name}
|
38
|
+
value={value}
|
39
|
+
onChange={onChange}
|
40
|
+
disabled={disabled}
|
41
|
+
required={required}
|
42
|
+
{...other}
|
43
|
+
/>
|
44
|
+
<ClearButton address={address} loading={loading} onClear={onClear} />
|
45
|
+
</div>
|
46
|
+
{address?.error && typeof (address.error) === 'string' && (
|
47
|
+
<span className="input__error">{address.error}</span>
|
48
|
+
)}
|
49
|
+
</div>
|
50
|
+
);
|
51
|
+
|
52
|
+
Input.propTypes = {
|
53
|
+
type: PropTypes.string,
|
54
|
+
id: PropTypes.string.isRequired,
|
55
|
+
name: PropTypes.string.isRequired,
|
56
|
+
label: PropTypes.string,
|
57
|
+
value: PropTypes.any.isRequired,
|
58
|
+
onChange: PropTypes.func.isRequired,
|
59
|
+
onClear: PropTypes.func.isRequired,
|
60
|
+
placeholder: PropTypes.string,
|
61
|
+
address: PropTypes.object.isRequired,
|
62
|
+
fullWidth: PropTypes.bool,
|
63
|
+
onDark: PropTypes.bool,
|
64
|
+
iconLeft: PropTypes.node,
|
65
|
+
iconRight: PropTypes.node,
|
66
|
+
loading: PropTypes.bool.isRequired,
|
67
|
+
disabled: PropTypes.oneOfType([
|
68
|
+
PropTypes.string,
|
69
|
+
PropTypes.bool,
|
70
|
+
]),
|
71
|
+
className: PropTypes.string,
|
72
|
+
required: PropTypes.bool,
|
73
|
+
};
|
74
|
+
|
75
|
+
Input.defaultProps = {
|
76
|
+
type: 'text',
|
77
|
+
label: '',
|
78
|
+
placeholder: '',
|
79
|
+
fullWidth: false,
|
80
|
+
onDark: true,
|
81
|
+
iconLeft: null,
|
82
|
+
iconRight: null,
|
83
|
+
disabled: false,
|
84
|
+
className: '',
|
85
|
+
required: false,
|
86
|
+
};
|
87
|
+
|
88
|
+
export default Input;
|
package/search/index.js
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import SearchForm from './search-form/search-form.component';
|
2
2
|
import LocationInput from './location-input/location-input.component';
|
3
|
+
import AddressLookupInput from './address-lookup-input/address-lookup-input.component';
|
3
4
|
import BedroomsSelect from './bedrooms-select/bedrooms-select.component';
|
4
5
|
import ChannelRadioButton from './channel-radio-button/channel-radio-button.component';
|
5
6
|
import PriceSelect from './price-select/price-select.component';
|
@@ -17,6 +18,7 @@ import generateSearchDescription from './saved-search/generate-description';
|
|
17
18
|
export {
|
18
19
|
SearchForm,
|
19
20
|
LocationInput,
|
21
|
+
AddressLookupInput,
|
20
22
|
BedroomsSelect,
|
21
23
|
ChannelRadioButton,
|
22
24
|
PriceSelect,
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
const CloseIcon = () => (
|
4
|
+
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="20.52" height="20.52" viewBox="0 0 20.52 20.52">
|
5
|
+
<title>Close Icon</title>
|
6
|
+
<g transform="translate(-336.011 -145.97)">
|
7
|
+
<line x2="19.459" y2="19.459" transform="translate(336.541 146.5)" fill="none" stroke="inherit" strokeWidth="1.5" />
|
8
|
+
<line x1="19.459" y2="19.459" transform="translate(336.541 146.5)" fill="none" stroke="inherit" strokeWidth="1.5" />
|
9
|
+
</g>
|
10
|
+
</svg>
|
11
|
+
);
|
12
|
+
|
13
|
+
export default CloseIcon;
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
const LoadingIcon = () => (
|
4
|
+
<svg role="img" className="loading-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
5
|
+
<title>Loading Icon</title>
|
6
|
+
<path d="M23 4V10H17" stroke="inherit" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
7
|
+
<path d="M20.49 15C19.84 16.8399 18.6096 18.4187 16.9842 19.4985C15.3588 20.5783 13.4265 21.1006 11.4784 20.9866C9.53038 20.8726 7.67216 20.1286 6.18376 18.8667C4.69536 17.6047 3.65743 15.8932 3.22637 13.9901C2.79531 12.0869 2.99448 10.0952 3.79386 8.31508C4.59325 6.53496 5.94954 5.06288 7.65836 4.12065C9.36717 3.17843 11.3359 2.81711 13.268 3.09116C15.2 3.3652 16.9906 4.25975 18.37 5.64001L23 10" stroke="inherit" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
8
|
+
</svg>
|
9
|
+
);
|
10
|
+
|
11
|
+
export default LoadingIcon;
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
|
4
|
+
const SearchIcon = ({ inherit }) => (
|
5
|
+
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="15.97" height="14.197" viewBox="0 0 15.97 14.197">
|
6
|
+
<title>Search Icon</title>
|
7
|
+
<g transform="translate(-1033.533 -21.389)">
|
8
|
+
<g transform="translate(1033.533 21.389)" fill="none" stroke={`${inherit ? 'inherit' : '#fff'}`} strokeWidth="1.5">
|
9
|
+
<ellipse cx="6.805" cy="6.806" rx="6.805" ry="6.806" stroke="none" />
|
10
|
+
<ellipse cx="6.805" cy="6.806" rx="6.055" ry="6.056" fill="none" />
|
11
|
+
</g>
|
12
|
+
<line x1="3.781" y1="3.025" transform="translate(1045.254 31.976)" fill="none" stroke={`${inherit ? 'inherit' : '#fff'}`} strokeWidth="1.5" />
|
13
|
+
</g>
|
14
|
+
</svg>
|
15
|
+
);
|
16
|
+
|
17
|
+
SearchIcon.propTypes = {
|
18
|
+
inherit: PropTypes.bool,
|
19
|
+
};
|
20
|
+
|
21
|
+
SearchIcon.defaultProps = {
|
22
|
+
inherit: false,
|
23
|
+
};
|
24
|
+
|
25
|
+
export default SearchIcon;
|