Skip to content

Commit 1553318

Browse files
authored
feat(FileInput): added new input file input (#24)
1 parent d78ccc1 commit 1553318

23 files changed

+261
-3
lines changed

docs/spec.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ type Spec = ArraySpec | BooleanSpec | NumberSpec | ObjectSpec | StringSpec;
111111
| viewSpec.monacoParams | `object` | | [Parameters](#monacoparams) that must be passed to Monaco editor |
112112
| viewSpec.placeholder | `string` | | A short hint displayed in the field before the user enters the value |
113113
| viewSpec.themeLabel | `'normal'` `'info'` `'danger'` `'warning'` `'success'` `'unknown'` | | Label color |
114+
| viewSpec.fileInput | `object` | | [Parameters](#FileInput) that must be passed to file input |
114115

115116
#### SizeParams
116117

@@ -127,6 +128,14 @@ type Spec = ArraySpec | BooleanSpec | NumberSpec | ObjectSpec | StringSpec;
127128
| language | `string` | yes | Syntax highlighting language |
128129
| fontSize | `string` | | Font size |
129130

131+
#### FileInput
132+
133+
| Property | Type | Required | Description |
134+
| :----------- | :---------------------------------------------------------------------------- | :------: | :------------------------------------------------------------------------------------- |
135+
| accept | `string` | | Acceptable file extensions, for example: `'.png'`, `'audio/\*'`, `'.jpg, .jpeg, .png'` |
136+
| readAsMethod | `'readAsArrayBuffer'` `'readAsBinaryString'` `'readAsDataURL'` `'readAsText'` | | File reading method |
137+
| ignoreText | `boolean` | | For `true`, will show the `File uploaded` stub instead of the field value |
138+
130139
#### Link
131140

132141
A component that serves as a wrapper for the value, if necessary, rendering the value as a link.

src/lib/core/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,9 @@ export enum SpecTypes {
55
Object = 'object',
66
String = 'string',
77
}
8+
9+
export type ReadAsMethod =
10+
| 'readAsArrayBuffer'
11+
| 'readAsBinaryString'
12+
| 'readAsDataURL'
13+
| 'readAsText';

src/lib/core/types/specs.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {LabelProps} from '@gravity-ui/uikit';
22

3-
import {SpecTypes} from '../constants';
3+
import {ReadAsMethod, SpecTypes} from '../constants';
44

55
import {ArrayValue, ObjectValue} from './';
66

@@ -118,6 +118,11 @@ export interface StringSpec<LinkType = any> {
118118
hideValues?: string[];
119119
placeholder?: string;
120120
themeLabel?: LabelProps['theme'];
121+
fileInput?: {
122+
accept?: string;
123+
readAsMethod?: ReadAsMethod;
124+
ignoreText?: boolean;
125+
};
121126
};
122127
}
123128

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@import '../../../styles/variables.scss';
2+
3+
.#{$ns}file-input {
4+
display: flex;
5+
6+
&__input {
7+
opacity: 0;
8+
position: absolute;
9+
clip: rect(0 0 0 0);
10+
width: 1px;
11+
height: 1px;
12+
margin: -1px;
13+
}
14+
15+
&__file-name {
16+
display: block;
17+
margin: auto 10px;
18+
max-width: 160px;
19+
text-overflow: ellipsis;
20+
overflow: hidden;
21+
white-space: nowrap;
22+
color: var(--yc-color-text-secondary);
23+
}
24+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import React from 'react';
2+
3+
import {Xmark} from '@gravity-ui/icons';
4+
import {Button, Icon, Label} from '@gravity-ui/uikit';
5+
6+
import {StringInputProps} from '../../../../core';
7+
import i18n from '../../../../kit/i18n';
8+
import {block} from '../../../utils';
9+
10+
import {readFile} from './utils';
11+
12+
import './FileInput.scss';
13+
14+
const b = block('file-input');
15+
16+
export const FileInput: React.FC<StringInputProps> = ({input, spec}) => {
17+
const {value, onChange} = input;
18+
19+
const inputRef = React.useRef<HTMLInputElement>(null);
20+
21+
const [fileName, setFileName] = React.useState<string>('');
22+
23+
const handleClick = React.useCallback(() => {
24+
inputRef.current?.click();
25+
}, []);
26+
27+
const handleDownload = React.useCallback(
28+
async (file: Blob) => await readFile(file, spec.viewSpec.fileInput?.readAsMethod),
29+
[spec.viewSpec.fileInput?.readAsMethod],
30+
);
31+
32+
const handleReset = React.useCallback(() => {
33+
setFileName('');
34+
onChange('');
35+
}, [onChange]);
36+
37+
const handleInputChange = React.useCallback(
38+
async (event: React.ChangeEvent<HTMLInputElement>) => {
39+
const file = event.target.files;
40+
41+
if (file && file.length > 0) {
42+
setFileName(file[0].name);
43+
const data = (await handleDownload(file[0])) as string;
44+
onChange(data);
45+
}
46+
},
47+
[handleDownload, onChange],
48+
);
49+
50+
const fileNameContent = React.useMemo(() => {
51+
if (value) {
52+
if (fileName) {
53+
return <React.Fragment>{fileName}</React.Fragment>;
54+
}
55+
56+
return (
57+
<Label size="m" theme="info">
58+
{i18n('label-data_loaded')}
59+
</Label>
60+
);
61+
}
62+
63+
return null;
64+
}, [fileName, value]);
65+
66+
return (
67+
<div className={b()}>
68+
<Button disabled={spec.viewSpec.disabled} onClick={handleClick}>
69+
{i18n('button-upload_file')}
70+
</Button>
71+
<input
72+
type="file"
73+
ref={inputRef}
74+
autoComplete="off"
75+
disabled={spec.viewSpec.disabled}
76+
onChange={handleInputChange}
77+
className={b('input')}
78+
tabIndex={-1}
79+
accept={spec.viewSpec.fileInput?.accept}
80+
/>
81+
<span className={b('file-name')}>{fileNameContent}</span>
82+
{value ? (
83+
<Button view="flat" onClick={handleReset} disabled={spec.viewSpec.disabled}>
84+
<Icon data={Xmark} size={16} />
85+
</Button>
86+
) : null}
87+
</div>
88+
);
89+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './FileInput';
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {ReadAsMethod} from '../../../../core';
2+
3+
export function readFile(
4+
file: Blob,
5+
readAsMethod: ReadAsMethod = 'readAsBinaryString',
6+
): Promise<string | ArrayBuffer | null> {
7+
return new Promise((resolve, reject) => {
8+
const reader = new FileReader();
9+
10+
if (typeof reader[readAsMethod] !== 'function') {
11+
reject(new Error(`Unknown parameter: ${readAsMethod}`));
12+
return;
13+
}
14+
15+
reader.addEventListener('load', () => resolve(reader.result));
16+
reader.addEventListener('error', () => reject(reader.error));
17+
18+
reader[readAsMethod](file);
19+
});
20+
}

src/lib/kit/components/Inputs/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './ArrayBase';
22
export * from './CardOneOf';
33
export * from './Checkbox';
4+
export * from './FileInput';
45
export * from './MultiSelect';
56
export * from './ObjectBase';
67
export * from './OneOf';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react';
2+
3+
import {StringViewProps} from '../../../../core';
4+
import i18n from '../../../../kit/i18n';
5+
import {LongValue} from '../../../components';
6+
7+
export const FileInputView: React.FC<StringViewProps> = ({value, spec}) => (
8+
<LongValue value={spec.viewSpec.fileInput?.ignoreText ? i18n('label-data_loaded') : value} />
9+
);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './FileInputView';

0 commit comments

Comments
 (0)