UI: FileDropzone component to handle file list overwrite (#37685)

* UI: FileDropzone component to handle file list overwrite

* FileListItem: use type=button everywhere

* Update packages/grafana-ui/src/components/FileDropzone/FileDropzone.story.tsx

Co-authored-by: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com>

* FileListItem: add aria-hidden and change cancel text

* Update packages/grafana-ui/src/components/FileDropzone/FileListItem.test.tsx

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

Co-authored-by: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com>
Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
This commit is contained in:
Zoltán Bedi
2021-08-10 10:23:49 +02:00
committed by GitHub
co-authored by Tobias Skarhed Alex Khomenko
parent 3e124c854e
commit ad6a4edfeb
5 changed files with 41 additions and 7 deletions
@@ -21,3 +21,7 @@ export default {
export const Basic: Story<FileDropzoneProps> = (args) => {
return <FileDropzone {...args} />;
};
export const WithCustomFileList: Story<FileDropzoneProps> = () => {
return <FileDropzone fileListRenderer={(file) => <div>Custom rendered item {file.file.name}</div>} />;
};
@@ -107,6 +107,17 @@ describe('The FileDropzone component', () => {
screen.getByText('Custom dropzone text');
});
it('should handle file list overwrite when fileListRenderer is passed', async () => {
render(<FileDropzone fileListRenderer={() => null} />);
dispatchEvt(screen.getByTestId('dropzone'), 'drop', mockData([file({})]));
// need to await this in order to have the drop finished
await screen.findByTestId('dropzone');
expect(screen.queryByText('ping.json')).not.toBeInTheDocument();
});
});
function dispatchEvt(node: HTMLElement, type: string, data: any) {
@@ -30,6 +30,11 @@ export interface FileDropzoneProps {
* Use the onLoad function to get the result from FileReader.
*/
onLoad?: (result: string | ArrayBuffer | null) => void;
/**
* The fileListRenderer property can be used to overwrite the list of files. To not to show
* any list return null in the function.
*/
fileListRenderer?: (file: DropzoneFile, removeFile: (file: DropzoneFile) => void) => ReactNode;
}
export interface DropzoneFile {
@@ -41,7 +46,7 @@ export interface DropzoneFile {
retryUpload?: () => void;
}
export function FileDropzone({ options, children, readAs, onLoad }: FileDropzoneProps) {
export function FileDropzone({ options, children, readAs, onLoad, fileListRenderer }: FileDropzoneProps) {
const [files, setFiles] = useState<DropzoneFile[]>([]);
const setFileProperty = useCallback(
@@ -133,7 +138,12 @@ export function FileDropzone({ options, children, readAs, onLoad }: FileDropzone
const { getRootProps, getInputProps, isDragActive } = useDropzone({ ...options, onDrop });
const theme = useTheme2();
const styles = getStyles(theme, isDragActive);
const fileList = files.map((file) => <FileListItem key={file.id} file={file} removeFile={removeFile} />);
const fileList = files.map((file) => {
if (fileListRenderer) {
return fileListRenderer(file, removeFile);
}
return <FileListItem key={file.id} file={file} removeFile={removeFile} />;
});
return (
<div className={styles.container}>
@@ -30,7 +30,7 @@ describe('The FileListItem component', () => {
it('should show a progressbar when the progress prop has a value', () => {
render(<FileListItem file={{ file: file({}), id: '1', error: null, progress: 6 }} />);
expect(screen.queryByText('Cancel')).not.toBeInTheDocument();
expect(screen.queryByText('Cancel upload')).not.toBeInTheDocument();
expect(screen.getByText('46%')).toBeInTheDocument();
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});
@@ -45,7 +45,7 @@ describe('The FileListItem component', () => {
const abortUpload = jest.fn();
render(<FileListItem file={{ file: file({}), id: '1', error: null, progress: 6, abortUpload }} />);
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
expect(abortUpload).toBeCalledTimes(1);
});
@@ -24,11 +24,19 @@ export function FileListItem({ file: customFile, removeFile }: FileListItemProps
<>
<span className={styles.error}>{error.message}</span>
{retryUpload && (
<IconButton aria-label="Retry" name="sync" tooltip="Retry" tooltipPlacement="top" onClick={retryUpload} />
<IconButton
type="button"
aria-label="Retry"
name="sync"
tooltip="Retry"
tooltipPlacement="top"
onClick={retryUpload}
/>
)}
{removeFile && (
<IconButton
className={retryUpload ? styles.marginLeft : ''}
type="button"
name="trash-alt"
onClick={() => removeFile(customFile)}
tooltip={REMOVE_FILE}
@@ -46,7 +54,7 @@ export function FileListItem({ file: customFile, removeFile }: FileListItemProps
<span className={styles.paddingLeft}>{Math.round((progress / file.size) * 100)}%</span>
{abortUpload && (
<Button variant="secondary" type="button" fill="text" onClick={abortUpload}>
Cancel
Cancel upload
</Button>
)}
</>
@@ -59,6 +67,7 @@ export function FileListItem({ file: customFile, removeFile }: FileListItemProps
onClick={() => removeFile(customFile)}
tooltip={REMOVE_FILE}
aria-label={REMOVE_FILE}
type="button"
tooltipPlacement="top"
/>
)
@@ -70,7 +79,7 @@ export function FileListItem({ file: customFile, removeFile }: FileListItemProps
return (
<div className={styles.fileListContainer}>
<span className={styles.fileNameWrapper}>
<Icon name="file-blank" size="lg" />
<Icon name="file-blank" size="lg" aria-hidden={true} />
<span className={styles.padding}>{trimFileName(file.name)}</span>
<span>{formattedValueToString(valueFormat)}</span>
</span>