aboutsummaryrefslogtreecommitdiffstats
path: root/packages/forms
diff options
context:
space:
mode:
authorLibravatar Vijay Aravamudhan <vraravam@users.noreply.github.com>2021-10-15 16:22:25 +0530
committerLibravatar GitHub <noreply@github.com>2021-10-15 16:22:25 +0530
commitec15f83b947fb2daf4ca1a72e3af527dc89512a3 (patch)
treeea049cee5184a58b5bc09505e723cd19a736c4bd /packages/forms
parentchore: move 'packages/ui' into 'src' (no longer an injected package) (#2077) (diff)
downloadferdium-app-ec15f83b947fb2daf4ca1a72e3af527dc89512a3.tar.gz
ferdium-app-ec15f83b947fb2daf4ca1a72e3af527dc89512a3.tar.zst
ferdium-app-ec15f83b947fb2daf4ca1a72e3af527dc89512a3.zip
chore: move 'packages/forms' into 'src' (no longer an injected package) (#2079)
Diffstat (limited to 'packages/forms')
-rw-r--r--packages/forms/.gitignore2
-rw-r--r--packages/forms/package.json33
-rw-r--r--packages/forms/src/button/index.tsx265
-rw-r--r--packages/forms/src/error/index.tsx20
-rw-r--r--packages/forms/src/error/styles.ts9
-rw-r--r--packages/forms/src/index.ts5
-rw-r--r--packages/forms/src/input/index.tsx208
-rw-r--r--packages/forms/src/input/scorePassword.ts42
-rw-r--r--packages/forms/src/input/styles.ts102
-rw-r--r--packages/forms/src/label/index.tsx52
-rw-r--r--packages/forms/src/label/styles.ts12
-rw-r--r--packages/forms/src/select/index.tsx460
-rw-r--r--packages/forms/src/textarea/index.tsx126
-rw-r--r--packages/forms/src/textarea/styles.ts54
-rw-r--r--packages/forms/src/toggle/index.tsx125
-rw-r--r--packages/forms/src/typings/generic.ts19
-rw-r--r--packages/forms/src/wrapper/index.tsx37
-rw-r--r--packages/forms/tsconfig.json12
18 files changed, 0 insertions, 1583 deletions
diff --git a/packages/forms/.gitignore b/packages/forms/.gitignore
deleted file mode 100644
index d01826a6b..000000000
--- a/packages/forms/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
1node_modules/
2lib
diff --git a/packages/forms/package.json b/packages/forms/package.json
deleted file mode 100644
index f073221bc..000000000
--- a/packages/forms/package.json
+++ /dev/null
@@ -1,33 +0,0 @@
1{
2 "name": "@meetfranz/forms",
3 "version": "1.2.1",
4 "description": "React form components for Franz",
5 "main": "lib/index.js",
6 "scripts": {
7 "dev": "tsc -w",
8 "build": "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": "Amine Mouafik <amine@mouafik.fr>",
24 "license": "Apache-2.0",
25 "dependencies": {
26 "@mdi/js": "3.3.92",
27 "@mdi/react": "1.5.0",
28 "@meetfranz/theme": "file:../theme",
29 "csstype": "3.0.8",
30 "react-loader": "2.4.7"
31 },
32 "gitHead": "00db2bddccb8bb8ad7d29b8d032876c798b8bbf3"
33}
diff --git a/packages/forms/src/button/index.tsx b/packages/forms/src/button/index.tsx
deleted file mode 100644
index d9f372c4b..000000000
--- a/packages/forms/src/button/index.tsx
+++ /dev/null
@@ -1,265 +0,0 @@
1import Icon from '@mdi/react';
2import classnames from 'classnames';
3import { Property } from 'csstype';
4import { Component, MouseEvent } from 'react';
5import injectStyle, { withTheme } from 'react-jss';
6import Loader from 'react-loader';
7
8import { IFormField, IWithStyle } from '../typings/generic';
9import { Theme } from '../../../theme';
10
11type ButtonType =
12 | 'primary'
13 | 'secondary'
14 | 'success'
15 | 'danger'
16 | 'warning'
17 | 'inverted';
18
19interface IProps extends IFormField, IWithStyle {
20 className?: string;
21 disabled?: boolean;
22 id?: string;
23 type?: 'button' | 'reset' | 'submit' | undefined;
24 onClick: (
25 event: MouseEvent<HTMLButtonElement> | MouseEvent<HTMLAnchorElement>,
26 ) => void;
27 buttonType?: ButtonType;
28 stretch?: boolean;
29 loaded?: boolean;
30 busy?: boolean;
31 icon?: string;
32 href?: string;
33 target?: string;
34}
35
36let buttonTransition: string = 'none';
37let loaderContainerTransition: string = 'none';
38
39if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
40 buttonTransition = 'background .5s, opacity 0.3s';
41 loaderContainerTransition = 'all 0.3s';
42}
43
44const styles = (theme: Theme) => ({
45 button: {
46 borderRadius: theme.borderRadiusSmall,
47 border: 'none',
48 display: 'inline-flex',
49 position: 'relative' as Property.Position,
50 transition: buttonTransition,
51 textAlign: 'center' as Property.TextAlign,
52 outline: 'none',
53 alignItems: 'center',
54 padding: 0,
55 width: (props: IProps) =>
56 (props.stretch ? '100%' : 'auto') as Property.Width<string>,
57 fontSize: theme.uiFontSize,
58 textDecoration: 'none',
59
60 '&:hover': {
61 opacity: 0.8,
62 },
63 '&:active': {
64 opacity: 0.5,
65 transition: 'none',
66 },
67 },
68 label: {
69 margin: '10px 20px',
70 width: '100%',
71 display: 'flex',
72 alignItems: 'center',
73 justifyContent: 'center',
74 },
75 primary: {
76 background: theme.buttonPrimaryBackground,
77 color: theme.buttonPrimaryTextColor,
78
79 '& svg': {
80 fill: theme.buttonPrimaryTextColor,
81 },
82 },
83 secondary: {
84 background: theme.buttonSecondaryBackground,
85 color: theme.buttonSecondaryTextColor,
86
87 '& svg': {
88 fill: theme.buttonSecondaryTextColor,
89 },
90 },
91 success: {
92 background: theme.buttonSuccessBackground,
93 color: theme.buttonSuccessTextColor,
94
95 '& svg': {
96 fill: theme.buttonSuccessTextColor,
97 },
98 },
99 danger: {
100 background: theme.buttonDangerBackground,
101 color: theme.buttonDangerTextColor,
102
103 '& svg': {
104 fill: theme.buttonDangerTextColor,
105 },
106 },
107 warning: {
108 background: theme.buttonWarningBackground,
109 color: theme.buttonWarningTextColor,
110
111 '& svg': {
112 fill: theme.buttonWarningTextColor,
113 },
114 },
115 inverted: {
116 background: theme.buttonInvertedBackground,
117 color: theme.buttonInvertedTextColor,
118 border: theme.buttonInvertedBorder,
119
120 '& svg': {
121 fill: theme.buttonInvertedTextColor,
122 },
123 },
124 disabled: {
125 opacity: theme.inputDisabledOpacity,
126 },
127 loader: {
128 position: 'relative' as Property.Position,
129 width: 20,
130 height: 18,
131 zIndex: 9999,
132 },
133 loaderContainer: {
134 width: (props: IProps): string => (!props.busy ? '0' : '40px'),
135 height: 20,
136 overflow: 'hidden',
137 transition: loaderContainerTransition,
138 marginLeft: (props: IProps): number => (!props.busy ? 10 : 20),
139 marginRight: (props: IProps): number => (!props.busy ? -10 : -20),
140 position: (props: IProps): Property.Position =>
141 props.stretch ? 'absolute' : 'inherit',
142 },
143 icon: {
144 margin: [1, 10, 0, -5],
145 },
146});
147
148class ButtonComponent extends Component<IProps> {
149 public static defaultProps = {
150 type: 'button',
151 disabled: false,
152 onClick: () => null,
153 buttonType: 'primary' as ButtonType,
154 stretch: false,
155 busy: false,
156 };
157
158 state = {
159 busy: false,
160 };
161
162 componentWillMount() {
163 this.setState({ busy: this.props.busy });
164 }
165
166 componentWillReceiveProps(nextProps: IProps) {
167 if (nextProps.busy !== this.props.busy) {
168 if (this.props.busy) {
169 setTimeout(() => {
170 this.setState({ busy: nextProps.busy });
171 }, 300);
172 } else {
173 this.setState({ busy: nextProps.busy });
174 }
175 }
176 }
177
178 render() {
179 const {
180 classes,
181 className,
182 theme,
183 disabled,
184 id,
185 label,
186 type,
187 onClick,
188 buttonType,
189 loaded,
190 icon,
191 href,
192 target,
193 } = this.props;
194
195 const { busy } = this.state;
196
197 let showLoader = false;
198 if (loaded) {
199 showLoader = !loaded;
200 console.warn(
201 'Ferdi Button prop `loaded` will be deprecated in the future. Please use `busy` instead',
202 );
203 }
204 if (busy) {
205 showLoader = busy;
206 }
207
208 const content = (
209 <>
210 <div className={classes.loaderContainer}>
211 {showLoader && (
212 <Loader
213 loaded={false}
214 width={4}
215 scale={0.45}
216 color={theme.buttonLoaderColor[buttonType!]}
217 parentClassName={classes.loader}
218 />
219 )}
220 </div>
221 <div className={classes.label}>
222 {icon && <Icon path={icon} size={0.8} className={classes.icon} />}
223 {label}
224 </div>
225 </>
226 );
227
228 const wrapperComponent = !href ? (
229 <button
230 id={id}
231 type={type}
232 onClick={onClick}
233 className={classnames({
234 [`${classes.button}`]: true,
235 [`${classes[buttonType as ButtonType]}`]: true,
236 [`${classes.disabled}`]: disabled,
237 [`${className}`]: className,
238 })}
239 disabled={disabled}
240 data-type="franz-button"
241 >
242 {content}
243 </button>
244 ) : (
245 <a
246 href={href}
247 target={target}
248 onClick={onClick}
249 className={classnames({
250 [`${classes.button}`]: true,
251 [`${classes[buttonType as ButtonType]}`]: true,
252 [`${className}`]: className,
253 })}
254 rel={target === '_blank' ? 'noopener' : ''}
255 data-type="franz-button"
256 >
257 {content}
258 </a>
259 );
260
261 return wrapperComponent;
262 }
263}
264
265export const Button = injectStyle(styles)(withTheme(ButtonComponent));
diff --git a/packages/forms/src/error/index.tsx b/packages/forms/src/error/index.tsx
deleted file mode 100644
index 8439bfc8b..000000000
--- a/packages/forms/src/error/index.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
1import { Classes } from 'jss';
2import { 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 { classes, message } = this.props;
15
16 return <p className={classes.message}>{message}</p>;
17 }
18}
19
20export const Error = injectSheet(styles)(ErrorComponent);
diff --git a/packages/forms/src/error/styles.ts b/packages/forms/src/error/styles.ts
deleted file mode 100644
index 4c0c7e2c0..000000000
--- a/packages/forms/src/error/styles.ts
+++ /dev/null
@@ -1,9 +0,0 @@
1import { Theme } from '../../../theme';
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
deleted file mode 100644
index 45a9ed8e3..000000000
--- a/packages/forms/src/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
1export { Input } from './input';
2export { Textarea } from './textarea';
3export { Toggle } from './toggle';
4export { Button } from './button';
5export { Select } from './select';
diff --git a/packages/forms/src/input/index.tsx b/packages/forms/src/input/index.tsx
deleted file mode 100644
index 0b16fe688..000000000
--- a/packages/forms/src/input/index.tsx
+++ /dev/null
@@ -1,208 +0,0 @@
1import { mdiEye, mdiEyeOff } from '@mdi/js';
2import Icon from '@mdi/react';
3import classnames from 'classnames';
4import { Component, createRef, InputHTMLAttributes } 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
21 extends InputHTMLAttributes<HTMLInputElement>,
22 IFormField,
23 IWithStyle {
24 focus?: boolean;
25 prefix?: string;
26 suffix?: string;
27 scorePassword?: boolean;
28 showPasswordToggle?: boolean;
29 data: IData;
30 inputClassName?: string;
31 onEnterKey?: Function;
32}
33
34interface IState {
35 showPassword: boolean;
36 passwordScore: number;
37}
38
39class InputComponent extends Component<IProps, IState> {
40 static defaultProps = {
41 focus: false,
42 onChange: () => {},
43 onBlur: () => {},
44 onFocus: () => {},
45 scorePassword: false,
46 showLabel: true,
47 showPasswordToggle: false,
48 type: 'text',
49 disabled: false,
50 };
51
52 state = {
53 passwordScore: 0,
54 showPassword: false,
55 };
56
57 private inputRef = createRef<HTMLInputElement>();
58
59 componentDidMount() {
60 const { focus, data } = this.props;
61
62 if (this.inputRef && this.inputRef.current) {
63 if (focus) {
64 this.inputRef.current.focus();
65 }
66
67 if (data) {
68 Object.keys(data).map(
69 key => (this.inputRef.current!.dataset[key] = data[key]),
70 );
71 }
72 }
73 }
74
75 onChange(e: React.ChangeEvent<HTMLInputElement>) {
76 const { scorePassword, onChange } = this.props;
77
78 if (onChange) {
79 onChange(e);
80 }
81
82 if (this.inputRef && this.inputRef.current && scorePassword) {
83 this.setState({
84 passwordScore: scorePasswordFunc(this.inputRef.current.value),
85 });
86 }
87 }
88
89 onInputKeyPress(e: React.KeyboardEvent) {
90 if (e.key === 'Enter') {
91 const { onEnterKey } = this.props;
92 onEnterKey && onEnterKey();
93 }
94 }
95
96 render() {
97 const {
98 classes,
99 className,
100 disabled,
101 error,
102 id,
103 inputClassName,
104 label,
105 prefix,
106 scorePassword,
107 suffix,
108 showLabel,
109 showPasswordToggle,
110 type,
111 value,
112 name,
113 placeholder,
114 spellCheck,
115 onBlur,
116 onFocus,
117 min,
118 max,
119 step,
120 required,
121 noMargin,
122 } = this.props;
123
124 const { showPassword, passwordScore } = this.state;
125
126 const inputType = type === 'password' && showPassword ? 'text' : type;
127
128 return (
129 <Wrapper
130 className={className}
131 identifier="franz-input"
132 noMargin={noMargin}
133 >
134 <Label
135 title={label}
136 showLabel={showLabel}
137 htmlFor={id}
138 className={classes.label}
139 isRequired={required}
140 >
141 <div
142 className={classnames({
143 [`${inputClassName}`]: inputClassName,
144 [`${classes.hasPasswordScore}`]: scorePassword,
145 [`${classes.wrapper}`]: true,
146 [`${classes.disabled}`]: disabled,
147 [`${classes.hasError}`]: error,
148 })}
149 >
150 {prefix && <span className={classes.prefix}>{prefix}</span>}
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 && <span className={classes.suffix}>{suffix}</span>}
170 {showPasswordToggle && (
171 <button
172 type="button"
173 className={classes.formModifier}
174 onClick={() =>
175 this.setState(prevState => ({
176 showPassword: !prevState.showPassword,
177 }))
178 }
179 tabIndex={-1}
180 >
181 <Icon path={!showPassword ? mdiEye : mdiEyeOff} size={1} />
182 </button>
183 )}
184 </div>
185 {scorePassword && (
186 <div
187 className={classnames({
188 [`${classes.passwordScore}`]: true,
189 [`${classes.hasError}`]: error,
190 })}
191 >
192 <meter
193 value={passwordScore < 5 ? 5 : passwordScore}
194 low={30}
195 high={75}
196 optimum={100}
197 max={100}
198 />
199 </div>
200 )}
201 </Label>
202 {error && <Error message={error} />}
203 </Wrapper>
204 );
205 }
206}
207
208export const Input = injectSheet(styles)(InputComponent);
diff --git a/packages/forms/src/input/scorePassword.ts b/packages/forms/src/input/scorePassword.ts
deleted file mode 100644
index 59502e2b0..000000000
--- a/packages/forms/src/input/scorePassword.ts
+++ /dev/null
@@ -1,42 +0,0 @@
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 = 0;
15 if (!password) {
16 return score;
17 }
18
19 // award every unique letter until 5 repetitions
20 const letters: ILetters = {};
21 for (const element of password) {
22 letters[element] = (letters[element] || 0) + 1;
23 score += 5 / letters[element];
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 for (const key of Object.keys(variations)) {
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
deleted file mode 100644
index 6d56e93b3..000000000
--- a/packages/forms/src/input/styles.ts
+++ /dev/null
@@ -1,102 +0,0 @@
1import { Property } from 'csstype';
2
3import { Theme } from '../../../theme';
4
5const prefixStyles = (theme: Theme) => ({
6 background: theme.inputPrefixBackground,
7 color: theme.inputPrefixColor,
8 lineHeight: `${theme.inputHeight}px`,
9 padding: '0 10px',
10 fontSize: theme.uiFontSize,
11});
12
13export default (theme: Theme) => ({
14 label: {
15 '& > div': {
16 marginTop: 5,
17 },
18 },
19 disabled: {
20 opacity: theme.inputDisabledOpacity,
21 },
22 formModifier: {
23 background: 'none',
24 border: 0,
25 borderLeft: theme.inputBorder,
26 padding: '4px 20px 0',
27 outline: 'none',
28
29 '&:active': {
30 opacity: 0.5,
31 },
32
33 '& svg': {
34 fill: theme.inputModifierColor,
35 },
36 },
37 input: {
38 background: 'none',
39 border: 0,
40 fontSize: theme.uiFontSize,
41 outline: 'none',
42 padding: 8,
43 width: '100%',
44 color: theme.inputColor,
45
46 '&::placeholder': {
47 color: theme.inputPlaceholderColor,
48 },
49 },
50 passwordScore: {
51 background: theme.inputScorePasswordBackground,
52 border: theme.inputBorder,
53 borderTopWidth: 0,
54 borderBottomLeftRadius: theme.borderRadiusSmall,
55 borderBottomRightRadius: theme.borderRadiusSmall,
56 display: 'block',
57 flexBasis: '100%',
58 height: 5,
59 overflow: 'hidden',
60
61 '& meter': {
62 display: 'block',
63 height: '100%',
64 width: '100%',
65
66 '&::-webkit-meter-bar': {
67 background: 'none',
68 },
69
70 '&::-webkit-meter-even-less-good-value': {
71 background: theme.brandDanger,
72 },
73
74 '&::-webkit-meter-suboptimum-value': {
75 background: theme.brandWarning,
76 },
77
78 '&::-webkit-meter-optimum-value': {
79 background: theme.brandSuccess,
80 },
81 },
82 },
83 prefix: prefixStyles(theme),
84 suffix: prefixStyles(theme),
85 wrapper: {
86 background: theme.inputBackground,
87 border: theme.inputBorder,
88 borderRadius: theme.borderRadiusSmall,
89 boxSizing: 'border-box' as Property.BoxSizing,
90 display: 'flex',
91 height: theme.inputHeight,
92 order: 1,
93 width: '100%',
94 },
95 hasPasswordScore: {
96 borderBottomLeftRadius: 0,
97 borderBottomRightRadius: 0,
98 },
99 hasError: {
100 borderColor: theme.brandDanger,
101 },
102});
diff --git a/packages/forms/src/label/index.tsx b/packages/forms/src/label/index.tsx
deleted file mode 100644
index 4d86f23f7..000000000
--- a/packages/forms/src/label/index.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
1import classnames from 'classnames';
2import { Classes } from 'jss';
3import { Component, LabelHTMLAttributes } from 'react';
4import injectSheet from 'react-jss';
5
6import { IFormField } from '../typings/generic';
7
8import styles from './styles';
9
10interface ILabel extends IFormField, 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}>
42 {title}
43 {isRequired && ' *'}
44 </span>
45 )}
46 <div className={classes.content}>{children}</div>
47 </label>
48 );
49 }
50}
51
52export const Label = injectSheet(styles)(LabelComponent);
diff --git a/packages/forms/src/label/styles.ts b/packages/forms/src/label/styles.ts
deleted file mode 100644
index 7c62b6b4c..000000000
--- a/packages/forms/src/label/styles.ts
+++ /dev/null
@@ -1,12 +0,0 @@
1import { Theme } from '../../../theme';
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
deleted file mode 100644
index d965d3c93..000000000
--- a/packages/forms/src/select/index.tsx
+++ /dev/null
@@ -1,460 +0,0 @@
1import {
2 mdiArrowRightDropCircleOutline,
3 mdiCloseCircle,
4 mdiMagnify,
5} from '@mdi/js';
6import Icon from '@mdi/react';
7import classnames from 'classnames';
8import { ChangeEvent, Component, createRef } from 'react';
9import injectStyle from 'react-jss';
10
11import { IFormField, IWithStyle } from '../typings/generic';
12import { Theme } from '../../../theme';
13
14import { Error } from '../error';
15import { Label } from '../label';
16import { Wrapper } from '../wrapper';
17
18interface IOptions {
19 [index: string]: string;
20}
21
22interface IData {
23 [index: string]: string;
24}
25
26interface IProps extends IFormField, IWithStyle {
27 actionText: string;
28 className?: string;
29 inputClassName?: string;
30 defaultValue?: string;
31 disabled?: boolean;
32 id?: string;
33 name: string;
34 options: IOptions;
35 value: string;
36 onChange: (event: ChangeEvent<HTMLInputElement>) => void;
37 showSearch: boolean;
38 data: IData;
39}
40
41interface IState {
42 open: boolean;
43 value: string;
44 needle: string;
45 selected: number;
46 options: IOptions;
47}
48
49let popupTransition: string = 'none';
50let toggleTransition: string = 'none';
51
52if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
53 popupTransition = 'all 0.3s';
54 toggleTransition = 'transform 0.3s';
55}
56
57const styles = (theme: Theme) => ({
58 select: {
59 background: theme.selectBackground,
60 border: theme.selectBorder,
61 borderRadius: theme.borderRadiusSmall,
62 height: theme.selectHeight,
63 fontSize: theme.uiFontSize,
64 width: '100%',
65 display: 'flex',
66 alignItems: 'center',
67 textAlign: 'left',
68 color: theme.selectColor,
69 },
70 label: {
71 '& > div': {
72 marginTop: 5,
73 },
74 },
75 popup: {
76 opacity: 0,
77 height: 0,
78 overflowX: 'scroll',
79 border: theme.selectBorder,
80 borderTop: 0,
81 transition: popupTransition,
82 },
83 open: {
84 opacity: 1,
85 height: 350,
86 background: theme.selectPopupBackground,
87 },
88 option: {
89 padding: 10,
90 borderBottom: theme.selectOptionBorder,
91 color: theme.selectOptionColor,
92
93 '&:hover': {
94 background: theme.selectOptionItemHover,
95 color: theme.selectOptionItemHoverColor,
96 },
97 '&:active': {
98 background: theme.selectOptionItemActive,
99 color: theme.selectOptionItemActiveColor,
100 },
101 },
102 selected: {
103 background: theme.selectOptionItemActive,
104 color: theme.selectOptionItemActiveColor,
105 },
106 toggle: {
107 marginLeft: 'auto',
108 fill: theme.selectToggleColor,
109 transition: toggleTransition,
110 },
111 toggleOpened: {
112 transform: 'rotateZ(90deg)',
113 },
114 searchContainer: {
115 display: 'flex',
116 background: theme.selectSearchBackground,
117 alignItems: 'center',
118 paddingLeft: 10,
119 color: theme.selectColor,
120
121 '& svg': {
122 fill: theme.selectSearchColor,
123 },
124 },
125 search: {
126 border: 0,
127 width: '100%',
128 fontSize: theme.uiFontSize,
129 background: 'none',
130 marginLeft: 10,
131 padding: [10, 0],
132 color: theme.selectSearchColor,
133 },
134 clearNeedle: {
135 background: 'none',
136 border: 0,
137 },
138 focused: {
139 fontWeight: 'bold',
140 background: theme.selectOptionItemHover,
141 color: theme.selectOptionItemHoverColor,
142 },
143 hasError: {
144 borderColor: theme.brandDanger,
145 },
146 disabled: {
147 opacity: theme.selectDisabledOpacity,
148 },
149});
150
151class SelectComponent extends Component<IProps> {
152 public static defaultProps = {
153 onChange: () => {},
154 showLabel: true,
155 disabled: false,
156 error: '',
157 };
158
159 state = {
160 open: false,
161 value: '',
162 needle: '',
163 selected: 0,
164 options: null,
165 };
166
167 private componentRef = createRef<HTMLDivElement>();
168
169 private inputRef = createRef<HTMLInputElement>();
170
171 private searchInputRef = createRef<HTMLInputElement>();
172
173 private scrollContainerRef = createRef<HTMLDivElement>();
174
175 private activeOptionRef = createRef<HTMLDivElement>();
176
177 private keyListener: any;
178
179 componentWillReceiveProps(nextProps: IProps) {
180 if (nextProps.value && nextProps.value !== this.props.value) {
181 this.setState({
182 value: nextProps.value,
183 });
184 }
185 }
186
187 componentDidUpdate() {
188 const { open } = this.state;
189
190 if (this.searchInputRef && this.searchInputRef.current && open) {
191 this.searchInputRef.current.focus();
192 }
193 }
194
195 componentDidMount() {
196 if (this.inputRef && this.inputRef.current) {
197 const { data } = this.props;
198
199 if (data) {
200 Object.keys(data).map(
201 key => (this.inputRef.current!.dataset[key] = data[key]),
202 );
203 }
204 }
205
206 window.addEventListener('keydown', this.arrowKeysHandler.bind(this), false);
207 }
208
209 componentWillMount() {
210 const { value } = this.props;
211
212 if (this.componentRef && this.componentRef.current) {
213 this.componentRef.current.removeEventListener(
214 'keydown',
215 this.keyListener,
216 );
217 }
218
219 if (value) {
220 this.setState({
221 value,
222 });
223 }
224
225 this.setFilter();
226 }
227
228 componentWillUnmount() {
229 // eslint-disable-next-line unicorn/no-invalid-remove-event-listener
230 window.removeEventListener('keydown', this.arrowKeysHandler.bind(this));
231 }
232
233 setFilter(needle = '') {
234 const { options } = this.props;
235
236 let filteredOptions = {};
237 if (needle) {
238 Object.keys(options).map(key => {
239 if (
240 key.toLocaleLowerCase().startsWith(needle.toLocaleLowerCase()) ||
241 options[key]
242 .toLocaleLowerCase()
243 .startsWith(needle.toLocaleLowerCase())
244 ) {
245 Object.assign(filteredOptions, {
246 [`${key}`]: options[key],
247 });
248 }
249 });
250 } else {
251 filteredOptions = options;
252 }
253
254 this.setState({
255 needle,
256 options: filteredOptions,
257 selected: 0,
258 });
259 }
260
261 select(key: string) {
262 this.setState(() => ({
263 value: key,
264 open: false,
265 }));
266
267 this.setFilter();
268
269 if (this.props.onChange) {
270 this.props.onChange(key as any);
271 }
272 }
273
274 arrowKeysHandler(e: KeyboardEvent) {
275 const { selected, open, options } = this.state;
276
277 if (!open) return;
278
279 if (e.keyCode === 38 || e.keyCode === 40) {
280 e.preventDefault();
281 }
282
283 if (this.componentRef && this.componentRef.current) {
284 if (e.keyCode === 38 && selected > 0) {
285 this.setState((state: IState) => ({
286 selected: state.selected - 1,
287 }));
288 } else if (
289 e.keyCode === 40 &&
290 selected < Object.keys(options!).length - 1
291 ) {
292 this.setState((state: IState) => ({
293 selected: state.selected + 1,
294 }));
295 } else if (e.keyCode === 13) {
296 this.select(Object.keys(options!)[selected]);
297 }
298
299 if (
300 this.activeOptionRef &&
301 this.activeOptionRef.current &&
302 this.scrollContainerRef &&
303 this.scrollContainerRef.current
304 ) {
305 const containerTopOffset = this.scrollContainerRef.current.offsetTop;
306 const optionTopOffset = this.activeOptionRef.current.offsetTop;
307
308 const topOffset = optionTopOffset - containerTopOffset;
309
310 this.scrollContainerRef.current.scrollTop = topOffset - 35;
311 }
312 }
313
314 switch (e.keyCode) {
315 case 37:
316 case 39:
317 case 38:
318 case 40: // Arrow keys
319 case 32:
320 break; // Space
321 default:
322 break; // do not block other keys
323 }
324 }
325
326 render() {
327 const {
328 actionText,
329 classes,
330 className,
331 defaultValue,
332 disabled,
333 error,
334 id,
335 inputClassName,
336 name,
337 label,
338 showLabel,
339 showSearch,
340 onChange,
341 required,
342 } = this.props;
343
344 const { open, needle, value, selected, options } = this.state;
345
346 let selection = '';
347 if (!value && defaultValue && options![defaultValue]) {
348 selection = options![defaultValue];
349 } else if (value && options![value]) {
350 selection = options![value];
351 } else {
352 selection = actionText;
353 }
354
355 return (
356 <Wrapper className={className} identifier="franz-select">
357 <Label
358 title={label}
359 showLabel={showLabel}
360 htmlFor={id}
361 className={classes.label}
362 isRequired={required}
363 >
364 <div
365 className={classnames({
366 [`${classes.hasError}`]: error,
367 [`${classes.disabled}`]: disabled,
368 })}
369 ref={this.componentRef}
370 >
371 <button
372 type="button"
373 className={classnames({
374 [`${inputClassName}`]: inputClassName,
375 [`${classes.select}`]: true,
376 [`${classes.hasError}`]: error,
377 })}
378 onClick={
379 !disabled
380 ? () =>
381 this.setState((state: IState) => ({
382 open: !state.open,
383 }))
384 : () => {}
385 }
386 >
387 {selection}
388 <Icon
389 path={mdiArrowRightDropCircleOutline}
390 size={0.8}
391 className={classnames({
392 [`${classes.toggle}`]: true,
393 [`${classes.toggleOpened}`]: open,
394 })}
395 />
396 </button>
397 {showSearch && open && (
398 <div className={classes.searchContainer}>
399 <Icon path={mdiMagnify} size={0.8} />
400 <input
401 type="text"
402 value={needle}
403 onChange={e => this.setFilter(e.currentTarget.value)}
404 placeholder="Search"
405 className={classes.search}
406 ref={this.searchInputRef}
407 />
408 {needle && (
409 <button
410 type="button"
411 className={classes.clearNeedle}
412 onClick={() => this.setFilter()}
413 >
414 <Icon path={mdiCloseCircle} size={0.7} />
415 </button>
416 )}
417 </div>
418 )}
419 <div
420 className={classnames({
421 [`${classes.popup}`]: true,
422 [`${classes.open}`]: open,
423 })}
424 ref={this.scrollContainerRef}
425 >
426 {Object.keys(options!).map((key, i) => (
427 <div
428 key={key}
429 onClick={() => this.select(key)}
430 className={classnames({
431 [`${classes.option}`]: true,
432 [`${classes.selected}`]: options![key] === selection,
433 [`${classes.focused}`]: selected === i,
434 })}
435 onMouseOver={() => this.setState({ selected: i })}
436 ref={selected === i ? this.activeOptionRef : null}
437 >
438 {options![key]}
439 </div>
440 ))}
441 </div>
442 </div>
443 <input
444 className={classes.input}
445 id={id}
446 name={name}
447 type="hidden"
448 defaultValue={value}
449 onChange={onChange}
450 disabled={disabled}
451 ref={this.inputRef}
452 />
453 </Label>
454 {error && <Error message={error} />}
455 </Wrapper>
456 );
457 }
458}
459
460export const Select = injectStyle(styles)(SelectComponent);
diff --git a/packages/forms/src/textarea/index.tsx b/packages/forms/src/textarea/index.tsx
deleted file mode 100644
index 1b16698eb..000000000
--- a/packages/forms/src/textarea/index.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
1import classnames from 'classnames';
2import { Component, createRef, TextareaHTMLAttributes } from 'react';
3import injectSheet from 'react-jss';
4
5import { IFormField, IWithStyle } from '../typings/generic';
6
7import { Error } from '../error';
8import { Label } from '../label';
9import { Wrapper } from '../wrapper';
10
11import styles from './styles';
12
13interface IData {
14 [index: string]: string;
15}
16
17interface IProps
18 extends TextareaHTMLAttributes<HTMLTextAreaElement>,
19 IFormField,
20 IWithStyle {
21 focus?: boolean;
22 data: IData;
23 textareaClassName?: string;
24}
25
26class TextareaComponent extends Component<IProps> {
27 static defaultProps = {
28 focus: false,
29 onChange: () => {},
30 onBlur: () => {},
31 onFocus: () => {},
32 showLabel: true,
33 disabled: false,
34 rows: 5,
35 };
36
37 private textareaRef = createRef<HTMLTextAreaElement>();
38
39 componentDidMount() {
40 const { data } = this.props;
41
42 if (this.textareaRef && this.textareaRef.current && data) {
43 Object.keys(data).map(
44 key => (this.textareaRef.current!.dataset[key] = data[key]),
45 );
46 }
47 }
48
49 onChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
50 const { onChange } = this.props;
51
52 if (onChange) {
53 onChange(e);
54 }
55 }
56
57 render() {
58 const {
59 classes,
60 className,
61 disabled,
62 error,
63 id,
64 textareaClassName,
65 label,
66 showLabel,
67 value,
68 name,
69 placeholder,
70 spellCheck,
71 onBlur,
72 onFocus,
73 minLength,
74 maxLength,
75 required,
76 rows,
77 noMargin,
78 } = this.props;
79
80 return (
81 <Wrapper
82 className={className}
83 identifier="franz-textarea"
84 noMargin={noMargin}
85 >
86 <Label
87 title={label}
88 showLabel={showLabel}
89 htmlFor={id}
90 className={classes.label}
91 isRequired={required}
92 >
93 <div
94 className={classnames({
95 [`${textareaClassName}`]: textareaClassName,
96 [`${classes.wrapper}`]: true,
97 [`${classes.disabled}`]: disabled,
98 [`${classes.hasError}`]: error,
99 })}
100 >
101 <textarea
102 id={id}
103 name={name}
104 placeholder={placeholder}
105 spellCheck={spellCheck}
106 className={classes.textarea}
107 ref={this.textareaRef}
108 onChange={this.onChange.bind(this)}
109 onFocus={onFocus}
110 onBlur={onBlur}
111 disabled={disabled}
112 minLength={minLength}
113 maxLength={maxLength}
114 rows={rows}
115 >
116 {value}
117 </textarea>
118 </div>
119 </Label>
120 {error && <Error message={error} />}
121 </Wrapper>
122 );
123 }
124}
125
126export const Textarea = injectSheet(styles)(TextareaComponent);
diff --git a/packages/forms/src/textarea/styles.ts b/packages/forms/src/textarea/styles.ts
deleted file mode 100644
index ff3a3202b..000000000
--- a/packages/forms/src/textarea/styles.ts
+++ /dev/null
@@ -1,54 +0,0 @@
1import { Property } from 'csstype';
2
3import { Theme } from '../../../theme';
4
5export default (theme: Theme) => ({
6 label: {
7 '& > div': {
8 marginTop: 5,
9 },
10 },
11 disabled: {
12 opacity: theme.inputDisabledOpacity,
13 },
14 formModifier: {
15 background: 'none',
16 border: 0,
17 borderLeft: theme.inputBorder,
18 padding: '4px 20px 0',
19 outline: 'none',
20
21 '&:active': {
22 opacity: 0.5,
23 },
24
25 '& svg': {
26 fill: theme.inputModifierColor,
27 },
28 },
29 textarea: {
30 background: 'none',
31 border: 0,
32 fontSize: theme.uiFontSize,
33 outline: 'none',
34 padding: 8,
35 width: '100%',
36 color: theme.inputColor,
37
38 '&::placeholder': {
39 color: theme.inputPlaceholderColor,
40 },
41 },
42 wrapper: {
43 background: theme.inputBackground,
44 border: theme.inputBorder,
45 borderRadius: theme.borderRadiusSmall,
46 boxSizing: 'border-box' as Property.BoxSizing,
47 display: 'flex',
48 order: 1,
49 width: '100%',
50 },
51 hasError: {
52 borderColor: theme.brandDanger,
53 },
54});
diff --git a/packages/forms/src/toggle/index.tsx b/packages/forms/src/toggle/index.tsx
deleted file mode 100644
index f9ef5775a..000000000
--- a/packages/forms/src/toggle/index.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
1import classnames from 'classnames';
2import { Property } from 'csstype';
3import { Component, InputHTMLAttributes } from 'react';
4import injectStyle from 'react-jss';
5
6import { IFormField, IWithStyle } from '../typings/generic';
7import { Theme } from '../../../theme';
8
9import { Error } from '../error';
10import { Label } from '../label';
11import { Wrapper } from '../wrapper';
12
13interface IProps
14 extends InputHTMLAttributes<HTMLInputElement>,
15 IFormField,
16 IWithStyle {
17 className?: string;
18}
19
20let buttonTransition: string = 'none';
21
22if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
23 buttonTransition = 'all .5s';
24}
25
26const styles = (theme: Theme) => ({
27 toggle: {
28 background: theme.toggleBackground,
29 borderRadius: theme.borderRadius,
30 height: theme.toggleHeight,
31 position: 'relative' as Property.Position,
32 width: theme.toggleWidth,
33 },
34 button: {
35 background: theme.toggleButton,
36 borderRadius: '100%',
37 boxShadow: '0 1px 4px rgba(0, 0, 0, .3)',
38 width: theme.toggleHeight - 2,
39 height: theme.toggleHeight - 2,
40 left: 1,
41 top: 1,
42 position: 'absolute' as Property.Position,
43 transition: buttonTransition,
44 },
45 buttonActive: {
46 background: theme.toggleButtonActive,
47 left: theme.toggleWidth - theme.toggleHeight + 1,
48 },
49 input: {
50 visibility: 'hidden' as any,
51 },
52 disabled: {
53 opacity: theme.inputDisabledOpacity,
54 },
55 toggleLabel: {
56 display: 'flex',
57 alignItems: 'center',
58
59 '& > span': {
60 order: 1,
61 marginLeft: 15,
62 },
63 },
64});
65
66class ToggleComponent extends Component<IProps> {
67 public static defaultProps = {
68 onChange: () => {},
69 showLabel: true,
70 disabled: false,
71 error: '',
72 };
73
74 render() {
75 const {
76 classes,
77 className,
78 disabled,
79 error,
80 id,
81 label,
82 showLabel,
83 checked,
84 value,
85 onChange,
86 } = this.props;
87
88 return (
89 <Wrapper className={className} identifier="franz-toggle">
90 <Label
91 title={label}
92 showLabel={showLabel}
93 htmlFor={id}
94 className={classes.toggleLabel}
95 >
96 <div
97 className={classnames({
98 [`${classes.toggle}`]: true,
99 [`${classes.disabled}`]: disabled,
100 })}
101 >
102 <div
103 className={classnames({
104 [`${classes.button}`]: true,
105 [`${classes.buttonActive}`]: checked,
106 })}
107 />
108 <input
109 className={classes.input}
110 id={id}
111 type="checkbox"
112 checked={checked}
113 value={value}
114 onChange={onChange}
115 disabled={disabled}
116 />
117 </div>
118 </Label>
119 {error && <Error message={error} />}
120 </Wrapper>
121 );
122 }
123}
124
125export const Toggle = injectStyle(styles)(ToggleComponent);
diff --git a/packages/forms/src/typings/generic.ts b/packages/forms/src/typings/generic.ts
deleted file mode 100644
index 0fd0cdbf3..000000000
--- a/packages/forms/src/typings/generic.ts
+++ /dev/null
@@ -1,19 +0,0 @@
1import { Classes } from 'jss';
2
3import { Theme } from '../../../theme';
4
5export interface IFormField {
6 showLabel?: boolean;
7 label?: string;
8 error?: string;
9 required?: boolean;
10 noMargin?: boolean;
11}
12
13export interface IWithStyle {
14 classes: Classes;
15 theme: Theme;
16}
17
18export type Merge<M, N> = Omit<M, Extract<keyof M, keyof N>> & N;
19export 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
deleted file mode 100644
index ffcd6fe0b..000000000
--- a/packages/forms/src/wrapper/index.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
1import classnames from 'classnames';
2import { Component, ReactNode } from 'react';
3import injectStyle from 'react-jss';
4import { IWithStyle } from '../typings/generic';
5
6interface IProps extends IWithStyle {
7 children: ReactNode;
8 className?: string;
9 identifier: string;
10 noMargin?: boolean;
11}
12
13const styles = {
14 container: {
15 marginBottom: (props: IProps) => (props.noMargin ? 0 : 20),
16 },
17};
18
19class WrapperComponent extends Component<IProps> {
20 render() {
21 const { children, classes, className, identifier } = 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/tsconfig.json b/packages/forms/tsconfig.json
deleted file mode 100644
index 015581136..000000000
--- a/packages/forms/tsconfig.json
+++ /dev/null
@@ -1,12 +0,0 @@
1{
2 "extends": "../../tsconfig.json",
3 "compilerOptions": {
4 "outDir": "lib",
5 "rootDir": "src"
6 },
7 "references": [
8 {
9 "path": "../theme"
10 }
11 ]
12}