aboutsummaryrefslogtreecommitdiffstats
path: root/packages/forms/src/select/index.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/forms/src/select/index.tsx')
-rw-r--r--packages/forms/src/select/index.tsx437
1 files changed, 437 insertions, 0 deletions
diff --git a/packages/forms/src/select/index.tsx b/packages/forms/src/select/index.tsx
new file mode 100644
index 000000000..4a9e3c56e
--- /dev/null
+++ b/packages/forms/src/select/index.tsx
@@ -0,0 +1,437 @@
1import { mdiArrowRightDropCircleOutline, mdiCloseCircle, mdiMagnify } from '@mdi/js';
2import Icon from '@mdi/react';
3import { Theme } from '@meetfranz/theme';
4import classnames from 'classnames';
5import React, { Component, createRef } from 'react';
6import injectStyle from 'react-jss';
7
8import { IFormField, IWithStyle } from '../typings/generic';
9
10import { NONAME } from 'dns';
11import { Error } from '../error';
12import { Label } from '../label';
13import { Wrapper } from '../wrapper';
14
15interface IOptions {
16 [index: string]: string;
17}
18
19interface IData {
20 [index: string]: string;
21}
22
23interface IProps extends IFormField, IWithStyle {
24 actionText: string;
25 className?: string;
26 inputClassName?: string;
27 defaultValue?: string;
28 disabled?: boolean;
29 id?: string;
30 name: string;
31 options: IOptions;
32 value: string;
33 onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
34 showSearch: boolean;
35 data: IData;
36}
37
38interface IState {
39 open: boolean;
40 value: string;
41 needle: string;
42 selected: number;
43 options: IOptions;
44}
45
46const styles = (theme: Theme) => ({
47 select: {
48 background: theme.selectBackground,
49 border: theme.selectBorder,
50 borderRadius: theme.borderRadiusSmall,
51 height: theme.selectHeight,
52 fontSize: theme.uiFontSize,
53 width: '100%',
54 display: 'flex',
55 alignItems: 'center',
56 textAlign: 'left',
57 color: theme.selectColor,
58 },
59 popup: {
60 opacity: 0,
61 height: 0,
62 overflowX: 'scroll',
63 border: theme.selectBorder,
64 borderTop: 0,
65 transition: 'all 0.3s',
66 },
67 open: {
68 opacity: 1,
69 height: 350,
70 background: theme.selectPopupBackground,
71 },
72 option: {
73 padding: 10,
74 borderBottom: theme.selectOptionBorder,
75 color: theme.selectOptionColor,
76
77 '&:hover': {
78 background: theme.selectOptionItemHover,
79 color: theme.selectOptionItemHoverColor,
80 },
81 '&:active': {
82 background: theme.selectOptionItemActive,
83 color: theme.selectOptionItemActiveColor,
84 },
85 },
86 selected: {
87 background: theme.selectOptionItemActive,
88 color: theme.selectOptionItemActiveColor,
89 },
90 toggle: {
91 marginLeft: 'auto',
92 fill: theme.selectToggleColor,
93 transition: 'transform 0.3s',
94 },
95 toggleOpened: {
96 transform: 'rotateZ(90deg)',
97 },
98 searchContainer: {
99 display: 'flex',
100 background: theme.selectSearchBackground,
101 alignItems: 'center',
102 paddingLeft: 10,
103 color: theme.selectColor,
104
105 '& svg': {
106 fill: theme.selectSearchColor,
107 },
108 },
109 search: {
110 border: 0,
111 width: '100%',
112 fontSize: theme.uiFontSize,
113 background: 'none',
114 marginLeft: 10,
115 padding: [10, 0],
116 color: theme.selectSearchColor,
117 },
118 clearNeedle: {
119 background: 'none',
120 border: 0,
121 },
122 focused: {
123 fontWeight: 'bold',
124 background: theme.selectOptionItemHover,
125 color: theme.selectOptionItemHoverColor,
126 },
127 hasError: {
128 borderColor: theme.brandDanger,
129 },
130 disabled: {
131 opacity: theme.selectDisabledOpacity,
132 },
133});
134
135class SelectComponent extends Component<IProps> {
136 public static defaultProps = {
137 onChange: () => {},
138 showLabel: true,
139 disabled: false,
140 error: '',
141 };
142
143 state = {
144 open: false,
145 value: '',
146 needle: '',
147 selected: 0,
148 options: null,
149 };
150
151 private componentRef = createRef<HTMLDivElement>();
152 private inputRef = createRef<HTMLInputElement>();
153 private searchInputRef = createRef<HTMLInputElement>();
154 private scrollContainerRef = createRef<HTMLDivElement>();
155 private activeOptionRef = createRef<HTMLDivElement>();
156
157 private keyListener: any;
158
159 componentWillReceiveProps(nextProps: IProps) {
160 if (nextProps.value && nextProps.value !== this.props.value) {
161 this.setState({
162 value: nextProps.value,
163 });
164 }
165 }
166
167 componentDidUpdate(prevProps: IProps, prevState: IState) {
168 const {
169 open,
170 } = this.state;
171
172 if (this.searchInputRef && this.searchInputRef.current) {
173 if (open) {
174 this.searchInputRef.current.focus();
175 }
176 }
177 }
178
179 componentDidMount() {
180 if (this.inputRef && this.inputRef.current) {
181 const {
182 data,
183 } = this.props;
184
185 if (data) {
186 Object.keys(data).map(key => this.inputRef.current!.dataset[key] = data[key]);
187 }
188 }
189
190 window.addEventListener('keydown', this.arrowKeysHandler.bind(this), false);
191 }
192
193 componentWillMount() {
194 const {
195 value,
196 } = this.props;
197
198 if (this.componentRef && this.componentRef.current) {
199 this.componentRef.current.removeEventListener('keydown', this.keyListener);
200 }
201
202 if (value) {
203 this.setState({
204 value,
205 });
206 }
207
208 this.setFilter();
209 }
210
211 componentWillUnmount() {
212 window.removeEventListener('keydown', this.arrowKeysHandler.bind(this));
213 }
214
215 setFilter(needle: string = '') {
216 const { options } = this.props;
217
218 let filteredOptions = {};
219 if (needle) {
220 Object.keys(options).map((key) => {
221 if (key.toLocaleLowerCase().startsWith(needle.toLocaleLowerCase()) || options[key].toLocaleLowerCase().startsWith(needle.toLocaleLowerCase())) {
222 Object.assign(filteredOptions, {
223 [`${key}`]: options[key],
224 });
225 }
226 });
227 } else {
228 filteredOptions = options;
229 }
230
231 this.setState({
232 needle,
233 options: filteredOptions,
234 selected: 0,
235 });
236 }
237
238 select(key: string) {
239 this.setState((state: IState) => ({
240 value: key,
241 open: false,
242 }));
243
244 this.setFilter();
245
246 if (this.props.onChange) {
247 this.props.onChange(key as any);
248 }
249 }
250
251 arrowKeysHandler(e: KeyboardEvent) {
252 const {
253 selected,
254 open,
255 options,
256 } = this.state;
257
258 if (!open) return;
259
260 if (e.keyCode === 38 || e.keyCode === 40) {
261 e.preventDefault();
262 }
263
264 if (this.componentRef && this.componentRef.current) {
265 if (e.keyCode === 38 && selected > 0) {
266 this.setState((state: IState) => ({
267 selected: state.selected - 1,
268 }));
269 } else if (e.keyCode === 40 && selected < Object.keys(options!).length - 1) {
270 this.setState((state: IState) => ({
271 selected: state.selected + 1,
272 }));
273 } else if (e.keyCode === 13) {
274 this.select(Object.keys(options!)[selected]);
275 }
276
277 if (this.activeOptionRef && this.activeOptionRef.current && this.scrollContainerRef && this.scrollContainerRef.current) {
278 const containerTopOffset = this.scrollContainerRef.current.offsetTop;
279 const optionTopOffset = this.activeOptionRef.current.offsetTop;
280
281 const topOffset = optionTopOffset - containerTopOffset;
282
283 this.scrollContainerRef.current.scrollTop = topOffset - 35;
284 }
285 }
286
287 switch (e.keyCode){
288 case 37: case 39: case 38: case 40: // Arrow keys
289 case 32: break; // Space
290 default: break; // do not block other keys
291 }
292 }
293
294 render() {
295 const {
296 actionText,
297 classes,
298 className,
299 defaultValue,
300 disabled,
301 error,
302 id,
303 inputClassName,
304 name,
305 label,
306 showLabel,
307 showSearch,
308 onChange,
309 } = this.props;
310
311 const {
312 open,
313 needle,
314 value,
315 selected,
316 options,
317 } = this.state;
318
319 let selection = '';
320 if (!value && defaultValue && options![defaultValue]) {
321 selection = options![defaultValue];
322 } else if (value && options![value]) {
323 selection = options![value];
324 } else {
325 selection = actionText;
326 }
327
328 return (
329 <Wrapper
330 className={className}
331 identifier="franz-select"
332 >
333 <Label
334 title={label}
335 showLabel={showLabel}
336 htmlFor={id}
337 >
338 <div
339 className={classnames({
340 [`${classes.hasError}`]: error,
341 [`${classes.disabled}`]: disabled,
342 })}
343 ref={this.componentRef}
344 >
345 <button
346 type="button"
347 className={classnames({
348 [`${inputClassName}`]: inputClassName,
349 [`${classes.select}`]: true,
350 [`${classes.hasError}`]: error,
351 })}
352 onClick= {!disabled ? () => this.setState((state: IState) => ({
353 open: !state.open,
354 })) : () => {}}
355 >
356 {selection}
357 <Icon
358 path={mdiArrowRightDropCircleOutline}
359 size={0.8}
360 className={classnames({
361 [`${classes.toggle}`]: true,
362 [`${classes.toggleOpened}`]: open,
363 })}
364 />
365 </button>
366 {showSearch && open && (
367 <div className={classes.searchContainer}>
368 <Icon
369 path={mdiMagnify}
370 size={0.8}
371 />
372 <input
373 type="text"
374 value={needle}
375 onChange={e => this.setFilter(e.currentTarget.value)}
376 placeholder="Search"
377 className={classes.search}
378 ref={this.searchInputRef}
379 />
380 {needle && (
381 <button
382 type="button"
383 className={classes.clearNeedle}
384 onClick={() => this.setFilter()}
385 >
386 <Icon
387 path={mdiCloseCircle}
388 size={0.7}
389 />
390 </button>
391 )}
392 </div>
393 )}
394 <div
395 className={classnames({
396 [`${classes.popup}`]: true,
397 [`${classes.open}`]: open,
398 })}
399 ref={this.scrollContainerRef}
400 >
401 {Object.keys(options!).map(((key, i) => (
402 <div
403 key={key}
404 onClick={() => this.select(key)}
405 className={classnames({
406 [`${classes.option}`]: true,
407 [`${classes.selected}`]: options![key] === selection,
408 [`${classes.focused}`]: selected === i,
409 })}
410 onMouseOver={() => this.setState({ selected: i })}
411 ref={selected === i ? this.activeOptionRef : null}
412 >
413 {options![key]}
414 </div>
415 )))}
416 </div>
417 </div>
418 <input
419 className={classes.input}
420 id={id}
421 name={name}
422 type="hidden"
423 defaultValue={value}
424 onChange={onChange}
425 disabled={disabled}
426 ref={this.inputRef}
427 />
428 </Label>
429 {error && (
430 <Error message={error} />
431 )}
432 </Wrapper>
433 );
434 }
435}
436
437export const Select = injectStyle(styles)(SelectComponent);