diff options
author | 2019-01-03 16:46:47 +0100 | |
---|---|---|
committer | 2019-01-03 16:46:47 +0100 | |
commit | 3fa0b0439c7dc49ffb2c4aff89e5a0f96aacf5e2 (patch) | |
tree | 450b9e21d0bfac2cdde28995a04a3643b3ea2a37 /packages/forms/src | |
parent | Add theme (diff) | |
download | ferdium-app-3fa0b0439c7dc49ffb2c4aff89e5a0f96aacf5e2.tar.gz ferdium-app-3fa0b0439c7dc49ffb2c4aff89e5a0f96aacf5e2.tar.zst ferdium-app-3fa0b0439c7dc49ffb2c4aff89e5a0f96aacf5e2.zip |
Add forms
Diffstat (limited to 'packages/forms/src')
-rw-r--r-- | packages/forms/src/error/index.tsx | 32 | ||||
-rw-r--r-- | packages/forms/src/error/styles.ts | 8 | ||||
-rw-r--r-- | packages/forms/src/index.ts | 1 | ||||
-rw-r--r-- | packages/forms/src/input/index.tsx | 175 | ||||
-rw-r--r-- | packages/forms/src/input/scorePassword.ts | 42 | ||||
-rw-r--r-- | packages/forms/src/input/styles.ts | 98 | ||||
-rw-r--r-- | packages/forms/src/label/index.tsx | 49 | ||||
-rw-r--r-- | packages/forms/src/label/styles.ts | 13 | ||||
-rw-r--r-- | packages/forms/src/typings/generic.ts | 11 | ||||
-rw-r--r-- | packages/forms/src/typings/react-html-attributes.d.ts | 1 | ||||
-rw-r--r-- | packages/forms/src/wrapper/index.tsx | 26 | ||||
-rw-r--r-- | packages/forms/src/wrapper/styles.ts | 5 |
12 files changed, 461 insertions, 0 deletions
diff --git a/packages/forms/src/error/index.tsx b/packages/forms/src/error/index.tsx new file mode 100644 index 000000000..3feaef7f6 --- /dev/null +++ b/packages/forms/src/error/index.tsx | |||
@@ -0,0 +1,32 @@ | |||
1 | import classnames from 'classnames'; | ||
2 | import { Classes } from 'jss'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import React, { Component } from 'react'; | ||
5 | import injectSheet from 'react-jss'; | ||
6 | |||
7 | import styles from './styles'; | ||
8 | |||
9 | interface IProps { | ||
10 | classes: Classes; | ||
11 | message: string; | ||
12 | } | ||
13 | |||
14 | @observer | ||
15 | class Error extends Component<IProps> { | ||
16 | render() { | ||
17 | const { | ||
18 | classes, | ||
19 | message, | ||
20 | } = this.props; | ||
21 | |||
22 | return ( | ||
23 | <p | ||
24 | className={classes.message} | ||
25 | > | ||
26 | {message} | ||
27 | </p> | ||
28 | ); | ||
29 | } | ||
30 | } | ||
31 | |||
32 | export default injectSheet(styles)(Error); | ||
diff --git a/packages/forms/src/error/styles.ts b/packages/forms/src/error/styles.ts new file mode 100644 index 000000000..36a2b24e7 --- /dev/null +++ b/packages/forms/src/error/styles.ts | |||
@@ -0,0 +1,8 @@ | |||
1 | import { Theme } from '../../../theme/lib'; | ||
2 | |||
3 | export default (theme: Theme) => ({ | ||
4 | message: { | ||
5 | color: theme.brandDanger, | ||
6 | margin: '5px 0 0', | ||
7 | }, | ||
8 | }); | ||
diff --git a/packages/forms/src/index.ts b/packages/forms/src/index.ts new file mode 100644 index 000000000..3f24ecaf7 --- /dev/null +++ b/packages/forms/src/index.ts | |||
@@ -0,0 +1 @@ | |||
export { default as Input } from './input'; | |||
diff --git a/packages/forms/src/input/index.tsx b/packages/forms/src/input/index.tsx new file mode 100644 index 000000000..107335573 --- /dev/null +++ b/packages/forms/src/input/index.tsx | |||
@@ -0,0 +1,175 @@ | |||
1 | import { mdiEye, mdiEyeOff } from '@mdi/js'; | ||
2 | import Icon from '@mdi/react'; | ||
3 | import classnames from 'classnames'; | ||
4 | import pick from 'lodash/pick'; | ||
5 | import { observer } from 'mobx-react'; | ||
6 | import React, { Component } from 'react'; | ||
7 | import htmlElementAttributes from 'react-html-attributes'; | ||
8 | import injectSheet from 'react-jss'; | ||
9 | |||
10 | import { IFormField, IWithStyle } from '../typings/generic'; | ||
11 | |||
12 | import Error from '../error'; | ||
13 | import Label from '../label'; | ||
14 | import Wrapper from '../wrapper'; | ||
15 | import scorePasswordFunc from './scorePassword'; | ||
16 | |||
17 | import styles from './styles'; | ||
18 | |||
19 | interface IProps extends IFormField, React.InputHTMLAttributes<HTMLInputElement>, IWithStyle { | ||
20 | label: string; | ||
21 | focus?: boolean; | ||
22 | prefix?: string; | ||
23 | suffix?: string; | ||
24 | scorePassword?: boolean; | ||
25 | showPasswordToggle?: boolean; | ||
26 | error?: string; | ||
27 | } | ||
28 | |||
29 | interface IState { | ||
30 | showPassword: boolean; | ||
31 | passwordScore: number; | ||
32 | } | ||
33 | |||
34 | @observer | ||
35 | class Input extends Component<IProps, IState> { | ||
36 | public static defaultProps = { | ||
37 | classes: {}, | ||
38 | focus: false, | ||
39 | onChange: () => {}, | ||
40 | scorePassword: false, | ||
41 | showLabel: true, | ||
42 | showPasswordToggle: false, | ||
43 | type: 'text', | ||
44 | }; | ||
45 | |||
46 | state = { | ||
47 | passwordScore: 0, | ||
48 | showPassword: false, | ||
49 | }; | ||
50 | |||
51 | private inputRef = React.createRef<HTMLInputElement>(); | ||
52 | |||
53 | componentDidMount() { | ||
54 | const { focus } = this.props; | ||
55 | |||
56 | if (focus && this.inputRef && this.inputRef.current) { | ||
57 | this.inputRef.current.focus(); | ||
58 | } | ||
59 | } | ||
60 | |||
61 | onChange(e: React.ChangeEvent<HTMLInputElement>) { | ||
62 | const { | ||
63 | scorePassword, | ||
64 | onChange, | ||
65 | } = this.props; | ||
66 | |||
67 | if (onChange) { | ||
68 | onChange(e); | ||
69 | } | ||
70 | |||
71 | if (this.inputRef && this.inputRef.current && scorePassword) { | ||
72 | this.setState({ passwordScore: scorePasswordFunc(this.inputRef.current.value) }); | ||
73 | } | ||
74 | } | ||
75 | |||
76 | render() { | ||
77 | const { | ||
78 | classes, | ||
79 | disabled, | ||
80 | error, | ||
81 | id, | ||
82 | label, | ||
83 | prefix, | ||
84 | scorePassword, | ||
85 | suffix, | ||
86 | showLabel, | ||
87 | showPasswordToggle, | ||
88 | type, | ||
89 | } = this.props; | ||
90 | |||
91 | const { | ||
92 | showPassword, | ||
93 | passwordScore, | ||
94 | } = this.state; | ||
95 | |||
96 | const inputProps = pick(this.props, htmlElementAttributes['input']); | ||
97 | |||
98 | if (type === 'password' && showPassword) { | ||
99 | inputProps.type = 'text'; | ||
100 | } | ||
101 | |||
102 | inputProps.onChange = this.onChange.bind(this); | ||
103 | |||
104 | const cssClasses = classnames({ | ||
105 | [`${inputProps.className}`]: inputProps.className, | ||
106 | [`${classes.input}`]: true, | ||
107 | }); | ||
108 | |||
109 | return ( | ||
110 | <Wrapper> | ||
111 | <Label | ||
112 | title={label} | ||
113 | showLabel={showLabel} | ||
114 | htmlFor={id} | ||
115 | > | ||
116 | <div | ||
117 | className={classnames({ | ||
118 | [`${classes.hasPasswordScore}`]: showPasswordToggle, | ||
119 | [`${classes.wrapper}`]: true, | ||
120 | [`${classes.disabled}`]: disabled, | ||
121 | [`${classes.hasError}`]: error, | ||
122 | })}> | ||
123 | {prefix && ( | ||
124 | <span className={classes.prefix}> | ||
125 | {prefix} | ||
126 | </span> | ||
127 | )} | ||
128 | <input | ||
129 | {...inputProps} | ||
130 | className={cssClasses} | ||
131 | ref={this.inputRef} | ||
132 | /> | ||
133 | {suffix && ( | ||
134 | <span className={classes.suffix}> | ||
135 | {suffix} | ||
136 | </span> | ||
137 | )} | ||
138 | {showPasswordToggle && ( | ||
139 | <button | ||
140 | type="button" | ||
141 | className={classes.formModifier} | ||
142 | onClick={() => this.setState(prevState => ({ showPassword: !prevState.showPassword }))} | ||
143 | tabIndex={-1} | ||
144 | > | ||
145 | <Icon | ||
146 | path={!showPassword ? mdiEye : mdiEyeOff} | ||
147 | size={1} | ||
148 | /> | ||
149 | </button> | ||
150 | )} | ||
151 | </div> | ||
152 | {scorePassword && ( | ||
153 | <div className={classnames({ | ||
154 | [`${classes.passwordScore}`]: true, | ||
155 | [`${classes.hasError}`]: error, | ||
156 | })}> | ||
157 | <meter | ||
158 | value={passwordScore < 5 ? 5 : passwordScore} | ||
159 | low={30} | ||
160 | high={75} | ||
161 | optimum={100} | ||
162 | max={100} | ||
163 | /> | ||
164 | </div> | ||
165 | )} | ||
166 | </Label> | ||
167 | {error && ( | ||
168 | <Error message={error} /> | ||
169 | )} | ||
170 | </Wrapper> | ||
171 | ); | ||
172 | } | ||
173 | } | ||
174 | |||
175 | export default injectSheet(styles)(Input); | ||
diff --git a/packages/forms/src/input/scorePassword.ts b/packages/forms/src/input/scorePassword.ts new file mode 100644 index 000000000..bdad7aa28 --- /dev/null +++ b/packages/forms/src/input/scorePassword.ts | |||
@@ -0,0 +1,42 @@ | |||
1 | interface ILetters { | ||
2 | [key: string]: number; | ||
3 | } | ||
4 | |||
5 | interface IVariations { | ||
6 | [index: string]: boolean; | ||
7 | digits: boolean; | ||
8 | lower: boolean; | ||
9 | nonWords: boolean; | ||
10 | upper: boolean; | ||
11 | } | ||
12 | |||
13 | export default function scorePasswordFunc(password: string): number { | ||
14 | let score: number = 0; | ||
15 | if (!password) { | ||
16 | return score; | ||
17 | } | ||
18 | |||
19 | // award every unique letter until 5 repetitions | ||
20 | const letters: ILetters = {}; | ||
21 | for (let i = 0; i < password.length; i += 1) { | ||
22 | letters[password[i]] = (letters[password[i]] || 0) + 1; | ||
23 | score += 5.0 / letters[password[i]]; | ||
24 | } | ||
25 | |||
26 | // bonus points for mixing it up | ||
27 | const variations: IVariations = { | ||
28 | digits: /\d/.test(password), | ||
29 | lower: /[a-z]/.test(password), | ||
30 | nonWords: /\W/.test(password), | ||
31 | upper: /[A-Z]/.test(password), | ||
32 | }; | ||
33 | |||
34 | let variationCount = 0; | ||
35 | Object.keys(variations).forEach((key) => { | ||
36 | variationCount += (variations[key] === true) ? 1 : 0; | ||
37 | }); | ||
38 | |||
39 | score += (variationCount - 1) * 10; | ||
40 | |||
41 | return Math.round(score); | ||
42 | } | ||
diff --git a/packages/forms/src/input/styles.ts b/packages/forms/src/input/styles.ts new file mode 100644 index 000000000..46c0ef701 --- /dev/null +++ b/packages/forms/src/input/styles.ts | |||
@@ -0,0 +1,98 @@ | |||
1 | import * as CSS from 'csstype'; | ||
2 | import { Theme } from '../../../theme/lib'; | ||
3 | |||
4 | const prefixStyles = (theme: Theme) => ({ | ||
5 | background: theme.inputPrefixBackground, | ||
6 | color: theme.inputPrefixColor, | ||
7 | lineHeight: theme.inputHeight, | ||
8 | padding: '0 10px', | ||
9 | }); | ||
10 | |||
11 | export default (theme: Theme) => ({ | ||
12 | container: { | ||
13 | // display: 'flex', | ||
14 | }, | ||
15 | disabled: { | ||
16 | opacity: theme.inputDisabledOpacity, | ||
17 | }, | ||
18 | formModifier: { | ||
19 | background: 'none', | ||
20 | border: 0, | ||
21 | borderLeft: theme.inputBorder, | ||
22 | padding: '4px 20px 0', | ||
23 | outline: 'none', | ||
24 | |||
25 | '&:active': { | ||
26 | opacity: 0.5, | ||
27 | }, | ||
28 | |||
29 | '& svg': { | ||
30 | fill: theme.inputModifierColor, | ||
31 | }, | ||
32 | }, | ||
33 | input: { | ||
34 | background: 'none', | ||
35 | border: 0, | ||
36 | fontSize: theme.inputFontSize, | ||
37 | outline: 'none', | ||
38 | padding: 8, | ||
39 | width: '100%', | ||
40 | color: theme.inputColor, | ||
41 | |||
42 | '&::placeholder': { | ||
43 | color: theme.inputPlaceholderColor, | ||
44 | }, | ||
45 | }, | ||
46 | passwordScore: { | ||
47 | background: theme.inputScorePasswordBackground, | ||
48 | border: theme.inputBorder, | ||
49 | borderTopWidth: 0, | ||
50 | borderBottomLeftRadius: theme.borderRadiusSmall, | ||
51 | borderBottomRightRadius: theme.borderRadiusSmall, | ||
52 | display: 'block', | ||
53 | flexBasis: '100%', | ||
54 | height: 5, | ||
55 | overflow: 'hidden', | ||
56 | |||
57 | '& meter': { | ||
58 | display: 'block', | ||
59 | height: '100%', | ||
60 | width: '100%', | ||
61 | |||
62 | '&::-webkit-meter-bar': { | ||
63 | background: 'none', | ||
64 | }, | ||
65 | |||
66 | '&::-webkit-meter-even-less-good-value': { | ||
67 | background: theme.brandDanger, | ||
68 | }, | ||
69 | |||
70 | '&::-webkit-meter-suboptimum-value': { | ||
71 | background: theme.brandWarning, | ||
72 | }, | ||
73 | |||
74 | '&::-webkit-meter-optimum-value': { | ||
75 | background: theme.brandSuccess, | ||
76 | }, | ||
77 | }, | ||
78 | }, | ||
79 | prefix: prefixStyles(theme), | ||
80 | suffix: prefixStyles(theme), | ||
81 | wrapper: { | ||
82 | background: theme.inputBackground, | ||
83 | border: theme.inputBorder, | ||
84 | borderRadius: theme.borderRadiusSmall, | ||
85 | boxSizing: 'border-box' as CSS.BoxSizingProperty, | ||
86 | display: 'flex', | ||
87 | height: theme.inputHeight, | ||
88 | order: 1, | ||
89 | width: '100%', | ||
90 | }, | ||
91 | hasPasswordScore: { | ||
92 | borderBottomLeftRadius: 0, | ||
93 | borderBottomRightRadius: 0, | ||
94 | }, | ||
95 | hasError: { | ||
96 | borderColor: theme.brandDanger, | ||
97 | }, | ||
98 | }); | ||
diff --git a/packages/forms/src/label/index.tsx b/packages/forms/src/label/index.tsx new file mode 100644 index 000000000..348b820c5 --- /dev/null +++ b/packages/forms/src/label/index.tsx | |||
@@ -0,0 +1,49 @@ | |||
1 | import classnames from 'classnames'; | ||
2 | import { Classes } from 'jss'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import React, { Component } from 'react'; | ||
5 | import injectSheet from 'react-jss'; | ||
6 | |||
7 | import { IFormField } from '../typings/generic'; | ||
8 | |||
9 | import styles from './styles'; | ||
10 | |||
11 | interface ILabel extends IFormField, React.LabelHTMLAttributes<HTMLLabelElement> { | ||
12 | classes: Classes; | ||
13 | } | ||
14 | |||
15 | @observer | ||
16 | class Label extends Component<ILabel> { | ||
17 | static defaultProps = { | ||
18 | showLabel: true, | ||
19 | }; | ||
20 | |||
21 | render() { | ||
22 | const { | ||
23 | title, | ||
24 | showLabel, | ||
25 | classes, | ||
26 | className, | ||
27 | children, | ||
28 | htmlFor, | ||
29 | } = this.props; | ||
30 | |||
31 | return ( | ||
32 | <label | ||
33 | className={classnames({ | ||
34 | [`${className}`]: className, | ||
35 | })} | ||
36 | htmlFor={htmlFor} | ||
37 | > | ||
38 | {showLabel && ( | ||
39 | <span className={classes.label}>{title}</span> | ||
40 | )} | ||
41 | <div className={classes.content}> | ||
42 | {children} | ||
43 | </div> | ||
44 | </label> | ||
45 | ); | ||
46 | } | ||
47 | } | ||
48 | |||
49 | export default injectSheet(styles)(Label); | ||
diff --git a/packages/forms/src/label/styles.ts b/packages/forms/src/label/styles.ts new file mode 100644 index 000000000..64011a93d --- /dev/null +++ b/packages/forms/src/label/styles.ts | |||
@@ -0,0 +1,13 @@ | |||
1 | import { Theme } from '../../../theme/lib'; | ||
2 | |||
3 | export default (theme: Theme) => ({ | ||
4 | content: { | ||
5 | marginTop: 5, | ||
6 | }, | ||
7 | label: { | ||
8 | color: theme.labelColor, | ||
9 | }, | ||
10 | hasError: { | ||
11 | color: theme.brandDanger, | ||
12 | }, | ||
13 | }); | ||
diff --git a/packages/forms/src/typings/generic.ts b/packages/forms/src/typings/generic.ts new file mode 100644 index 000000000..e1e497050 --- /dev/null +++ b/packages/forms/src/typings/generic.ts | |||
@@ -0,0 +1,11 @@ | |||
1 | import { Classes } from 'jss'; | ||
2 | |||
3 | export interface IFormField { | ||
4 | title?: string; | ||
5 | showLabel?: boolean; | ||
6 | isError?: boolean; | ||
7 | } | ||
8 | |||
9 | export interface IWithStyle { | ||
10 | classes: Classes; | ||
11 | } | ||
diff --git a/packages/forms/src/typings/react-html-attributes.d.ts b/packages/forms/src/typings/react-html-attributes.d.ts new file mode 100644 index 000000000..6f8f20fe4 --- /dev/null +++ b/packages/forms/src/typings/react-html-attributes.d.ts | |||
@@ -0,0 +1 @@ | |||
declare module 'react-html-attributes'; | |||
diff --git a/packages/forms/src/wrapper/index.tsx b/packages/forms/src/wrapper/index.tsx new file mode 100644 index 000000000..c2444b0ea --- /dev/null +++ b/packages/forms/src/wrapper/index.tsx | |||
@@ -0,0 +1,26 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import injectStyle from 'react-jss'; | ||
3 | import { IWithStyle } from '../typings/generic'; | ||
4 | |||
5 | import styles from './styles'; | ||
6 | |||
7 | interface IProps extends IWithStyle { | ||
8 | children: React.ReactNode; | ||
9 | } | ||
10 | |||
11 | class Wrapper extends Component<IProps> { | ||
12 | render() { | ||
13 | const { | ||
14 | children, | ||
15 | classes, | ||
16 | } = this.props; | ||
17 | |||
18 | return ( | ||
19 | <div className={classes.container}> | ||
20 | {children} | ||
21 | </div> | ||
22 | ); | ||
23 | } | ||
24 | } | ||
25 | |||
26 | export default injectStyle(styles)(Wrapper); | ||
diff --git a/packages/forms/src/wrapper/styles.ts b/packages/forms/src/wrapper/styles.ts new file mode 100644 index 000000000..72306b252 --- /dev/null +++ b/packages/forms/src/wrapper/styles.ts | |||
@@ -0,0 +1,5 @@ | |||
1 | export default { | ||
2 | container: { | ||
3 | marginBottom: 20, | ||
4 | }, | ||
5 | }; | ||