Making FileUpload component accessible via keyboard (#47497)

This commit is contained in:
Timur Olzhabayev 2022-04-29 09:47:33 +02:00 committed by GitHub
parent 3e752a0db1
commit 17eca4505c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 75 additions and 47 deletions

View File

@ -11,9 +11,6 @@ exports[`no enzyme tests`] = {
"packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx:3311646309": [
[0, 31, 13, "RegExp match", "2409514259"]
],
"packages/grafana-ui/src/components/FileUpload/FileUpload.test.tsx:4145620610": [
[0, 19, 13, "RegExp match", "2409514259"]
],
"packages/grafana-ui/src/components/FormField/FormField.test.tsx:3429087660": [
[0, 19, 13, "RegExp match", "2409514259"]
],

View File

@ -333,4 +333,8 @@ export const Components = {
orgsTable: 'data-testid-user-orgs-table',
sessionsTable: 'data-testid-user-sessions-table',
},
FileUpload: {
inputField: 'data-testid-file-upload-input-field',
fileNameSpan: 'data-testid-file-upload-file-name',
},
};

View File

@ -1,33 +1,44 @@
import { shallow } from 'enzyme';
import { render, waitFor, fireEvent, screen } from '@testing-library/react';
import React from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { FileUpload } from './FileUpload';
describe('FileUpload', () => {
it('should render upload button with default text and no file name', () => {
const wrapper = shallow(<FileUpload onFileUpload={() => {}} />);
expect(wrapper.findWhere((comp) => comp.text() === 'Upload file').exists()).toBeTruthy();
expect(wrapper.find({ 'aria-label': 'File name' }).exists()).toBeFalsy();
render(<FileUpload onFileUpload={() => {}} />);
expect(screen.getByText('Upload file')).toBeInTheDocument();
expect(screen.queryByLabelText('File name')).toBeNull();
});
it("should trim uploaded file's name", () => {
const wrapper = shallow(<FileUpload onFileUpload={() => {}} />);
it('should display uploaded file name', async () => {
const testFileName = 'grafana.png';
const file = new File(['(⌐□_□)'], testFileName, { type: 'image/png' });
const onFileUpload = jest.fn();
const { getByTestId } = render(<FileUpload onFileUpload={onFileUpload} />);
let uploader = getByTestId(selectors.components.FileUpload.inputField);
await waitFor(() =>
fireEvent.change(uploader, {
target: { files: [file] },
})
);
let uploaderLabel = getByTestId(selectors.components.FileUpload.fileNameSpan);
expect(uploaderLabel).toHaveTextContent(testFileName);
});
wrapper.find('input').simulate('change', {
currentTarget: {
files: [{ name: 'longFileName.something.png' }],
},
});
expect(wrapper.find({ 'aria-label': 'File name' }).exists()).toBeTruthy();
// Trim file name longer than 16 chars
expect(wrapper.find({ 'aria-label': 'File name' }).text()).toEqual('longFileName.som....png');
// Keep the name below the length limit intact
wrapper.find('input').simulate('change', {
currentTarget: {
files: [{ name: 'longFileName.png' }],
},
});
expect(wrapper.find({ 'aria-label': 'File name' }).text()).toEqual('longFileName.png');
it("should trim uploaded file's name", async () => {
const testFileName = 'longFileName.something.png';
const file = new File(['(⌐□_□)'], testFileName, { type: 'image/png' });
const onFileUpload = jest.fn();
const { getByTestId } = render(<FileUpload onFileUpload={onFileUpload} />);
let uploader = getByTestId(selectors.components.FileUpload.inputField);
await waitFor(() =>
fireEvent.change(uploader, {
target: { files: [file] },
})
);
let uploaderLabel = getByTestId(selectors.components.FileUpload.fileNameSpan);
expect(uploaderLabel).toHaveTextContent('longFileName.som....png');
});
});

View File

@ -2,8 +2,10 @@ import { css, cx } from '@emotion/css';
import React, { FC, FormEvent, useCallback, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { stylesFactory, useTheme2 } from '../../themes';
import { useStyles2 } from '../../themes';
import { getFocusStyles } from '../../themes/mixins';
import { ComponentSize } from '../../types/size';
import { trimFileName } from '../../utils/file';
import { getButtonStyles } from '../Button';
@ -27,8 +29,7 @@ export const FileUpload: FC<Props> = ({
accept = '*',
size = 'md',
}) => {
const theme = useTheme2();
const style = getStyles(theme, size);
const style = useStyles2(getStyles(size));
const [fileName, setFileName] = useState('');
const onChange = useCallback(
@ -44,20 +45,26 @@ export const FileUpload: FC<Props> = ({
return (
<>
<label className={cx(style.button, className)}>
<input
type="file"
id="fileUpload"
className={style.fileUpload}
onChange={onChange}
multiple={false}
accept={accept}
data-testid={selectors.components.FileUpload.inputField}
/>
<label className={cx(style.labelWrapper, className)}>
<Icon name="upload" className={style.icon} />
{children}
<input
type="file"
id="fileUpload"
className={style.fileUpload}
onChange={onChange}
multiple={false}
accept={accept}
/>
</label>
{fileName && (
<span aria-label="File name" className={style.fileName}>
<span
aria-label="File name"
className={style.fileName}
data-testid={selectors.components.FileUpload.fileNameSpan}
>
{trimFileName(fileName)}
</span>
)}
@ -65,16 +72,25 @@ export const FileUpload: FC<Props> = ({
);
};
const getStyles = stylesFactory((theme: GrafanaTheme2, size: ComponentSize) => {
const getStyles = (size: ComponentSize) => (theme: GrafanaTheme2) => {
const buttonStyles = getButtonStyles({ theme, variant: 'primary', size, iconOnly: false });
const focusStyle = getFocusStyles(theme);
return {
fileUpload: css`
display: none;
`,
button: buttonStyles.button,
fileUpload: css({
height: '0.1px',
opacity: '0',
overflow: 'hidden',
position: 'absolute',
width: '0.1px',
zIndex: -1,
'&:focus + label': focusStyle,
'&:focus-visible + label': focusStyle,
}),
labelWrapper: buttonStyles.button,
icon: buttonStyles.icon,
fileName: css`
margin-left: ${theme.spacing(0.5)};
`,
fileName: css({
marginLeft: theme.spacing(0.5),
}),
};
});
};