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