mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Making FileUpload component accessible via keyboard (#47497)
This commit is contained in:
@@ -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"]
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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),
|
||||||
`,
|
}),
|
||||||
};
|
};
|
||||||
});
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user