aboutsummaryrefslogtreecommitdiffstats
path: root/packages/forms
diff options
context:
space:
mode:
authorLibravatar Amine Mouafik <amine@mouafik.fr>2019-05-12 20:00:41 +0700
committerLibravatar Amine Mouafik <amine@mouafik.fr>2019-05-12 20:00:41 +0700
commitd8a1d5f9151cc31f4c2b5c0096a35e49b2c74d61 (patch)
tree3974d449d8ef389fc61bf880ae758b5debc22a80 /packages/forms
parentUse dark background in SVG logo (diff)
parentUpdate CHANGELOG.md (diff)
downloadferdium-app-d8a1d5f9151cc31f4c2b5c0096a35e49b2c74d61.tar.gz
ferdium-app-d8a1d5f9151cc31f4c2b5c0096a35e49b2c74d61.tar.zst
ferdium-app-d8a1d5f9151cc31f4c2b5c0096a35e49b2c74d61.zip
Merge tag 'v5.1.0'
# Conflicts: # README.md # src/components/layout/AppLayout.js
Diffstat (limited to 'packages/forms')
-rw-r--r--packages/forms/.gitignore2
-rw-r--r--packages/forms/package-lock.json220
-rw-r--r--packages/forms/package.json39
-rw-r--r--packages/forms/src/button/index.tsx275
-rw-r--r--packages/forms/src/error/index.tsx29
-rw-r--r--packages/forms/src/error/styles.ts9
-rw-r--r--packages/forms/src/index.ts4
-rw-r--r--packages/forms/src/input/index.tsx211
-rw-r--r--packages/forms/src/input/scorePassword.ts42
-rw-r--r--packages/forms/src/input/styles.ts101
-rw-r--r--packages/forms/src/label/index.tsx51
-rw-r--r--packages/forms/src/label/styles.ts12
-rw-r--r--packages/forms/src/select/index.tsx445
-rw-r--r--packages/forms/src/toggle/index.tsx117
-rw-r--r--packages/forms/src/typings/generic.ts17
-rw-r--r--packages/forms/src/wrapper/index.tsx37
-rw-r--r--packages/forms/src/wrapper/styles.ts5
-rw-r--r--packages/forms/tsconfig.json12
-rw-r--r--packages/forms/tslint.json3
19 files changed, 1631 insertions, 0 deletions
diff --git a/packages/forms/.gitignore b/packages/forms/.gitignore
new file mode 100644
index 000000000..d01826a6b
--- /dev/null
+++ b/packages/forms/.gitignore
@@ -0,0 +1,2 @@
1node_modules/
2lib
diff --git a/packages/forms/package-lock.json b/packages/forms/package-lock.json
new file mode 100644
index 000000000..b5b24c239
--- /dev/null
+++ b/packages/forms/package-lock.json
@@ -0,0 +1,220 @@
1{
2 "name": "@meetfranz/forms",
3 "version": "1.0.6",
4 "lockfileVersion": 1,
5 "requires": true,
6 "dependencies": {
7 "@mdi/js": {
8 "version": "3.3.92",
9 "resolved": "https://registry.npmjs.org/@mdi/js/-/js-3.3.92.tgz",
10 "integrity": "sha512-l+12IwTycHlijWMiRWBAssm0RSgkQiwMthIy/EcBAdSqtnsHnFjHq+aI2MBZ8/AYX0QBxNUv4+EN0SXZgNkWDg=="
11 },
12 "@mdi/react": {
13 "version": "1.1.0",
14 "resolved": "https://registry.npmjs.org/@mdi/react/-/react-1.1.0.tgz",
15 "integrity": "sha512-c0+avMYEZ6i7Pg1ULLFs+p7k8bDPiie9rrgGYs8VWQhw2tUUYz7r0lIPVzD3bzMghWfyhfkArj88K5Of0WTMNw=="
16 },
17 "@meetfranz/theme": {
18 "version": "file:../theme",
19 "requires": {
20 "color": "^3.1.0"
21 },
22 "dependencies": {
23 "color": {
24 "version": "3.1.0",
25 "bundled": true,
26 "requires": {
27 "color-convert": "^1.9.1",
28 "color-string": "^1.5.2"
29 }
30 },
31 "color-convert": {
32 "version": "1.9.3",
33 "bundled": true,
34 "requires": {
35 "color-name": "1.1.3"
36 }
37 },
38 "color-name": {
39 "version": "1.1.3",
40 "bundled": true
41 },
42 "color-string": {
43 "version": "1.5.3",
44 "bundled": true,
45 "requires": {
46 "color-name": "^1.0.0",
47 "simple-swizzle": "^0.2.2"
48 }
49 },
50 "is-arrayish": {
51 "version": "0.3.2",
52 "bundled": true
53 },
54 "simple-swizzle": {
55 "version": "0.2.2",
56 "bundled": true,
57 "requires": {
58 "is-arrayish": "^0.3.1"
59 }
60 }
61 }
62 },
63 "asap": {
64 "version": "2.0.6",
65 "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
66 "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
67 },
68 "core-js": {
69 "version": "1.2.7",
70 "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
71 "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
72 },
73 "create-react-class": {
74 "version": "15.6.3",
75 "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz",
76 "integrity": "sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==",
77 "requires": {
78 "fbjs": "^0.8.9",
79 "loose-envify": "^1.3.1",
80 "object-assign": "^4.1.1"
81 }
82 },
83 "encoding": {
84 "version": "0.1.12",
85 "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
86 "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
87 "requires": {
88 "iconv-lite": "~0.4.13"
89 }
90 },
91 "fbjs": {
92 "version": "0.8.17",
93 "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz",
94 "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=",
95 "requires": {
96 "core-js": "^1.0.0",
97 "isomorphic-fetch": "^2.1.1",
98 "loose-envify": "^1.0.0",
99 "object-assign": "^4.1.0",
100 "promise": "^7.1.1",
101 "setimmediate": "^1.0.5",
102 "ua-parser-js": "^0.7.18"
103 }
104 },
105 "html-element-attributes": {
106 "version": "1.3.1",
107 "resolved": "https://registry.npmjs.org/html-element-attributes/-/html-element-attributes-1.3.1.tgz",
108 "integrity": "sha512-UrRKgp5sQmRnDy4TEwAUsu14XBUlzKB8U3hjIYDjcZ3Hbp86Jtftzxfgrv6E/ii/h78tsaZwAnAE8HwnHr0dPA=="
109 },
110 "iconv-lite": {
111 "version": "0.4.24",
112 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
113 "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
114 "requires": {
115 "safer-buffer": ">= 2.1.2 < 3"
116 }
117 },
118 "is-stream": {
119 "version": "1.1.0",
120 "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
121 "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
122 },
123 "isomorphic-fetch": {
124 "version": "2.2.1",
125 "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
126 "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
127 "requires": {
128 "node-fetch": "^1.0.1",
129 "whatwg-fetch": ">=0.10.0"
130 }
131 },
132 "js-tokens": {
133 "version": "4.0.0",
134 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
135 "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
136 },
137 "loose-envify": {
138 "version": "1.4.0",
139 "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
140 "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
141 "requires": {
142 "js-tokens": "^3.0.0 || ^4.0.0"
143 }
144 },
145 "node-fetch": {
146 "version": "1.7.3",
147 "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
148 "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
149 "requires": {
150 "encoding": "^0.1.11",
151 "is-stream": "^1.0.1"
152 }
153 },
154 "object-assign": {
155 "version": "4.1.1",
156 "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
157 "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
158 },
159 "promise": {
160 "version": "7.3.1",
161 "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
162 "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
163 "requires": {
164 "asap": "~2.0.3"
165 }
166 },
167 "prop-types": {
168 "version": "15.6.2",
169 "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
170 "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
171 "requires": {
172 "loose-envify": "^1.3.1",
173 "object-assign": "^4.1.1"
174 }
175 },
176 "react-html-attributes": {
177 "version": "1.4.3",
178 "resolved": "https://registry.npmjs.org/react-html-attributes/-/react-html-attributes-1.4.3.tgz",
179 "integrity": "sha1-jDbDX85rdQk40oavQo7R2nYlGG4=",
180 "requires": {
181 "html-element-attributes": "^1.0.0"
182 }
183 },
184 "react-loader": {
185 "version": "2.4.5",
186 "resolved": "https://registry.npmjs.org/react-loader/-/react-loader-2.4.5.tgz",
187 "integrity": "sha1-zT5VHGzQc4wcDxPwc2VPk4KL5ak=",
188 "requires": {
189 "create-react-class": "^15.5.2",
190 "prop-types": "^15.5.8",
191 "spin.js": "2.x"
192 }
193 },
194 "safer-buffer": {
195 "version": "2.1.2",
196 "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
197 "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
198 },
199 "setimmediate": {
200 "version": "1.0.5",
201 "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
202 "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
203 },
204 "spin.js": {
205 "version": "2.3.2",
206 "resolved": "https://registry.npmjs.org/spin.js/-/spin.js-2.3.2.tgz",
207 "integrity": "sha1-bKpW1SBnNFD9XPvGlx5tB3LDeho="
208 },
209 "ua-parser-js": {
210 "version": "0.7.19",
211 "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.19.tgz",
212 "integrity": "sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ=="
213 },
214 "whatwg-fetch": {
215 "version": "3.0.0",
216 "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
217 "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
218 }
219 }
220}
diff --git a/packages/forms/package.json b/packages/forms/package.json
new file mode 100644
index 000000000..0ac9a846f
--- /dev/null
+++ b/packages/forms/package.json
@@ -0,0 +1,39 @@
1{
2 "name": "@meetfranz/forms",
3 "version": "1.0.15",
4 "description": "React form components for Franz",
5 "main": "lib/index.js",
6 "scripts": {
7 "dev": "tsc -w",
8 "prepare": "tsc"
9 },
10 "publishConfig": {
11 "access": "public"
12 },
13 "repository": {
14 "type": "git",
15 "url": "git+https://github.com/meetfranz/franz.git"
16 },
17 "keywords": [
18 "Franz",
19 "Forms",
20 "React",
21 "UI"
22 ],
23 "author": "Stefan Malzner <stefan@adlk.io>",
24 "license": "Apache-2.0",
25 "dependencies": {
26 "@mdi/js": "^3.3.92",
27 "@mdi/react": "^1.1.0",
28 "@meetfranz/theme": "^1.0.13",
29 "react-html-attributes": "^1.4.3",
30 "react-loader": "^2.4.5"
31 },
32 "peerDependencies": {
33 "classnames": "^2.2.6",
34 "react": "^16.7.0",
35 "react-dom": "16.7.0",
36 "react-jss": "^8.6.1"
37 },
38 "gitHead": "e9b9079dc921e85961954727a7b2a8eabe5b9798"
39}
diff --git a/packages/forms/src/button/index.tsx b/packages/forms/src/button/index.tsx
new file mode 100644
index 000000000..6959cde73
--- /dev/null
+++ b/packages/forms/src/button/index.tsx
@@ -0,0 +1,275 @@
1import * as mdiIcons from '@mdi/js';
2import Icon from '@mdi/react';
3import { Theme } from '@meetfranz/theme';
4import classnames from 'classnames';
5import CSS from 'csstype';
6import React, { Component } from 'react';
7import injectStyle, { withTheme } from 'react-jss';
8import Loader from 'react-loader';
9
10import { IFormField, IWithStyle } from '../typings/generic';
11
12type ButtonType = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'inverted';
13
14interface IProps extends IFormField, IWithStyle {
15 className?: string;
16 disabled?: boolean;
17 id?: string;
18 type?: string;
19 onClick: (event: React.MouseEvent<HTMLButtonElement> | React.MouseEvent<HTMLAnchorElement>) => void;
20 buttonType?: ButtonType;
21 stretch?: boolean;
22 loaded?: boolean;
23 busy?: boolean;
24 icon?: keyof typeof mdiIcons;
25 href?: string;
26 target?: string;
27}
28
29interface IState {
30 busy: boolean;
31}
32
33const styles = (theme: Theme) => ({
34 button: {
35 borderRadius: theme.borderRadiusSmall,
36 border: 'none',
37 display: 'inline-flex',
38 position: 'relative' as CSS.PositionProperty,
39 transition: 'background .5s, opacity 0.3s',
40 textAlign: 'center' as CSS.TextAlignProperty,
41 outline: 'none',
42 alignItems: 'center',
43 padding: 0,
44 width: (props: IProps) => (props.stretch ? '100%' : 'auto') as CSS.WidthProperty<string>,
45 fontSize: theme.uiFontSize,
46 textDecoration: 'none',
47 height: theme.buttonHeight,
48
49 '&:hover': {
50 opacity: 0.8,
51 },
52 '&:active': {
53 opacity: 0.5,
54 transition: 'none',
55 },
56 },
57 label: {
58 margin: '10px 20px',
59 width: '100%',
60 display: 'flex',
61 alignItems: 'center',
62 justifyContent: 'center',
63 },
64 primary: {
65 background: theme.buttonPrimaryBackground,
66 color: theme.buttonPrimaryTextColor,
67
68 '& svg': {
69 fill: theme.buttonPrimaryTextColor,
70 },
71 },
72 secondary: {
73 background: theme.buttonSecondaryBackground,
74 color: theme.buttonSecondaryTextColor,
75
76 '& svg': {
77 fill: theme.buttonSecondaryTextColor,
78 },
79 },
80 success: {
81 background: theme.buttonSuccessBackground,
82 color: theme.buttonSuccessTextColor,
83
84 '& svg': {
85 fill: theme.buttonSuccessTextColor,
86 },
87 },
88 danger: {
89 background: theme.buttonDangerBackground,
90 color: theme.buttonDangerTextColor,
91
92 '& svg': {
93 fill: theme.buttonDangerTextColor,
94 },
95 },
96 warning: {
97 background: theme.buttonWarningBackground,
98 color: theme.buttonWarningTextColor,
99
100 '& svg': {
101 fill: theme.buttonWarningTextColor,
102 },
103 },
104 inverted: {
105 background: theme.buttonInvertedBackground,
106 color: theme.buttonInvertedTextColor,
107 border: theme.buttonInvertedBorder,
108
109 '& svg': {
110 fill: theme.buttonInvertedTextColor,
111 },
112 },
113 disabled: {
114 opacity: theme.inputDisabledOpacity,
115 },
116 loader: {
117 position: 'relative' as CSS.PositionProperty,
118 width: 20,
119 height: 18,
120 zIndex: 9999,
121 },
122 loaderContainer: {
123 width: (props: IProps): string => (!props.busy ? '0' : '40px'),
124 height: 20,
125 overflow: 'hidden',
126 transition: 'all 0.3s',
127 marginLeft: (props: IProps): number => !props.busy ? 10 : 20,
128 marginRight: (props: IProps): number => !props.busy ? -10 : -20,
129 position: (props: IProps): CSS.PositionProperty => props.stretch ? 'absolute' : 'inherit',
130 },
131 icon: {
132 marginLeft: -5,
133 marginRight: 10,
134 },
135});
136
137class ButtonComponent extends Component<IProps> {
138 public static defaultProps = {
139 type: 'button',
140 disabled: false,
141 onClick: () => null,
142 buttonType: 'primary' as ButtonType,
143 stretch: false,
144 busy: false,
145 // target: '_self'
146 };
147
148 state = {
149 busy: false,
150 };
151
152 componentWillMount() {
153 this.setState({ busy: this.props.busy });
154 }
155
156 componentWillReceiveProps(nextProps: IProps) {
157 if (nextProps.busy !== this.props.busy) {
158 if (this.props.busy) {
159 setTimeout(() => {
160 this.setState({ busy: nextProps.busy });
161 }, 300);
162 } else {
163 this.setState({ busy: nextProps.busy });
164 }
165 }
166 }
167
168 render() {
169 const {
170 classes,
171 className,
172 theme,
173 disabled,
174 id,
175 label,
176 type,
177 onClick,
178 buttonType,
179 loaded,
180 icon: iconName,
181 busy: busyProp,
182 href,
183 target,
184 } = this.props;
185
186 const {
187 busy,
188 } = this.state;
189
190 let icon = '';
191 if (iconName && mdiIcons[iconName]) {
192 icon = mdiIcons[iconName];
193 } else if (iconName && !mdiIcons[iconName]) {
194 console.warn(`Icon '${iconName}' was not found`);
195 }
196
197 let showLoader = false;
198 if (loaded) {
199 showLoader = !loaded;
200 console.warn('Franz Button prop `loaded` will be deprecated in the future. Please use `busy` instead');
201 }
202 if (busy) {
203 showLoader = busy;
204 }
205
206 const content = (
207 <>
208 <div className={classes.loaderContainer}>
209 {showLoader && (
210 <Loader
211 loaded={false}
212 width={4}
213 scale={0.45}
214 color={theme.buttonLoaderColor[buttonType!]}
215 parentClassName={classes.loader}
216 />
217 )}
218 </div>
219 <div className={classes.label}>
220 {icon && (
221 <Icon
222 path={icon}
223 size={1}
224 className={classes.icon}
225 />
226 )}
227 {label}
228 </div>
229 </>
230 );
231
232 let wrapperComponent = null;
233
234 if (!href) {
235 wrapperComponent = (
236 <button
237 id={id}
238 type={type}
239 onClick={onClick}
240 className={classnames({
241 [`${classes.button}`]: true,
242 [`${classes[buttonType as ButtonType]}`]: true,
243 [`${classes.disabled}`]: disabled,
244 [`${className}`]: className,
245 })}
246 disabled={disabled}
247 data-type="franz-button"
248 >
249 {content}
250 </button>
251 );
252 } else {
253 wrapperComponent = (
254 <a
255 href={href}
256 target={target}
257 onClick={onClick}
258 className={classnames({
259 [`${classes.button}`]: true,
260 [`${classes[buttonType as ButtonType]}`]: true,
261 [`${className}`]: className,
262 })}
263 rel={target === '_blank' ? 'noopener' : ''}
264 data-type="franz-button"
265 >
266 {content}
267 </a>
268 );
269 }
270
271 return wrapperComponent;
272 }
273}
274
275export const Button = injectStyle(styles)(withTheme(ButtonComponent));
diff --git a/packages/forms/src/error/index.tsx b/packages/forms/src/error/index.tsx
new file mode 100644
index 000000000..a487bb281
--- /dev/null
+++ b/packages/forms/src/error/index.tsx
@@ -0,0 +1,29 @@
1import { Classes } from 'jss';
2import React, { Component } from 'react';
3import injectSheet from 'react-jss';
4
5import styles from './styles';
6
7interface IProps {
8 classes: Classes;
9 message: string;
10}
11
12class ErrorComponent extends Component<IProps> {
13 render() {
14 const {
15 classes,
16 message,
17 } = this.props;
18
19 return (
20 <p
21 className={classes.message}
22 >
23 {message}
24 </p>
25 );
26 }
27}
28
29export const Error = injectSheet(styles)(ErrorComponent);
diff --git a/packages/forms/src/error/styles.ts b/packages/forms/src/error/styles.ts
new file mode 100644
index 000000000..5104838a5
--- /dev/null
+++ b/packages/forms/src/error/styles.ts
@@ -0,0 +1,9 @@
1import { Theme } from '../../../theme/lib';
2
3export default (theme: Theme) => ({
4 message: {
5 color: theme.brandDanger,
6 margin: '5px 0 0',
7 fontSize: theme.uiFontSize,
8 },
9});
diff --git a/packages/forms/src/index.ts b/packages/forms/src/index.ts
new file mode 100644
index 000000000..ea47fe25e
--- /dev/null
+++ b/packages/forms/src/index.ts
@@ -0,0 +1,4 @@
1export { Input } from './input';
2export { Toggle } from './toggle';
3export { Button } from './button';
4export { Select } from './select';
diff --git a/packages/forms/src/input/index.tsx b/packages/forms/src/input/index.tsx
new file mode 100644
index 000000000..a2d7c62d5
--- /dev/null
+++ b/packages/forms/src/input/index.tsx
@@ -0,0 +1,211 @@
1import { mdiEye, mdiEyeOff } from '@mdi/js';
2import Icon from '@mdi/react';
3import classnames from 'classnames';
4import React, { Component, createRef } from 'react';
5import injectSheet from 'react-jss';
6
7import { IFormField, IWithStyle } from '../typings/generic';
8
9import { Error } from '../error';
10import { Label } from '../label';
11import { Wrapper } from '../wrapper';
12import { scorePasswordFunc } from './scorePassword';
13
14import styles from './styles';
15
16interface IData {
17 [index: string]: string;
18}
19
20interface IProps extends React.InputHTMLAttributes<HTMLInputElement>, IFormField, IWithStyle {
21 focus?: boolean;
22 prefix?: string;
23 suffix?: string;
24 scorePassword?: boolean;
25 showPasswordToggle?: boolean;
26 data: IData;
27 inputClassName?: string;
28 onEnterKey?: Function;
29}
30
31interface IState {
32 showPassword: boolean;
33 passwordScore: number;
34}
35
36class InputComponent extends Component<IProps, IState> {
37 static defaultProps = {
38 focus: false,
39 onChange: () => {},
40 onBlur: () => {},
41 onFocus: () => {},
42 scorePassword: false,
43 showLabel: true,
44 showPasswordToggle: false,
45 type: 'text',
46 disabled: false,
47 };
48
49 state = {
50 passwordScore: 0,
51 showPassword: false,
52 };
53
54 private inputRef = createRef<HTMLInputElement>();
55
56 componentDidMount() {
57 const { focus, data } = this.props;
58
59 if (this.inputRef && this.inputRef.current) {
60 if (focus) {
61 this.inputRef.current.focus();
62 }
63
64 if (data) {
65 Object.keys(data).map(key => this.inputRef.current!.dataset[key] = data[key]);
66 }
67 }
68 }
69
70 onChange(e: React.ChangeEvent<HTMLInputElement>) {
71 const {
72 scorePassword,
73 onChange,
74 } = this.props;
75
76 if (onChange) {
77 onChange(e);
78 }
79
80 if (this.inputRef && this.inputRef.current && scorePassword) {
81 this.setState({ passwordScore: scorePasswordFunc(this.inputRef.current.value) });
82 }
83 }
84
85 onInputKeyPress(e: React.KeyboardEvent) {
86 if (e.key === "Enter") {
87 const { onEnterKey } = this.props;
88 onEnterKey && onEnterKey();
89 }
90 }
91
92 render() {
93 const {
94 classes,
95 className,
96 disabled,
97 error,
98 id,
99 inputClassName,
100 label,
101 prefix,
102 scorePassword,
103 suffix,
104 showLabel,
105 showPasswordToggle,
106 type,
107 value,
108 name,
109 placeholder,
110 spellCheck,
111 onBlur,
112 onFocus,
113 min,
114 max,
115 step,
116 required,
117 } = this.props;
118
119 const {
120 showPassword,
121 passwordScore,
122 } = this.state;
123
124 const inputType = type === 'password' && showPassword ? 'text' : type;
125
126 return (
127 <Wrapper
128 className={className}
129 identifier="franz-input"
130 >
131 <Label
132 title={label}
133 showLabel={showLabel}
134 htmlFor={id}
135 className={classes.label}
136 isRequired={required}
137 >
138 <div
139 className={classnames({
140 [`${inputClassName}`]: inputClassName,
141 [`${classes.hasPasswordScore}`]: scorePassword,
142 [`${classes.wrapper}`]: true,
143 [`${classes.disabled}`]: disabled,
144 [`${classes.hasError}`]: error,
145 })}>
146 {prefix && (
147 <span className={classes.prefix}>
148 {prefix}
149 </span>
150 )}
151 <input
152 id={id}
153 type={inputType}
154 name={name}
155 value={value as string}
156 placeholder={placeholder}
157 spellCheck={spellCheck}
158 className={classes.input}
159 ref={this.inputRef}
160 onChange={this.onChange.bind(this)}
161 onFocus={onFocus}
162 onBlur={onBlur}
163 disabled={disabled}
164 onKeyPress={this.onInputKeyPress.bind(this)}
165 min={min}
166 max={max}
167 step={step}
168 />
169 {suffix && (
170 <span className={classes.suffix}>
171 {suffix}
172 </span>
173 )}
174 {showPasswordToggle && (
175 <button
176 type="button"
177 className={classes.formModifier}
178 onClick={() => this.setState(prevState => ({ showPassword: !prevState.showPassword }))}
179 tabIndex={-1}
180 >
181 <Icon
182 path={!showPassword ? mdiEye : mdiEyeOff}
183 size={1}
184 />
185 </button>
186 )}
187 </div>
188 {scorePassword && (
189 <div className={classnames({
190 [`${classes.passwordScore}`]: true,
191 [`${classes.hasError}`]: error,
192 })}>
193 <meter
194 value={passwordScore < 5 ? 5 : passwordScore}
195 low={30}
196 high={75}
197 optimum={100}
198 max={100}
199 />
200 </div>
201 )}
202 </Label>
203 {error && (
204 <Error message={error} />
205 )}
206 </Wrapper>
207 );
208 }
209}
210
211export const Input = injectSheet(styles)(InputComponent);
diff --git a/packages/forms/src/input/scorePassword.ts b/packages/forms/src/input/scorePassword.ts
new file mode 100644
index 000000000..0b7719ec1
--- /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 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..e2ab30a4f
--- /dev/null
+++ b/packages/forms/src/input/styles.ts
@@ -0,0 +1,101 @@
1import { Theme } from '@meetfranz/theme';
2import CSS from 'csstype';
3
4const prefixStyles = (theme: Theme) => ({
5 background: theme.inputPrefixBackground,
6 color: theme.inputPrefixColor,
7 lineHeight: `${theme.inputHeight}px`,
8 padding: '0 10px',
9 fontSize: theme.uiFontSize,
10});
11
12export default (theme: Theme) => ({
13 label: {
14 '& > div': {
15 marginTop: 5,
16 }
17 },
18 disabled: {
19 opacity: theme.inputDisabledOpacity,
20 },
21 formModifier: {
22 background: 'none',
23 border: 0,
24 borderLeft: theme.inputBorder,
25 padding: '4px 20px 0',
26 outline: 'none',
27
28 '&:active': {
29 opacity: 0.5,
30 },
31
32 '& svg': {
33 fill: theme.inputModifierColor,
34 },
35 },
36 input: {
37 background: 'none',
38 border: 0,
39 fontSize: theme.uiFontSize,
40 outline: 'none',
41 padding: 8,
42 width: '100%',
43 color: theme.inputColor,
44
45 '&::placeholder': {
46 color: theme.inputPlaceholderColor,
47 },
48 },
49 passwordScore: {
50 background: theme.inputScorePasswordBackground,
51 border: theme.inputBorder,
52 borderTopWidth: 0,
53 borderBottomLeftRadius: theme.borderRadiusSmall,
54 borderBottomRightRadius: theme.borderRadiusSmall,
55 display: 'block',
56 flexBasis: '100%',
57 height: 5,
58 overflow: 'hidden',
59
60 '& meter': {
61 display: 'block',
62 height: '100%',
63 width: '100%',
64
65 '&::-webkit-meter-bar': {
66 background: 'none',
67 },
68
69 '&::-webkit-meter-even-less-good-value': {
70 background: theme.brandDanger,
71 },
72
73 '&::-webkit-meter-suboptimum-value': {
74 background: theme.brandWarning,
75 },
76
77 '&::-webkit-meter-optimum-value': {
78 background: theme.brandSuccess,
79 },
80 },
81 },
82 prefix: prefixStyles(theme),
83 suffix: prefixStyles(theme),
84 wrapper: {
85 background: theme.inputBackground,
86 border: theme.inputBorder,
87 borderRadius: theme.borderRadiusSmall,
88 boxSizing: 'border-box' as CSS.BoxSizingProperty,
89 display: 'flex',
90 height: theme.inputHeight,
91 order: 1,
92 width: '100%',
93 },
94 hasPasswordScore: {
95 borderBottomLeftRadius: 0,
96 borderBottomRightRadius: 0,
97 },
98 hasError: {
99 borderColor: theme.brandDanger,
100 },
101});
diff --git a/packages/forms/src/label/index.tsx b/packages/forms/src/label/index.tsx
new file mode 100644
index 000000000..1b33ba22c
--- /dev/null
+++ b/packages/forms/src/label/index.tsx
@@ -0,0 +1,51 @@
1import classnames from 'classnames';
2import { Classes } from 'jss';
3import React, { Component } from 'react';
4import injectSheet from 'react-jss';
5
6import { IFormField } from '../typings/generic';
7
8import styles from './styles';
9
10interface ILabel extends IFormField, React.LabelHTMLAttributes<HTMLLabelElement> {
11 classes: Classes;
12 isRequired: boolean;
13}
14
15class LabelComponent extends Component<ILabel> {
16 static defaultProps = {
17 showLabel: true,
18 };
19
20 render() {
21 const {
22 title,
23 showLabel,
24 classes,
25 className,
26 children,
27 htmlFor,
28 isRequired,
29 } = this.props;
30
31 if (!showLabel) return children;
32
33 return (
34 <label
35 className={classnames({
36 [`${className}`]: className,
37 })}
38 htmlFor={htmlFor}
39 >
40 {showLabel && (
41 <span className={classes.label}>{title}{isRequired && ' *'}</span>
42 )}
43 <div className={classes.content}>
44 {children}
45 </div>
46 </label>
47 );
48 }
49}
50
51export const Label = injectSheet(styles)(LabelComponent);
diff --git a/packages/forms/src/label/styles.ts b/packages/forms/src/label/styles.ts
new file mode 100644
index 000000000..c64c9b285
--- /dev/null
+++ b/packages/forms/src/label/styles.ts
@@ -0,0 +1,12 @@
1import { Theme } from '../../../theme/lib';
2
3export default (theme: Theme) => ({
4 content: {},
5 label: {
6 color: theme.labelColor,
7 fontSize: theme.uiFontSize,
8 },
9 hasError: {
10 color: theme.brandDanger,
11 },
12});
diff --git a/packages/forms/src/select/index.tsx b/packages/forms/src/select/index.tsx
new file mode 100644
index 000000000..0e5ded176
--- /dev/null
+++ b/packages/forms/src/select/index.tsx
@@ -0,0 +1,445 @@
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 label: {
60 '& > div': {
61 marginTop: 5,
62 }
63 },
64 popup: {
65 opacity: 0,
66 height: 0,
67 overflowX: 'scroll',
68 border: theme.selectBorder,
69 borderTop: 0,
70 transition: 'all 0.3s',
71 },
72 open: {
73 opacity: 1,
74 height: 350,
75 background: theme.selectPopupBackground,
76 },
77 option: {
78 padding: 10,
79 borderBottom: theme.selectOptionBorder,
80 color: theme.selectOptionColor,
81
82 '&:hover': {
83 background: theme.selectOptionItemHover,
84 color: theme.selectOptionItemHoverColor,
85 },
86 '&:active': {
87 background: theme.selectOptionItemActive,
88 color: theme.selectOptionItemActiveColor,
89 },
90 },
91 selected: {
92 background: theme.selectOptionItemActive,
93 color: theme.selectOptionItemActiveColor,
94 },
95 toggle: {
96 marginLeft: 'auto',
97 fill: theme.selectToggleColor,
98 transition: 'transform 0.3s',
99 },
100 toggleOpened: {
101 transform: 'rotateZ(90deg)',
102 },
103 searchContainer: {
104 display: 'flex',
105 background: theme.selectSearchBackground,
106 alignItems: 'center',
107 paddingLeft: 10,
108 color: theme.selectColor,
109
110 '& svg': {
111 fill: theme.selectSearchColor,
112 },
113 },
114 search: {
115 border: 0,
116 width: '100%',
117 fontSize: theme.uiFontSize,
118 background: 'none',
119 marginLeft: 10,
120 padding: [10, 0],
121 color: theme.selectSearchColor,
122 },
123 clearNeedle: {
124 background: 'none',
125 border: 0,
126 },
127 focused: {
128 fontWeight: 'bold',
129 background: theme.selectOptionItemHover,
130 color: theme.selectOptionItemHoverColor,
131 },
132 hasError: {
133 borderColor: theme.brandDanger,
134 },
135 disabled: {
136 opacity: theme.selectDisabledOpacity,
137 },
138});
139
140class SelectComponent extends Component<IProps> {
141 public static defaultProps = {
142 onChange: () => {},
143 showLabel: true,
144 disabled: false,
145 error: '',
146 };
147
148 state = {
149 open: false,
150 value: '',
151 needle: '',
152 selected: 0,
153 options: null,
154 };
155
156 private componentRef = createRef<HTMLDivElement>();
157 private inputRef = createRef<HTMLInputElement>();
158 private searchInputRef = createRef<HTMLInputElement>();
159 private scrollContainerRef = createRef<HTMLDivElement>();
160 private activeOptionRef = createRef<HTMLDivElement>();
161
162 private keyListener: any;
163
164 componentWillReceiveProps(nextProps: IProps) {
165 if (nextProps.value && nextProps.value !== this.props.value) {
166 this.setState({
167 value: nextProps.value,
168 });
169 }
170 }
171
172 componentDidUpdate(prevProps: IProps, prevState: IState) {
173 const {
174 open,
175 } = this.state;
176
177 if (this.searchInputRef && this.searchInputRef.current) {
178 if (open) {
179 this.searchInputRef.current.focus();
180 }
181 }
182 }
183
184 componentDidMount() {
185 if (this.inputRef && this.inputRef.current) {
186 const {
187 data,
188 } = this.props;
189
190 if (data) {
191 Object.keys(data).map(key => this.inputRef.current!.dataset[key] = data[key]);
192 }
193 }
194
195 window.addEventListener('keydown', this.arrowKeysHandler.bind(this), false);
196 }
197
198 componentWillMount() {
199 const {
200 value,
201 } = this.props;
202
203 if (this.componentRef && this.componentRef.current) {
204 this.componentRef.current.removeEventListener('keydown', this.keyListener);
205 }
206
207 if (value) {
208 this.setState({
209 value,
210 });
211 }
212
213 this.setFilter();
214 }
215
216 componentWillUnmount() {
217 window.removeEventListener('keydown', this.arrowKeysHandler.bind(this));
218 }
219
220 setFilter(needle: string = '') {
221 const { options } = this.props;
222
223 let filteredOptions = {};
224 if (needle) {
225 Object.keys(options).map((key) => {
226 if (key.toLocaleLowerCase().startsWith(needle.toLocaleLowerCase()) || options[key].toLocaleLowerCase().startsWith(needle.toLocaleLowerCase())) {
227 Object.assign(filteredOptions, {
228 [`${key}`]: options[key],
229 });
230 }
231 });
232 } else {
233 filteredOptions = options;
234 }
235
236 this.setState({
237 needle,
238 options: filteredOptions,
239 selected: 0,
240 });
241 }
242
243 select(key: string) {
244 this.setState((state: IState) => ({
245 value: key,
246 open: false,
247 }));
248
249 this.setFilter();
250
251 if (this.props.onChange) {
252 this.props.onChange(key as any);
253 }
254 }
255
256 arrowKeysHandler(e: KeyboardEvent) {
257 const {
258 selected,
259 open,
260 options,
261 } = this.state;
262
263 if (!open) return;
264
265 if (e.keyCode === 38 || e.keyCode === 40) {
266 e.preventDefault();
267 }
268
269 if (this.componentRef && this.componentRef.current) {
270 if (e.keyCode === 38 && selected > 0) {
271 this.setState((state: IState) => ({
272 selected: state.selected - 1,
273 }));
274 } else if (e.keyCode === 40 && selected < Object.keys(options!).length - 1) {
275 this.setState((state: IState) => ({
276 selected: state.selected + 1,
277 }));
278 } else if (e.keyCode === 13) {
279 this.select(Object.keys(options!)[selected]);
280 }
281
282 if (this.activeOptionRef && this.activeOptionRef.current && this.scrollContainerRef && this.scrollContainerRef.current) {
283 const containerTopOffset = this.scrollContainerRef.current.offsetTop;
284 const optionTopOffset = this.activeOptionRef.current.offsetTop;
285
286 const topOffset = optionTopOffset - containerTopOffset;
287
288 this.scrollContainerRef.current.scrollTop = topOffset - 35;
289 }
290 }
291
292 switch (e.keyCode){
293 case 37: case 39: case 38: case 40: // Arrow keys
294 case 32: break; // Space
295 default: break; // do not block other keys
296 }
297 }
298
299 render() {
300 const {
301 actionText,
302 classes,
303 className,
304 defaultValue,
305 disabled,
306 error,
307 id,
308 inputClassName,
309 name,
310 label,
311 showLabel,
312 showSearch,
313 onChange,
314 required,
315 } = this.props;
316
317 const {
318 open,
319 needle,
320 value,
321 selected,
322 options,
323 } = this.state;
324
325 let selection = '';
326 if (!value && defaultValue && options![defaultValue]) {
327 selection = options![defaultValue];
328 } else if (value && options![value]) {
329 selection = options![value];
330 } else {
331 selection = actionText;
332 }
333
334 return (
335 <Wrapper
336 className={className}
337 identifier="franz-select"
338 >
339 <Label
340 title={label}
341 showLabel={showLabel}
342 htmlFor={id}
343 className={classes.label}
344 isRequired={required}
345 >
346 <div
347 className={classnames({
348 [`${classes.hasError}`]: error,
349 [`${classes.disabled}`]: disabled,
350 })}
351 ref={this.componentRef}
352 >
353 <button
354 type="button"
355 className={classnames({
356 [`${inputClassName}`]: inputClassName,
357 [`${classes.select}`]: true,
358 [`${classes.hasError}`]: error,
359 })}
360 onClick= {!disabled ? () => this.setState((state: IState) => ({
361 open: !state.open,
362 })) : () => {}}
363 >
364 {selection}
365 <Icon
366 path={mdiArrowRightDropCircleOutline}
367 size={0.8}
368 className={classnames({
369 [`${classes.toggle}`]: true,
370 [`${classes.toggleOpened}`]: open,
371 })}
372 />
373 </button>
374 {showSearch && open && (
375 <div className={classes.searchContainer}>
376 <Icon
377 path={mdiMagnify}
378 size={0.8}
379 />
380 <input
381 type="text"
382 value={needle}
383 onChange={e => this.setFilter(e.currentTarget.value)}
384 placeholder="Search"
385 className={classes.search}
386 ref={this.searchInputRef}
387 />
388 {needle && (
389 <button
390 type="button"
391 className={classes.clearNeedle}
392 onClick={() => this.setFilter()}
393 >
394 <Icon
395 path={mdiCloseCircle}
396 size={0.7}
397 />
398 </button>
399 )}
400 </div>
401 )}
402 <div
403 className={classnames({
404 [`${classes.popup}`]: true,
405 [`${classes.open}`]: open,
406 })}
407 ref={this.scrollContainerRef}
408 >
409 {Object.keys(options!).map(((key, i) => (
410 <div
411 key={key}
412 onClick={() => this.select(key)}
413 className={classnames({
414 [`${classes.option}`]: true,
415 [`${classes.selected}`]: options![key] === selection,
416 [`${classes.focused}`]: selected === i,
417 })}
418 onMouseOver={() => this.setState({ selected: i })}
419 ref={selected === i ? this.activeOptionRef : null}
420 >
421 {options![key]}
422 </div>
423 )))}
424 </div>
425 </div>
426 <input
427 className={classes.input}
428 id={id}
429 name={name}
430 type="hidden"
431 defaultValue={value}
432 onChange={onChange}
433 disabled={disabled}
434 ref={this.inputRef}
435 />
436 </Label>
437 {error && (
438 <Error message={error} />
439 )}
440 </Wrapper>
441 );
442 }
443}
444
445export const Select = injectStyle(styles)(SelectComponent);
diff --git a/packages/forms/src/toggle/index.tsx b/packages/forms/src/toggle/index.tsx
new file mode 100644
index 000000000..d84508a5f
--- /dev/null
+++ b/packages/forms/src/toggle/index.tsx
@@ -0,0 +1,117 @@
1import { Theme } from '@meetfranz/theme';
2import classnames from 'classnames';
3import CSS from 'csstype';
4import React, { Component } from 'react';
5import injectStyle from 'react-jss';
6
7import { IFormField, IWithStyle, Omit } from '../typings/generic';
8
9import { Error } from '../error';
10import { Label } from '../label';
11import { Wrapper } from '../wrapper';
12
13interface IProps extends React.InputHTMLAttributes<HTMLInputElement>, IFormField, IWithStyle {
14 className?: string;
15}
16
17const styles = (theme: Theme) => ({
18 toggle: {
19 background: theme.toggleBackground,
20 borderRadius: theme.borderRadius,
21 height: theme.toggleHeight,
22 position: 'relative' as CSS.PositionProperty,
23 width: theme.toggleWidth,
24 },
25 button: {
26 background: theme.toggleButton,
27 borderRadius: '100%',
28 boxShadow: '0 1px 4px rgba(0, 0, 0, .3)',
29 width: theme.toggleHeight - 2,
30 height: theme.toggleHeight - 2,
31 left: 1,
32 top: 1,
33 position: 'absolute' as CSS.PositionProperty,
34 transition: 'all .5s',
35 },
36 buttonActive: {
37 background: theme.toggleButtonActive,
38 left: (theme.toggleWidth - theme.toggleHeight) + 1,
39 },
40 input: {
41 visibility: 'hidden' as any,
42 },
43 disabled: {
44 opacity: theme.inputDisabledOpacity,
45 },
46 toggleLabel: {
47 display: 'flex',
48 alignItems: 'center',
49
50 '& > span': {
51 order: 1,
52 marginLeft: 15,
53 },
54 },
55});
56
57class ToggleComponent extends Component<IProps> {
58 public static defaultProps = {
59 onChange: () => {},
60 showLabel: true,
61 disabled: false,
62 error: '',
63 };
64
65 render() {
66 const {
67 classes,
68 className,
69 disabled,
70 error,
71 id,
72 label,
73 showLabel,
74 checked,
75 value,
76 onChange,
77 } = this.props;
78
79 return (
80 <Wrapper
81 className={className}
82 identifier="franz-toggle"
83 >
84 <Label
85 title={label}
86 showLabel={showLabel}
87 htmlFor={id}
88 className={classes.toggleLabel}
89 >
90 <div className={classnames({
91 [`${classes.toggle}`]: true,
92 [`${classes.disabled}`]: disabled,
93 })}>
94 <div className={classnames({
95 [`${classes.button}`]: true,
96 [`${classes.buttonActive}`]: checked,
97 })} />
98 <input
99 className={classes.input}
100 id={id || name}
101 type="checkbox"
102 checked={checked}
103 value={value}
104 onChange={onChange}
105 disabled={disabled}
106 />
107 </div>
108 </Label>
109 {error && (
110 <Error message={error} />
111 )}
112 </Wrapper>
113 );
114 }
115}
116
117export const Toggle = injectStyle(styles)(ToggleComponent);
diff --git a/packages/forms/src/typings/generic.ts b/packages/forms/src/typings/generic.ts
new file mode 100644
index 000000000..9688ce2c7
--- /dev/null
+++ b/packages/forms/src/typings/generic.ts
@@ -0,0 +1,17 @@
1import { Theme } from '@meetfranz/theme/lib';
2import { Classes } from 'jss';
3
4export interface IFormField {
5 showLabel?: boolean;
6 label?: string;
7 error?: string;
8 required?: boolean;
9}
10
11export interface IWithStyle {
12 classes: Classes;
13 theme: Theme;
14}
15
16export type Merge<M, N> = Omit<M, Extract<keyof M, keyof N>> & N;
17export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
diff --git a/packages/forms/src/wrapper/index.tsx b/packages/forms/src/wrapper/index.tsx
new file mode 100644
index 000000000..d9c61381d
--- /dev/null
+++ b/packages/forms/src/wrapper/index.tsx
@@ -0,0 +1,37 @@
1import classnames from 'classnames';
2import React, { Component } from 'react';
3import injectStyle from 'react-jss';
4import { IWithStyle } from '../typings/generic';
5
6import styles from './styles';
7
8interface IProps extends IWithStyle {
9 children: React.ReactNode;
10 className?: string;
11 identifier: string;
12}
13
14class WrapperComponent extends Component<IProps> {
15 render() {
16 const {
17 children,
18 classes,
19 className,
20 identifier,
21 } = this.props;
22
23 return (
24 <div
25 className={classnames({
26 [`${classes.container}`]: true,
27 [`${className}`]: className,
28 })}
29 data-type={identifier}
30 >
31 {children}
32 </div>
33 );
34 }
35}
36
37export const Wrapper = injectStyle(styles)(WrapperComponent);
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 @@
1export default {
2 container: {
3 marginBottom: 20,
4 },
5};
diff --git a/packages/forms/tsconfig.json b/packages/forms/tsconfig.json
new file mode 100644
index 000000000..8b9507eac
--- /dev/null
+++ b/packages/forms/tsconfig.json
@@ -0,0 +1,12 @@
1{
2 "extends": "../../tsconfig.settings.json",
3 "compilerOptions": {
4 "outDir": "lib",
5 "rootDir": "src"
6 },
7 "references": [
8 {
9 "path": "../theme"
10 }
11 ]
12}
diff --git a/packages/forms/tslint.json b/packages/forms/tslint.json
new file mode 100644
index 000000000..0946f2096
--- /dev/null
+++ b/packages/forms/tslint.json
@@ -0,0 +1,3 @@
1{
2 "extends": "../../tslint.json"
3}