@sio-group/form-react 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -82
- package/README.md +2 -2
- package/dist/index.cjs +268 -18
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +258 -17
- package/package.json +6 -5
- package/src/assets/scss/components/button.scss +164 -0
- package/src/assets/scss/components/checkbox.scss +90 -0
- package/src/assets/scss/components/color.scss +29 -0
- package/src/assets/scss/components/form-field.scss +34 -0
- package/src/assets/scss/components/form-states.scss +80 -0
- package/src/assets/scss/components/grid.scss +134 -0
- package/src/assets/scss/components/input.scss +112 -0
- package/src/assets/scss/components/link.scss +66 -0
- package/src/assets/scss/components/radio.scss +104 -0
- package/src/assets/scss/components/range.scss +52 -0
- package/src/assets/scss/components/select.scss +35 -0
- package/src/assets/scss/components/upload.scss +52 -0
- package/src/assets/scss/index.scss +19 -0
- package/src/assets/scss/tokens/_colors.scss +49 -0
- package/src/assets/scss/tokens/_form.scss +6 -0
- package/src/assets/scss/utilities/_mixins.scss +6 -0
- package/src/components/Button/index.tsx +106 -0
- package/src/components/Fields/Checkbox/index.tsx +59 -0
- package/src/components/Fields/Input/DateInput/index.tsx +95 -0
- package/src/components/Fields/Input/FileInput/index.tsx +169 -0
- package/src/components/Fields/Input/Input.tsx +45 -0
- package/src/components/Fields/Input/NumberInput/index.tsx +169 -0
- package/src/components/Fields/Input/RangeInput/index.tsx +77 -0
- package/src/components/Fields/Input/TextInput/index.tsx +65 -0
- package/src/components/Fields/InputWrapper/index.tsx +78 -0
- package/src/components/Fields/Radio/index.tsx +82 -0
- package/src/components/Fields/Select/index.tsx +103 -0
- package/src/components/Fields/Textarea/index.tsx +70 -0
- package/src/components/Fields/index.tsx +11 -0
- package/src/components/Form.tsx +163 -0
- package/src/components/Icon/index.tsx +16 -0
- package/src/components/Link/index.tsx +106 -0
- package/src/hooks/useConnectionStatus.ts +20 -0
- package/src/hooks/useForm.ts +230 -0
- package/src/index.ts +15 -0
- package/src/types/field-props.d.ts +94 -0
- package/src/types/field-setters.d.ts +6 -0
- package/src/types/field-state.d.ts +21 -0
- package/src/types/form-config.d.ts +30 -0
- package/src/types/form-layout.d.ts +6 -0
- package/src/types/index.ts +18 -0
- package/src/types/ui-props.d.ts +33 -0
- package/src/types/use-form-options.d.ts +3 -0
- package/src/utils/create-field-props.ts +115 -0
- package/src/utils/create-field-state.ts +99 -0
- package/src/utils/custom-icons.tsx +145 -0
- package/src/utils/file-type-icon.ts +63 -0
- package/src/utils/get-accept-string.ts +24 -0
- package/src/utils/get-column-classes.ts +21 -0
- package/src/utils/get-file-size.ts +9 -0
- package/src/utils/parse-date.ts +36 -0
- package/src/utils/slugify.ts +9 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ButtonProps } from "../../types";
|
|
3
|
+
|
|
4
|
+
const ButtonComponent: React.FC<ButtonProps> = ({
|
|
5
|
+
type = 'button',
|
|
6
|
+
label,
|
|
7
|
+
onClick,
|
|
8
|
+
variant = 'primary',
|
|
9
|
+
color = 'default',
|
|
10
|
+
size = 'md',
|
|
11
|
+
block = false,
|
|
12
|
+
loading = false,
|
|
13
|
+
disabled = false,
|
|
14
|
+
className = '',
|
|
15
|
+
ariaLabel = '',
|
|
16
|
+
style = {},
|
|
17
|
+
children,
|
|
18
|
+
}: ButtonProps) => {
|
|
19
|
+
const isDisabled: boolean = disabled || loading;
|
|
20
|
+
|
|
21
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
22
|
+
if (isDisabled) {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
onClick?.(e);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const buttonClasses = [
|
|
31
|
+
'btn',
|
|
32
|
+
`btn--${variant}`,
|
|
33
|
+
`btn--${size}`,
|
|
34
|
+
`btn--${color}`,
|
|
35
|
+
block && 'btn--block',
|
|
36
|
+
loading && 'btn--loading',
|
|
37
|
+
isDisabled && 'btn--disabled',
|
|
38
|
+
className,
|
|
39
|
+
]
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.join(' ');
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<button
|
|
45
|
+
type={type}
|
|
46
|
+
onClick={handleClick}
|
|
47
|
+
className={buttonClasses}
|
|
48
|
+
style={style}
|
|
49
|
+
disabled={isDisabled}
|
|
50
|
+
aria-label={ariaLabel || (label as string)}
|
|
51
|
+
aria-busy={loading}
|
|
52
|
+
aria-disabled={isDisabled}>
|
|
53
|
+
{loading ? (
|
|
54
|
+
<>
|
|
55
|
+
<span className='btn__spinner' aria-hidden='true'>
|
|
56
|
+
<svg viewBox='0 0 20 20'>
|
|
57
|
+
<circle cx='10' cy='10' r='8' />
|
|
58
|
+
</svg>
|
|
59
|
+
</span>
|
|
60
|
+
<span className='btn__loading-text'>Processing...</span>
|
|
61
|
+
</>
|
|
62
|
+
) : (
|
|
63
|
+
<>
|
|
64
|
+
{children}
|
|
65
|
+
{label}
|
|
66
|
+
</>
|
|
67
|
+
)}
|
|
68
|
+
</button>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Button component for user interaction.
|
|
74
|
+
*
|
|
75
|
+
* @component
|
|
76
|
+
* @example
|
|
77
|
+
* // Primaire button
|
|
78
|
+
* <Button label="Save" onClick={handleSave} />
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* // Submit button with loading state
|
|
82
|
+
* <Button
|
|
83
|
+
* type="submit"
|
|
84
|
+
* label="Send"
|
|
85
|
+
* variant="primary"
|
|
86
|
+
* loading
|
|
87
|
+
* />
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* // Button with icon and tekst
|
|
91
|
+
* <Button type="button" onClick={handleClick}>
|
|
92
|
+
* <Icon name="plus" />
|
|
93
|
+
* <span>Add</span>
|
|
94
|
+
* </Button>
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* // Error variant
|
|
98
|
+
* <Button
|
|
99
|
+
* type="button"
|
|
100
|
+
* label="Delete"
|
|
101
|
+
* variant="secondary"
|
|
102
|
+
* color="error"
|
|
103
|
+
* onClick={handleDelete}
|
|
104
|
+
* />
|
|
105
|
+
*/
|
|
106
|
+
export const Button: React.FC<ButtonProps> = React.memo(ButtonComponent);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { FieldProps } from "../../../types";
|
|
3
|
+
import InputWrapper from "../InputWrapper";
|
|
4
|
+
import { Icon } from "../../Icon";
|
|
5
|
+
|
|
6
|
+
export const Checkbox = ({
|
|
7
|
+
value,
|
|
8
|
+
onChange,
|
|
9
|
+
|
|
10
|
+
name,
|
|
11
|
+
id,
|
|
12
|
+
label,
|
|
13
|
+
required,
|
|
14
|
+
setTouched,
|
|
15
|
+
readOnly,
|
|
16
|
+
disabled,
|
|
17
|
+
icon,
|
|
18
|
+
|
|
19
|
+
description,
|
|
20
|
+
focused,
|
|
21
|
+
errors,
|
|
22
|
+
touched,
|
|
23
|
+
type,
|
|
24
|
+
className,
|
|
25
|
+
style,
|
|
26
|
+
}: FieldProps) => {
|
|
27
|
+
return (
|
|
28
|
+
<InputWrapper
|
|
29
|
+
type={type}
|
|
30
|
+
id={id}
|
|
31
|
+
description={description}
|
|
32
|
+
required={required}
|
|
33
|
+
focused={focused}
|
|
34
|
+
disabled={disabled || readOnly}
|
|
35
|
+
hasValue={value === true}
|
|
36
|
+
hasError={errors.length > 0 && touched}
|
|
37
|
+
errors={errors}
|
|
38
|
+
className={className}
|
|
39
|
+
style={style}
|
|
40
|
+
hideLayout>
|
|
41
|
+
<Icon icon={icon} />
|
|
42
|
+
<label htmlFor={id}>
|
|
43
|
+
<input
|
|
44
|
+
name={name}
|
|
45
|
+
type={type}
|
|
46
|
+
id={id}
|
|
47
|
+
checked={value as boolean}
|
|
48
|
+
onChange={(e) => {
|
|
49
|
+
onChange?.(e.target.checked);
|
|
50
|
+
setTouched?.(true);
|
|
51
|
+
}}
|
|
52
|
+
readOnly={readOnly}
|
|
53
|
+
disabled={disabled}
|
|
54
|
+
/>
|
|
55
|
+
<div>{label}</div> {required && <span aria-hidden={true}>*</span>}
|
|
56
|
+
</label>
|
|
57
|
+
</InputWrapper>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { DateFieldProps } from "../../../../types/field-props";
|
|
2
|
+
import { RefObject, useRef } from "react";
|
|
3
|
+
import InputWrapper from "../../InputWrapper";
|
|
4
|
+
import { Button } from "../../../Button";
|
|
5
|
+
import { Icon } from "../../../Icon";
|
|
6
|
+
import { CustomIcons } from '../../../../utils/custom-icons'
|
|
7
|
+
|
|
8
|
+
export const DateInput = ({
|
|
9
|
+
value,
|
|
10
|
+
min,
|
|
11
|
+
max,
|
|
12
|
+
step = 1,
|
|
13
|
+
onChange,
|
|
14
|
+
|
|
15
|
+
name,
|
|
16
|
+
id,
|
|
17
|
+
placeholder,
|
|
18
|
+
label,
|
|
19
|
+
required,
|
|
20
|
+
autocomplete,
|
|
21
|
+
setTouched,
|
|
22
|
+
setFocused,
|
|
23
|
+
readOnly,
|
|
24
|
+
disabled,
|
|
25
|
+
icon,
|
|
26
|
+
|
|
27
|
+
description,
|
|
28
|
+
focused,
|
|
29
|
+
errors,
|
|
30
|
+
touched,
|
|
31
|
+
type,
|
|
32
|
+
className,
|
|
33
|
+
style,
|
|
34
|
+
}: DateFieldProps) => {
|
|
35
|
+
const inputRef: RefObject<HTMLInputElement> =
|
|
36
|
+
useRef<HTMLInputElement>(null);
|
|
37
|
+
|
|
38
|
+
const showPicker = () => {
|
|
39
|
+
if (inputRef.current) {
|
|
40
|
+
inputRef.current.focus();
|
|
41
|
+
if (inputRef.current?.showPicker) {
|
|
42
|
+
inputRef.current.showPicker();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<InputWrapper
|
|
49
|
+
type={type}
|
|
50
|
+
id={id}
|
|
51
|
+
label={label}
|
|
52
|
+
description={description}
|
|
53
|
+
required={required}
|
|
54
|
+
focused={focused}
|
|
55
|
+
disabled={disabled || readOnly}
|
|
56
|
+
hasValue={!!value}
|
|
57
|
+
hasError={errors.length > 0 && touched}
|
|
58
|
+
errors={errors}
|
|
59
|
+
className={className}
|
|
60
|
+
style={style}>
|
|
61
|
+
<Icon icon={icon} />
|
|
62
|
+
<input
|
|
63
|
+
ref={inputRef}
|
|
64
|
+
type={type}
|
|
65
|
+
value={(value ?? '').toString()}
|
|
66
|
+
min={min}
|
|
67
|
+
max={max}
|
|
68
|
+
step={step}
|
|
69
|
+
onChange={e => onChange(e.target.value)}
|
|
70
|
+
name={name}
|
|
71
|
+
id={id}
|
|
72
|
+
placeholder={`${placeholder}${!label && required ? ' *' : ''}`}
|
|
73
|
+
autoComplete={autocomplete ? autocomplete : 'off'}
|
|
74
|
+
onBlur={() => {
|
|
75
|
+
if (setTouched) setTouched(true);
|
|
76
|
+
if (setFocused) setFocused(false);
|
|
77
|
+
}}
|
|
78
|
+
onFocus={() => {
|
|
79
|
+
if (setFocused) setFocused(true);
|
|
80
|
+
}}
|
|
81
|
+
readOnly={readOnly}
|
|
82
|
+
disabled={disabled}
|
|
83
|
+
aria-label={label || placeholder}
|
|
84
|
+
/>
|
|
85
|
+
<Button
|
|
86
|
+
className='form-field__action'
|
|
87
|
+
type='button'
|
|
88
|
+
onClick={showPicker}
|
|
89
|
+
disabled={disabled}
|
|
90
|
+
aria-label={`Open ${type} picker`}
|
|
91
|
+
label={type === 'date' ? CustomIcons.Date() : (type === 'time' ? CustomIcons.Time() : CustomIcons.DateTime())}
|
|
92
|
+
/>
|
|
93
|
+
</InputWrapper>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { ChangeEvent } from "react";
|
|
2
|
+
import { FileFieldProps } from "../../../../types/field-props";
|
|
3
|
+
import InputWrapper from "../../InputWrapper";
|
|
4
|
+
import { Button } from "../../../Button";
|
|
5
|
+
import { Icon } from "../../../Icon";
|
|
6
|
+
import { isValidFile } from "@sio-group/form-validation";
|
|
7
|
+
import { getAccept } from "../../../../utils/get-accept-string";
|
|
8
|
+
import { CustomIcons } from "../../../../utils/custom-icons";
|
|
9
|
+
import { getFileSize } from "../../../../utils/get-file-size";
|
|
10
|
+
import { FileTypeIcon } from "../../../../utils/file-type-icon";
|
|
11
|
+
|
|
12
|
+
export const FileInput = ({
|
|
13
|
+
value,
|
|
14
|
+
onChange,
|
|
15
|
+
onError,
|
|
16
|
+
|
|
17
|
+
accept,
|
|
18
|
+
filesize,
|
|
19
|
+
multiple = false,
|
|
20
|
+
capture,
|
|
21
|
+
onFileRemove,
|
|
22
|
+
onRemoveAll,
|
|
23
|
+
|
|
24
|
+
name,
|
|
25
|
+
id,
|
|
26
|
+
placeholder,
|
|
27
|
+
label,
|
|
28
|
+
required,
|
|
29
|
+
autocomplete,
|
|
30
|
+
setTouched,
|
|
31
|
+
setFocused,
|
|
32
|
+
readOnly,
|
|
33
|
+
disabled,
|
|
34
|
+
icon,
|
|
35
|
+
|
|
36
|
+
description,
|
|
37
|
+
focused,
|
|
38
|
+
errors,
|
|
39
|
+
touched,
|
|
40
|
+
type,
|
|
41
|
+
className,
|
|
42
|
+
style,
|
|
43
|
+
}: FileFieldProps) => {
|
|
44
|
+
const currentFiles = (value as File[]) ?? [];
|
|
45
|
+
|
|
46
|
+
const handleRemove = (index: number) => {
|
|
47
|
+
const removedFile: File = currentFiles[index];
|
|
48
|
+
const newFiles: File[] = currentFiles.filter((_, i) => i !== index);
|
|
49
|
+
onChange(newFiles);
|
|
50
|
+
onFileRemove?.(removedFile, index, newFiles);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const handleRemoveAll = () => {
|
|
54
|
+
const removedFiles: File[] = [...currentFiles];
|
|
55
|
+
onChange([]);
|
|
56
|
+
onRemoveAll?.(removedFiles);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
60
|
+
const files: File[] = Array.from(e.target.files ?? []);
|
|
61
|
+
const validator = isValidFile(filesize, getAccept(accept));
|
|
62
|
+
|
|
63
|
+
let fileList: File[] = [...currentFiles];
|
|
64
|
+
const errorList: string[] = [];
|
|
65
|
+
|
|
66
|
+
for (let file of files) {
|
|
67
|
+
const error: string | null = validator(file);
|
|
68
|
+
if (error) {
|
|
69
|
+
errorList.push(error);
|
|
70
|
+
} else {
|
|
71
|
+
if (multiple) {
|
|
72
|
+
fileList.push(file);
|
|
73
|
+
} else {
|
|
74
|
+
fileList = [file]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
onChange(fileList);
|
|
80
|
+
if (onError) onError(errorList ?? [])
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<>
|
|
85
|
+
<InputWrapper
|
|
86
|
+
type={type}
|
|
87
|
+
id={id}
|
|
88
|
+
label={label}
|
|
89
|
+
description={description}
|
|
90
|
+
required={required}
|
|
91
|
+
focused={focused}
|
|
92
|
+
disabled={disabled || readOnly}
|
|
93
|
+
hasValue={currentFiles.length > 0}
|
|
94
|
+
hasError={errors.length > 0 && touched}
|
|
95
|
+
errors={errors}
|
|
96
|
+
className={className}
|
|
97
|
+
style={style}>
|
|
98
|
+
<Icon icon={icon} />
|
|
99
|
+
<input
|
|
100
|
+
type={type}
|
|
101
|
+
name={name}
|
|
102
|
+
id={id}
|
|
103
|
+
placeholder={`${placeholder}${!label && required ? ' *' : ''}`}
|
|
104
|
+
autoComplete={autocomplete ? autocomplete : 'off'}
|
|
105
|
+
onChange={handleChange}
|
|
106
|
+
onBlur={() => {
|
|
107
|
+
if (setTouched) setTouched(true);
|
|
108
|
+
if (setFocused) setFocused(false);
|
|
109
|
+
}}
|
|
110
|
+
onFocus={() => {
|
|
111
|
+
if (setFocused) setFocused(true);
|
|
112
|
+
}}
|
|
113
|
+
accept={getAccept(accept)}
|
|
114
|
+
capture={capture}
|
|
115
|
+
readOnly={readOnly}
|
|
116
|
+
disabled={disabled}
|
|
117
|
+
aria-label={label || placeholder}
|
|
118
|
+
multiple={multiple}
|
|
119
|
+
/>
|
|
120
|
+
<Button
|
|
121
|
+
className='form-field__action'
|
|
122
|
+
type='button'
|
|
123
|
+
onClick={() => {
|
|
124
|
+
if (setTouched) setTouched(true);
|
|
125
|
+
document.getElementById(id)?.click();
|
|
126
|
+
}}
|
|
127
|
+
disabled={disabled}
|
|
128
|
+
aria-label='Select files'
|
|
129
|
+
label={CustomIcons.FileUpload()}
|
|
130
|
+
/>
|
|
131
|
+
</InputWrapper>
|
|
132
|
+
|
|
133
|
+
{currentFiles.length !== 0 && (
|
|
134
|
+
<div className='form-field__upload-file-section'>
|
|
135
|
+
{currentFiles.map((file, index) => (
|
|
136
|
+
<div className='form-field__upload-file' key={index}>
|
|
137
|
+
<div className='form-field__upload-file-label'>
|
|
138
|
+
{FileTypeIcon(file.type || 'text/uri-list')}
|
|
139
|
+
<span>
|
|
140
|
+
{file.name}
|
|
141
|
+
{file.size
|
|
142
|
+
? ` (${getFileSize(file.size)})`
|
|
143
|
+
: ''}
|
|
144
|
+
</span>
|
|
145
|
+
</div>
|
|
146
|
+
<div className='form-field__upload-file__buttons'>
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
className='form-field__upload-file-remove-button'
|
|
150
|
+
onClick={() => handleRemove(index)}>
|
|
151
|
+
{CustomIcons.TrashIcon()}
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
))}
|
|
156
|
+
<div className='form-field__upload-buttons'>
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
className='form-field__upload-remove-all-button'
|
|
160
|
+
onClick={handleRemoveAll}>
|
|
161
|
+
{CustomIcons.TrashIcon()}
|
|
162
|
+
<span>Verwijder alles</span>
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
</>
|
|
168
|
+
);
|
|
169
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React, { memo, useMemo } from 'react';
|
|
2
|
+
import { NumberInput } from "./NumberInput";
|
|
3
|
+
import { RangeInput } from "./RangeInput";
|
|
4
|
+
import { DateInput } from "./DateInput";
|
|
5
|
+
import { FileInput } from "./FileInput";
|
|
6
|
+
import { TextInput } from "./TextInput";
|
|
7
|
+
import {
|
|
8
|
+
DateFieldProps,
|
|
9
|
+
FieldProps,
|
|
10
|
+
FileFieldProps,
|
|
11
|
+
NumberFieldProps,
|
|
12
|
+
RangeFieldProps
|
|
13
|
+
} from "../../../types";
|
|
14
|
+
|
|
15
|
+
export const Input = memo(
|
|
16
|
+
(props: FieldProps) => {
|
|
17
|
+
const { type = 'text' } = props;
|
|
18
|
+
|
|
19
|
+
return useMemo(() => {
|
|
20
|
+
switch (props.type) {
|
|
21
|
+
case 'number':
|
|
22
|
+
return <NumberInput {...props as NumberFieldProps} />;
|
|
23
|
+
case 'range':
|
|
24
|
+
return <RangeInput {...props as RangeFieldProps} />;
|
|
25
|
+
case 'date':
|
|
26
|
+
case 'time':
|
|
27
|
+
case 'datetime-local':
|
|
28
|
+
return <DateInput{...props as DateFieldProps} />;
|
|
29
|
+
case 'file':
|
|
30
|
+
return <FileInput {...props as FileFieldProps} />;
|
|
31
|
+
case 'hidden':
|
|
32
|
+
return (
|
|
33
|
+
<input
|
|
34
|
+
type={props.type}
|
|
35
|
+
name={props.name}
|
|
36
|
+
id={props.id}
|
|
37
|
+
value={props.value as string}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
default:
|
|
41
|
+
return <TextInput {...props} />;
|
|
42
|
+
}
|
|
43
|
+
}, [type, props]);
|
|
44
|
+
},
|
|
45
|
+
);
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { ChangeEvent, KeyboardEvent } from "react";
|
|
2
|
+
import { NumberFieldProps } from "../../../../types/field-props";
|
|
3
|
+
import InputWrapper from "../../InputWrapper";
|
|
4
|
+
import { Icon } from "../../../Icon";
|
|
5
|
+
import { Button } from "../../../Button";
|
|
6
|
+
|
|
7
|
+
export const NumberInput = ({
|
|
8
|
+
value,
|
|
9
|
+
min,
|
|
10
|
+
max,
|
|
11
|
+
step = 1,
|
|
12
|
+
spinner = true,
|
|
13
|
+
onChange,
|
|
14
|
+
|
|
15
|
+
name,
|
|
16
|
+
id,
|
|
17
|
+
placeholder,
|
|
18
|
+
label,
|
|
19
|
+
required,
|
|
20
|
+
autocomplete,
|
|
21
|
+
setTouched,
|
|
22
|
+
setFocused,
|
|
23
|
+
readOnly,
|
|
24
|
+
disabled,
|
|
25
|
+
icon,
|
|
26
|
+
|
|
27
|
+
description,
|
|
28
|
+
focused,
|
|
29
|
+
errors,
|
|
30
|
+
touched,
|
|
31
|
+
type,
|
|
32
|
+
className,
|
|
33
|
+
style,
|
|
34
|
+
}: NumberFieldProps) => {
|
|
35
|
+
const getPrecision = (step: number) => {
|
|
36
|
+
const stepStr: string = step.toString();
|
|
37
|
+
if (!stepStr.includes(".")) return 0;
|
|
38
|
+
return stepStr.split(".")[1].length;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleIncrement = () => {
|
|
42
|
+
const newValue: number = Math.max(min ?? 0, Math.min(Number(value) + step, max ?? Infinity));
|
|
43
|
+
onChange(newValue.toFixed(getPrecision(step)));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handleDecrement = () => {
|
|
47
|
+
const newValue: number = Math.max(Number(value) - step, min ?? -Infinity);
|
|
48
|
+
onChange(newValue.toFixed(getPrecision(step)));
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
52
|
+
if (e.key === 'ArrowUp') {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
handleIncrement();
|
|
55
|
+
} else if (e.key === 'ArrowDown') {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
handleDecrement();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
62
|
+
let value: string = ''
|
|
63
|
+
if (e.target.value) {
|
|
64
|
+
value = e.target.valueAsNumber.toString();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
onChange(value)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<InputWrapper
|
|
72
|
+
type={type}
|
|
73
|
+
id={id}
|
|
74
|
+
label={label}
|
|
75
|
+
description={description}
|
|
76
|
+
required={required}
|
|
77
|
+
focused={focused}
|
|
78
|
+
disabled={disabled || readOnly}
|
|
79
|
+
hasValue={!!value}
|
|
80
|
+
hasError={errors.length > 0 && touched}
|
|
81
|
+
errors={errors}
|
|
82
|
+
className={className}
|
|
83
|
+
style={style}>
|
|
84
|
+
{spinner && spinner === 'horizontal' && (
|
|
85
|
+
<Button
|
|
86
|
+
type='button'
|
|
87
|
+
variant="link"
|
|
88
|
+
className="form-field__spinner-btn"
|
|
89
|
+
onClick={handleDecrement}
|
|
90
|
+
disabled={
|
|
91
|
+
disabled ||
|
|
92
|
+
(min !== undefined && Number(value) <= min)
|
|
93
|
+
}
|
|
94
|
+
aria-label='Decrease value'
|
|
95
|
+
label="-"
|
|
96
|
+
/>
|
|
97
|
+
)}
|
|
98
|
+
<Icon icon={icon} />
|
|
99
|
+
<input
|
|
100
|
+
type='number'
|
|
101
|
+
value={value !== '' ? Number(value) : ''}
|
|
102
|
+
min={min}
|
|
103
|
+
max={max}
|
|
104
|
+
step={step}
|
|
105
|
+
onChange={handleChange}
|
|
106
|
+
onKeyDown={(e) => {
|
|
107
|
+
handleKeyDown(e);
|
|
108
|
+
setTouched?.(true)
|
|
109
|
+
}}
|
|
110
|
+
aria-valuemin={min}
|
|
111
|
+
aria-valuemax={max}
|
|
112
|
+
aria-valuenow={Number(value)}
|
|
113
|
+
name={name}
|
|
114
|
+
id={id}
|
|
115
|
+
placeholder={`${placeholder ?? ''}${!label && required ? ' *' : ''}`}
|
|
116
|
+
autoComplete={autocomplete ? autocomplete : 'off'}
|
|
117
|
+
onBlur={() => {
|
|
118
|
+
setTouched?.(true);
|
|
119
|
+
setFocused?.(false);
|
|
120
|
+
}}
|
|
121
|
+
onFocus={() => {
|
|
122
|
+
setFocused?.(true);
|
|
123
|
+
}}
|
|
124
|
+
readOnly={readOnly}
|
|
125
|
+
disabled={disabled}
|
|
126
|
+
aria-label={label || placeholder}
|
|
127
|
+
style={spinner && spinner === 'horizontal' ? {textAlign: 'center'} : {}}
|
|
128
|
+
/>
|
|
129
|
+
|
|
130
|
+
{spinner &&
|
|
131
|
+
(spinner === 'horizontal'
|
|
132
|
+
? <Button
|
|
133
|
+
type='button'
|
|
134
|
+
variant="link"
|
|
135
|
+
className="form-field__spinner-btn"
|
|
136
|
+
onClick={handleIncrement}
|
|
137
|
+
disabled={
|
|
138
|
+
disabled ||
|
|
139
|
+
(max !== undefined && Number(value) >= max)
|
|
140
|
+
}
|
|
141
|
+
aria-label='Increase value'
|
|
142
|
+
label="+"
|
|
143
|
+
/>
|
|
144
|
+
: <div className='form-field__spinner' aria-hidden='true'>
|
|
145
|
+
<Button
|
|
146
|
+
type='button'
|
|
147
|
+
onClick={handleIncrement}
|
|
148
|
+
disabled={
|
|
149
|
+
disabled ||
|
|
150
|
+
(max !== undefined && Number(value) >= max)
|
|
151
|
+
}
|
|
152
|
+
aria-label='Increase value'
|
|
153
|
+
label="▲"
|
|
154
|
+
/>
|
|
155
|
+
<Button
|
|
156
|
+
type='button'
|
|
157
|
+
onClick={handleDecrement}
|
|
158
|
+
disabled={
|
|
159
|
+
disabled ||
|
|
160
|
+
(min !== undefined && Number(value) <= min)
|
|
161
|
+
}
|
|
162
|
+
aria-label='Decrease value'
|
|
163
|
+
label="▼"
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
</InputWrapper>
|
|
168
|
+
);
|
|
169
|
+
};
|