aboutsummaryrefslogtreecommitdiffstats
path: root/packages/forms/src/input
diff options
context:
space:
mode:
Diffstat (limited to 'packages/forms/src/input')
-rw-r--r--packages/forms/src/input/index.tsx175
-rw-r--r--packages/forms/src/input/scorePassword.ts42
-rw-r--r--packages/forms/src/input/styles.ts98
3 files changed, 315 insertions, 0 deletions
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 @@
1import { mdiEye, mdiEyeOff } from '@mdi/js';
2import Icon from '@mdi/react';
3import classnames from 'classnames';
4import pick from 'lodash/pick';
5import { observer } from 'mobx-react';
6import React, { Component } from 'react';
7import htmlElementAttributes from 'react-html-attributes';
8import injectSheet from 'react-jss';
9
10import { IFormField, IWithStyle } from '../typings/generic';
11
12import Error from '../error';
13import Label from '../label';
14import Wrapper from '../wrapper';
15import scorePasswordFunc from './scorePassword';
16
17import styles from './styles';
18
19interface 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
29interface IState {
30 showPassword: boolean;
31 passwordScore: number;
32}
33
34@observer
35class 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
175export 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 @@
1interface ILetters {
2 [key: string]: number;
3}
4
5interface IVariations {
6 [index: string]: boolean;
7 digits: boolean;
8 lower: boolean;
9 nonWords: boolean;
10 upper: boolean;
11}
12
13export 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 @@
1import * as CSS from 'csstype';
2import { Theme } from '../../../theme/lib';
3
4const prefixStyles = (theme: Theme) => ({
5 background: theme.inputPrefixBackground,
6 color: theme.inputPrefixColor,
7 lineHeight: theme.inputHeight,
8 padding: '0 10px',
9});
10
11export 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});