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
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": [ "packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx:3311646309": [
[0, 31, 13, "RegExp match", "2409514259"] [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": [ "packages/grafana-ui/src/components/FormField/FormField.test.tsx:3429087660": [
[0, 19, 13, "RegExp match", "2409514259"] [0, 19, 13, "RegExp match", "2409514259"]
], ],

View File

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

View File

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