Commit 82ed0375 authored by Alejandro Celaya's avatar Alejandro Celaya Committed by Alejandro Celaya

Implement custom file input for annotations import

parent ac230b38
import {
FileGenericIcon,
IconButton,
Input,
InputGroup,
} from '@hypothesis/frontend-shared';
import { useId, useRef } from 'preact/hooks';
export type FileInputProps = {
onFileSelected: (file: File) => void;
disabled?: boolean;
};
export default function FileInput({
onFileSelected,
disabled,
}: FileInputProps) {
const fileInputRef = useRef<HTMLInputElement>(null);
const inputId = useId();
return (
<>
<input
ref={fileInputRef}
accept=".json"
type="file"
disabled={disabled}
className="invisible absolute w-0 h-0"
aria-hidden
tabIndex={-1}
data-testid="file-input"
onChange={e => {
const files = (e.target as HTMLInputElement)!.files;
if (files !== null && files.length > 0) {
onFileSelected(files[0]);
}
}}
/>
<label htmlFor={inputId}>Select Hypothesis export file:</label>
<InputGroup>
<Input
id={inputId}
onClick={() => fileInputRef.current?.click()}
readonly
disabled={disabled}
value={fileInputRef.current?.files![0]?.name ?? 'Select a file'}
data-testid="file-input-proxy-input"
classes="cursor-pointer"
/>
<IconButton
title="Select a file"
onClick={() => fileInputRef.current?.click()}
icon={FileGenericIcon}
variant="dark"
disabled={disabled}
data-testid="file-input-proxy-button"
/>
</InputGroup>
</>
);
}
...@@ -9,6 +9,7 @@ import { withServices } from '../../service-context'; ...@@ -9,6 +9,7 @@ import { withServices } from '../../service-context';
import type { ExportContent } from '../../services/annotations-exporter'; import type { ExportContent } from '../../services/annotations-exporter';
import type { ImportAnnotationsService } from '../../services/import-annotations'; import type { ImportAnnotationsService } from '../../services/import-annotations';
import { useSidebarStore } from '../../store'; import { useSidebarStore } from '../../store';
import FileInput from './FileInput';
import LoadingSpinner from './LoadingSpinner'; import LoadingSpinner from './LoadingSpinner';
/** /**
...@@ -136,7 +137,6 @@ function ImportAnnotations({ ...@@ -136,7 +137,6 @@ function ImportAnnotations({
}; };
} }
const fileInputId = useId();
const userSelectId = useId(); const userSelectId = useId();
if (!currentUser) { if (!currentUser) {
...@@ -154,25 +154,13 @@ function ImportAnnotations({ ...@@ -154,25 +154,13 @@ function ImportAnnotations({
return ( return (
<> <>
<label htmlFor={fileInputId}> <FileInput onFileSelected={setFile} disabled={busy} />
<p>Select Hypothesis export file:</p>
</label>
<input
id={fileInputId}
accept=".json"
type="file"
disabled={busy}
onChange={e => {
const files = (e.target as HTMLInputElement)!.files;
if (files !== null && files.length > 0) {
setFile(files[0]);
}
}}
/>
{userList && ( {userList && (
<> <>
<label htmlFor={userSelectId}> <label htmlFor={userSelectId}>
<p>Select which user&apos;s annotations to import:</p> <p className="mt-3">
Select which user&apos;s annotations to import:
</p>
</label> </label>
<Select <Select
id={userSelectId} id={userSelectId}
......
import { mount } from 'enzyme';
import FileInput from '../FileInput';
describe('FileInput', () => {
let container;
let fakeOnFileSelected;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
fakeOnFileSelected = sinon.stub();
});
afterEach(() => {
container.remove();
});
const createFile = name => new File([name], `${name}.json`);
const fillInputWithFiles = (fileInput, files) => {
const list = new DataTransfer();
files.forEach(file => list.items.add(file));
fileInput.getDOMNode().files = list.files;
};
const getActualFileInput = wrapper =>
wrapper.find('[data-testid="file-input"]');
const getProxyInput = wrapper =>
wrapper.find('input[data-testid="file-input-proxy-input"]');
const getProxyButton = wrapper =>
wrapper.find('button[data-testid="file-input-proxy-button"]');
const createInput = (disabled = undefined) => {
const wrapper = mount(
<FileInput onFileSelected={fakeOnFileSelected} disabled={disabled} />,
{ attachTo: container },
);
// Stub "click" method on the native input, so it doesn't show a real file
// dialog
sinon.stub(getActualFileInput(wrapper).getDOMNode(), 'click');
return wrapper;
};
it('calls onFileSelected when selected file changes', () => {
const wrapper = createInput();
const firstFile = createFile('foo');
const fileInput = getActualFileInput(wrapper);
fillInputWithFiles(fileInput, [firstFile, createFile('bar')]);
fileInput.simulate('change');
assert.calledWith(fakeOnFileSelected, firstFile);
});
it('does not call onFileSelected when input changes with no files', () => {
const wrapper = createInput();
const fileInput = getActualFileInput(wrapper);
fileInput.simulate('change');
assert.notCalled(fakeOnFileSelected);
});
it('forwards click on proxy input to actual file input', () => {
const wrapper = createInput();
const proxyInput = getProxyInput(wrapper);
proxyInput.simulate('click');
assert.called(getActualFileInput(wrapper).getDOMNode().click);
});
it('forwards click on proxy button to actual file input', () => {
const wrapper = createInput();
const proxyButton = getProxyButton(wrapper);
proxyButton.simulate('click');
assert.called(getActualFileInput(wrapper).getDOMNode().click);
});
[true, false, undefined].forEach(disabled => {
it('disables all inner components when FileInput is disabled', () => {
const wrapper = createInput(disabled);
const fileInput = getActualFileInput(wrapper);
const proxyInput = getProxyInput(wrapper);
const proxyButton = getProxyButton(wrapper);
assert.equal(fileInput.prop('disabled'), disabled);
assert.equal(proxyInput.prop('disabled'), disabled);
assert.equal(proxyButton.prop('disabled'), disabled);
});
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment