@transferwise/components 46.97.5 → 46.98.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/alert/Alert.js +8 -0
- package/build/alert/Alert.js.map +1 -1
- package/build/alert/Alert.mjs +8 -0
- package/build/alert/Alert.mjs.map +1 -1
- package/build/common/closeButton/CloseButton.js +3 -1
- package/build/common/closeButton/CloseButton.js.map +1 -1
- package/build/common/closeButton/CloseButton.mjs +3 -1
- package/build/common/closeButton/CloseButton.mjs.map +1 -1
- package/build/i18n/cs.json +3 -0
- package/build/i18n/cs.json.js +3 -0
- package/build/i18n/cs.json.js.map +1 -1
- package/build/i18n/cs.json.mjs +3 -0
- package/build/i18n/cs.json.mjs.map +1 -1
- package/build/i18n/de.json +3 -0
- package/build/i18n/de.json.js +3 -0
- package/build/i18n/de.json.js.map +1 -1
- package/build/i18n/de.json.mjs +3 -0
- package/build/i18n/de.json.mjs.map +1 -1
- package/build/i18n/en.json +3 -0
- package/build/i18n/en.json.js +3 -0
- package/build/i18n/en.json.js.map +1 -1
- package/build/i18n/en.json.mjs +3 -0
- package/build/i18n/en.json.mjs.map +1 -1
- package/build/i18n/es.json +3 -0
- package/build/i18n/es.json.js +3 -0
- package/build/i18n/es.json.js.map +1 -1
- package/build/i18n/es.json.mjs +3 -0
- package/build/i18n/es.json.mjs.map +1 -1
- package/build/i18n/fr.json +3 -0
- package/build/i18n/fr.json.js +3 -0
- package/build/i18n/fr.json.js.map +1 -1
- package/build/i18n/fr.json.mjs +3 -0
- package/build/i18n/fr.json.mjs.map +1 -1
- package/build/i18n/hu.json +3 -0
- package/build/i18n/hu.json.js +3 -0
- package/build/i18n/hu.json.js.map +1 -1
- package/build/i18n/hu.json.mjs +3 -0
- package/build/i18n/hu.json.mjs.map +1 -1
- package/build/i18n/id.json +3 -0
- package/build/i18n/id.json.js +3 -0
- package/build/i18n/id.json.js.map +1 -1
- package/build/i18n/id.json.mjs +3 -0
- package/build/i18n/id.json.mjs.map +1 -1
- package/build/i18n/it.json +3 -0
- package/build/i18n/it.json.js +3 -0
- package/build/i18n/it.json.js.map +1 -1
- package/build/i18n/it.json.mjs +3 -0
- package/build/i18n/it.json.mjs.map +1 -1
- package/build/i18n/ja.json +3 -0
- package/build/i18n/ja.json.js +3 -0
- package/build/i18n/ja.json.js.map +1 -1
- package/build/i18n/ja.json.mjs +3 -0
- package/build/i18n/ja.json.mjs.map +1 -1
- package/build/i18n/nl.json +6 -3
- package/build/i18n/pl.json +3 -0
- package/build/i18n/pl.json.js +3 -0
- package/build/i18n/pl.json.js.map +1 -1
- package/build/i18n/pl.json.mjs +3 -0
- package/build/i18n/pl.json.mjs.map +1 -1
- package/build/i18n/pt.json +3 -0
- package/build/i18n/pt.json.js +3 -0
- package/build/i18n/pt.json.js.map +1 -1
- package/build/i18n/pt.json.mjs +3 -0
- package/build/i18n/pt.json.mjs.map +1 -1
- package/build/i18n/ro.json +3 -0
- package/build/i18n/ro.json.js +3 -0
- package/build/i18n/ro.json.js.map +1 -1
- package/build/i18n/ro.json.mjs +3 -0
- package/build/i18n/ro.json.mjs.map +1 -1
- package/build/i18n/ru.json +3 -0
- package/build/i18n/ru.json.js +3 -0
- package/build/i18n/ru.json.js.map +1 -1
- package/build/i18n/ru.json.mjs +3 -0
- package/build/i18n/ru.json.mjs.map +1 -1
- package/build/i18n/th.json +3 -0
- package/build/i18n/th.json.js +3 -0
- package/build/i18n/th.json.js.map +1 -1
- package/build/i18n/th.json.mjs +3 -0
- package/build/i18n/th.json.mjs.map +1 -1
- package/build/i18n/tr.json +3 -0
- package/build/i18n/tr.json.js +3 -0
- package/build/i18n/tr.json.js.map +1 -1
- package/build/i18n/tr.json.mjs +3 -0
- package/build/i18n/tr.json.mjs.map +1 -1
- package/build/i18n/zh-CN.json +3 -0
- package/build/i18n/zh-CN.json.js +3 -0
- package/build/i18n/zh-CN.json.js.map +1 -1
- package/build/i18n/zh-CN.json.mjs +3 -0
- package/build/i18n/zh-CN.json.mjs.map +1 -1
- package/build/i18n/zh-HK.json +3 -0
- package/build/i18n/zh-HK.json.js +3 -0
- package/build/i18n/zh-HK.json.js.map +1 -1
- package/build/i18n/zh-HK.json.mjs +3 -0
- package/build/i18n/zh-HK.json.mjs.map +1 -1
- package/build/image/Image.js +9 -10
- package/build/image/Image.js.map +1 -1
- package/build/image/Image.mjs +11 -11
- package/build/image/Image.mjs.map +1 -1
- package/build/main.css +5 -2
- package/build/moneyInput/MoneyInput.js +2 -6
- package/build/moneyInput/MoneyInput.js.map +1 -1
- package/build/moneyInput/MoneyInput.messages.js +3 -0
- package/build/moneyInput/MoneyInput.messages.js.map +1 -1
- package/build/moneyInput/MoneyInput.messages.mjs +3 -0
- package/build/moneyInput/MoneyInput.messages.mjs.map +1 -1
- package/build/moneyInput/MoneyInput.mjs +2 -6
- package/build/moneyInput/MoneyInput.mjs.map +1 -1
- package/build/phoneNumberInput/PhoneNumberInput.js +36 -2
- package/build/phoneNumberInput/PhoneNumberInput.js.map +1 -1
- package/build/phoneNumberInput/PhoneNumberInput.messages.js +6 -0
- package/build/phoneNumberInput/PhoneNumberInput.messages.js.map +1 -1
- package/build/phoneNumberInput/PhoneNumberInput.messages.mjs +6 -0
- package/build/phoneNumberInput/PhoneNumberInput.messages.mjs.map +1 -1
- package/build/phoneNumberInput/PhoneNumberInput.mjs +36 -2
- package/build/phoneNumberInput/PhoneNumberInput.mjs.map +1 -1
- package/build/styles/circularButton/CircularButton.css +1 -0
- package/build/styles/dateInput/DateInput.css +2 -1
- package/build/styles/main.css +5 -2
- package/build/styles/uploadInput/uploadItem/UploadItem.css +2 -1
- package/build/types/alert/Alert.d.ts.map +1 -1
- package/build/types/common/closeButton/CloseButton.d.ts +2 -0
- package/build/types/common/closeButton/CloseButton.d.ts.map +1 -1
- package/build/types/image/Image.d.ts +0 -1
- package/build/types/image/Image.d.ts.map +1 -1
- package/build/types/moneyInput/MoneyInput.d.ts.map +1 -1
- package/build/types/moneyInput/MoneyInput.messages.d.ts +5 -0
- package/build/types/moneyInput/MoneyInput.messages.d.ts.map +1 -1
- package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
- package/build/types/phoneNumberInput/PhoneNumberInput.messages.d.ts +8 -0
- package/build/types/phoneNumberInput/PhoneNumberInput.messages.d.ts.map +1 -1
- package/build/types/test-utils/index.d.ts +6 -0
- package/build/types/test-utils/index.d.ts.map +1 -1
- package/build/types/upload/Upload.d.ts +1 -2
- package/build/types/upload/Upload.d.ts.map +1 -1
- package/build/types/upload/steps/processingStep/processingStep.d.ts +1 -3
- package/build/types/upload/steps/processingStep/processingStep.d.ts.map +1 -1
- package/build/types/uploadInput/UploadInput.d.ts.map +1 -1
- package/build/types/uploadInput/uploadItem/UploadItem.d.ts +1 -1
- package/build/types/uploadInput/uploadItem/UploadItem.d.ts.map +1 -1
- package/build/types/withDisplayFormat/WithDisplayFormat.d.ts.map +1 -1
- package/build/upload/Upload.js +27 -43
- package/build/upload/Upload.js.map +1 -1
- package/build/upload/Upload.mjs +27 -43
- package/build/upload/Upload.mjs.map +1 -1
- package/build/upload/steps/processingStep/processingStep.js +1 -3
- package/build/upload/steps/processingStep/processingStep.js.map +1 -1
- package/build/upload/steps/processingStep/processingStep.mjs +1 -3
- package/build/upload/steps/processingStep/processingStep.mjs.map +1 -1
- package/build/uploadInput/UploadInput.js +55 -6
- package/build/uploadInput/UploadInput.js.map +1 -1
- package/build/uploadInput/UploadInput.mjs +55 -6
- package/build/uploadInput/UploadInput.mjs.map +1 -1
- package/build/uploadInput/uploadItem/UploadItem.js +12 -6
- package/build/uploadInput/uploadItem/UploadItem.js.map +1 -1
- package/build/uploadInput/uploadItem/UploadItem.mjs +12 -6
- package/build/uploadInput/uploadItem/UploadItem.mjs.map +1 -1
- package/build/withDisplayFormat/WithDisplayFormat.js +3 -2
- package/build/withDisplayFormat/WithDisplayFormat.js.map +1 -1
- package/build/withDisplayFormat/WithDisplayFormat.mjs +3 -2
- package/build/withDisplayFormat/WithDisplayFormat.mjs.map +1 -1
- package/package.json +6 -9
- package/src/alert/Alert.spec.tsx +11 -0
- package/src/alert/Alert.story.tsx +23 -9
- package/src/alert/Alert.tsx +14 -1
- package/src/circularButton/CircularButton.css +1 -0
- package/src/circularButton/CircularButton.less +1 -0
- package/src/circularButton/CircularButton.tests.story.tsx +23 -0
- package/src/common/closeButton/CloseButton.spec.tsx +13 -1
- package/src/common/closeButton/CloseButton.tsx +3 -0
- package/src/dateInput/DateInput.css +2 -1
- package/src/dateInput/DateInput.less +7 -4
- package/src/i18n/cs.json +3 -0
- package/src/i18n/de.json +3 -0
- package/src/i18n/en.json +3 -0
- package/src/i18n/es.json +3 -0
- package/src/i18n/fr.json +3 -0
- package/src/i18n/hu.json +3 -0
- package/src/i18n/id.json +3 -0
- package/src/i18n/it.json +3 -0
- package/src/i18n/ja.json +3 -0
- package/src/i18n/nl.json +6 -3
- package/src/i18n/pl.json +3 -0
- package/src/i18n/pt.json +3 -0
- package/src/i18n/ro.json +3 -0
- package/src/i18n/ru.json +3 -0
- package/src/i18n/th.json +3 -0
- package/src/i18n/tr.json +3 -0
- package/src/i18n/zh-CN.json +3 -0
- package/src/i18n/zh-HK.json +3 -0
- package/src/image/Image.spec.tsx +3 -3
- package/src/image/Image.tsx +10 -12
- package/src/main.css +5 -2
- package/src/moneyInput/MoneyInput.messages.ts +5 -0
- package/src/moneyInput/MoneyInput.spec.tsx +42 -5
- package/src/moneyInput/MoneyInput.story.tsx +11 -2
- package/src/moneyInput/MoneyInput.tsx +5 -7
- package/src/phoneNumberInput/PhoneNumberInput.messages.ts +8 -0
- package/src/phoneNumberInput/PhoneNumberInput.spec.tsx +77 -43
- package/src/phoneNumberInput/PhoneNumberInput.tsx +34 -2
- package/src/promoCard/__snapshots__/PromoCard.spec.tsx.snap +1 -0
- package/src/promoCard/__snapshots__/PromoCardGroup.spec.tsx.snap +2 -0
- package/src/test-utils/jest.setup.ts +0 -4
- package/src/typeahead/Typeahead.spec.tsx +182 -0
- package/src/typeahead/typeaheadInput/TypeaheadInput.spec.tsx +103 -0
- package/src/typeahead/util/highlight.spec.tsx +43 -0
- package/src/upload/Upload.spec.tsx +63 -0
- package/src/upload/Upload.story.tsx +0 -51
- package/src/upload/Upload.tests.story.tsx +93 -0
- package/src/upload/Upload.tsx +28 -49
- package/src/upload/steps/processingStep/processingStep.tsx +2 -7
- package/src/uploadInput/UploadInput.tsx +81 -8
- package/src/uploadInput/uploadItem/UploadItem.css +2 -1
- package/src/uploadInput/uploadItem/UploadItem.less +1 -1
- package/src/uploadInput/uploadItem/UploadItem.tsx +11 -6
- package/src/withDisplayFormat/WithDisplayFormat.spec.js +11 -15
- package/src/withDisplayFormat/WithDisplayFormat.tsx +3 -2
- package/src/typeahead/Typeahead.rtl.spec.tsx +0 -54
- package/src/typeahead/Typeahead.spec.js +0 -404
- package/src/typeahead/typeaheadInput/TypeaheadInput.spec.js +0 -74
- package/src/typeahead/typeaheadOption/TypeaheadOption.spec.js +0 -75
- package/src/typeahead/util/highlight.spec.js +0 -34
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { render, fireEvent, screen } from '../../test-utils';
|
|
2
|
+
import TypeaheadInput, { TypeaheadInputProps } from './TypeaheadInput';
|
|
3
|
+
import { TypeaheadOption } from '../Typeahead';
|
|
4
|
+
|
|
5
|
+
const defaultProps: TypeaheadInputProps<number> = {
|
|
6
|
+
id: 'test-id',
|
|
7
|
+
name: 'test-name',
|
|
8
|
+
typeaheadId: 'test-id',
|
|
9
|
+
value: '',
|
|
10
|
+
selected: [],
|
|
11
|
+
onChange: jest.fn(),
|
|
12
|
+
onKeyDown: jest.fn(),
|
|
13
|
+
onFocus: jest.fn(),
|
|
14
|
+
onPaste: jest.fn(),
|
|
15
|
+
autoComplete: 'off',
|
|
16
|
+
placeholder: 'Search...',
|
|
17
|
+
renderChip: jest.fn(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe('TypeaheadInput', () => {
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
jest.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('renders input with placeholder', () => {
|
|
26
|
+
render(<TypeaheadInput {...defaultProps} />);
|
|
27
|
+
expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renders with given value', () => {
|
|
31
|
+
render(<TypeaheadInput {...defaultProps} value="hello" />);
|
|
32
|
+
expect(screen.getByDisplayValue('hello')).toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('calls onChange when input value changes', () => {
|
|
36
|
+
const onChange = jest.fn();
|
|
37
|
+
render(<TypeaheadInput {...defaultProps} onChange={onChange} />);
|
|
38
|
+
const input = screen.getByPlaceholderText('Search...');
|
|
39
|
+
fireEvent.change(input, { target: { value: 'test' } });
|
|
40
|
+
expect(onChange).toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('calls onFocus when input is focused', () => {
|
|
44
|
+
const onFocus = jest.fn();
|
|
45
|
+
render(<TypeaheadInput {...defaultProps} onFocus={onFocus} />);
|
|
46
|
+
const input = screen.getByPlaceholderText('Search...');
|
|
47
|
+
fireEvent.focus(input);
|
|
48
|
+
expect(onFocus).toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('calls onPaste when input is pasted into', () => {
|
|
52
|
+
const onPaste = jest.fn();
|
|
53
|
+
render(<TypeaheadInput {...defaultProps} onPaste={onPaste} />);
|
|
54
|
+
const input = screen.getByPlaceholderText('Search...');
|
|
55
|
+
fireEvent.paste(input, { clipboardData: { getData: () => 'pasted' } });
|
|
56
|
+
expect(onPaste).toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('calls onKeyDown when key is pressed', () => {
|
|
60
|
+
const onKeyDown = jest.fn();
|
|
61
|
+
render(<TypeaheadInput {...defaultProps} onKeyDown={onKeyDown} />);
|
|
62
|
+
const input = screen.getByPlaceholderText('Search...');
|
|
63
|
+
fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' });
|
|
64
|
+
expect(onKeyDown).toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('renders chips when multiple is true and selected has items', () => {
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
|
69
|
+
const renderChip = jest.fn((chip, idx) => <span key={idx}>{chip.label}</span>);
|
|
70
|
+
const selected = [
|
|
71
|
+
{ label: 'Chip 1', value: 1 },
|
|
72
|
+
{ label: 'Chip 2', value: 2 },
|
|
73
|
+
];
|
|
74
|
+
render(
|
|
75
|
+
<TypeaheadInput {...defaultProps} multiple selected={selected} renderChip={renderChip} />,
|
|
76
|
+
);
|
|
77
|
+
expect(screen.getByText('Chip 1')).toBeInTheDocument();
|
|
78
|
+
expect(screen.getByText('Chip 2')).toBeInTheDocument();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('does not show placeholder if multiple is true and selected has items', () => {
|
|
82
|
+
const selected: TypeaheadOption<number>[] = [{ label: 'Chip', value: 1 }];
|
|
83
|
+
render(<TypeaheadInput<number> {...defaultProps} multiple selected={selected} />);
|
|
84
|
+
expect(screen.queryByPlaceholderText('Search...')).not.toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('applies input width style when multiple and selected has items', () => {
|
|
88
|
+
const selected = [{ label: 'Chip', value: 1 }];
|
|
89
|
+
const { container } = render(
|
|
90
|
+
<TypeaheadInput {...defaultProps} multiple selected={selected} value="test" />,
|
|
91
|
+
);
|
|
92
|
+
const input = container.querySelector('input');
|
|
93
|
+
expect(input?.style.width).not.toBe('');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('sets aria attributes correctly', () => {
|
|
97
|
+
render(<TypeaheadInput {...defaultProps} dropdownOpen ariaActivedescendant="option-1" />);
|
|
98
|
+
const input = screen.getByRole('combobox');
|
|
99
|
+
expect(input).toHaveAttribute('aria-expanded', 'true');
|
|
100
|
+
expect(input).toHaveAttribute('aria-haspopup', 'listbox');
|
|
101
|
+
expect(input).toHaveAttribute('aria-activedescendant', expect.stringContaining('option-1'));
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { render, screen } from '../../test-utils';
|
|
2
|
+
import Highlight from './highlight';
|
|
3
|
+
|
|
4
|
+
describe('Highlight', () => {
|
|
5
|
+
it('renders value with highlighted query (case-insensitive)', () => {
|
|
6
|
+
render(<Highlight value="Hello World" query="world" />);
|
|
7
|
+
expect(screen.getByText('World')).toBeInTheDocument();
|
|
8
|
+
expect(screen.getByText('World').tagName).toBe('STRONG');
|
|
9
|
+
expect(screen.getByText('Hello', { exact: false })).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('renders value with highlighted query (case-sensitive in output)', () => {
|
|
13
|
+
render(<Highlight value="Hello World" query="HELLO" />);
|
|
14
|
+
expect(screen.getByText('Hello').tagName).toBe('STRONG');
|
|
15
|
+
expect(screen.getByText('World', { exact: false })).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('renders value without highlight if query not found', () => {
|
|
19
|
+
render(<Highlight value="Hello World" query="foo" />);
|
|
20
|
+
expect(screen.getByText('Hello World')).toBeInTheDocument();
|
|
21
|
+
expect(screen.queryByRole('strong')).not.toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('renders value as is if query is empty', () => {
|
|
25
|
+
render(<Highlight value="Hello World" query="" />);
|
|
26
|
+
expect(screen.getByText('Hello World')).toBeInTheDocument();
|
|
27
|
+
expect(screen.queryByRole('strong')).not.toBeInTheDocument();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renders value as is if value is empty', () => {
|
|
31
|
+
render(<Highlight value="" query="test" />);
|
|
32
|
+
expect(screen.queryByText(/./)).not.toBeInTheDocument();
|
|
33
|
+
expect(screen.queryByRole('strong')).not.toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('wraps content in span if className is provided', () => {
|
|
37
|
+
const { container } = render(
|
|
38
|
+
<Highlight value="Hello World" query="world" className="highlighted" />,
|
|
39
|
+
);
|
|
40
|
+
expect(container.querySelector('span.highlighted')).toBeInTheDocument();
|
|
41
|
+
expect(container.querySelector('strong')).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -6,6 +6,14 @@ import { postData } from './utils/postData';
|
|
|
6
6
|
|
|
7
7
|
jest.mock('./utils/asyncFileRead');
|
|
8
8
|
jest.mock('./utils/postData');
|
|
9
|
+
jest.mock('commonmark', () => ({
|
|
10
|
+
Parser: jest.fn().mockImplementation(() => ({
|
|
11
|
+
parse: jest.fn(() => ({})),
|
|
12
|
+
})),
|
|
13
|
+
HtmlRenderer: jest.fn().mockImplementation(() => ({
|
|
14
|
+
render: jest.fn(() => ''),
|
|
15
|
+
})),
|
|
16
|
+
}));
|
|
9
17
|
|
|
10
18
|
const TEST_FILE = new File(['test content'], 'test.png', { type: 'image/png' });
|
|
11
19
|
const INVALID_FILE = new File(['invalid content'], 'invalid.txt', { type: 'text/plain' });
|
|
@@ -76,6 +84,11 @@ describe('Upload Component', () => {
|
|
|
76
84
|
dispatchEvent: jest.fn(),
|
|
77
85
|
})),
|
|
78
86
|
});
|
|
87
|
+
props.onStart.mockReset();
|
|
88
|
+
props.onSuccess.mockReset();
|
|
89
|
+
props.onFailure.mockReset();
|
|
90
|
+
props.onCancel.mockReset();
|
|
91
|
+
props.onChange.mockReset();
|
|
79
92
|
});
|
|
80
93
|
|
|
81
94
|
afterEach(async () => {
|
|
@@ -292,4 +305,54 @@ describe('Upload Component', () => {
|
|
|
292
305
|
const errorIcon = await screen.findByLabelText(/Custom error label/i);
|
|
293
306
|
expect(errorIcon).toBeInTheDocument();
|
|
294
307
|
});
|
|
308
|
+
|
|
309
|
+
function runAnimationDelayTest(label: string, delay: number) {
|
|
310
|
+
test(`should respect a ${label} animationDelay`, async () => {
|
|
311
|
+
(asyncFileRead as jest.Mock).mockResolvedValue('mockBase64Image');
|
|
312
|
+
(postData as jest.Mock).mockResolvedValue('mockSuccessResponse');
|
|
313
|
+
|
|
314
|
+
const { container, unmount } = render(<Upload {...props} animationDelay={delay} />);
|
|
315
|
+
|
|
316
|
+
await act(async () => {
|
|
317
|
+
const droppableElement = await waitFor(() => container.querySelector('.droppable-area'));
|
|
318
|
+
const dataTransfer = new DataTransfer();
|
|
319
|
+
dataTransfer.items.add(TEST_FILE);
|
|
320
|
+
fireEvent.drop(droppableElement!, { dataTransfer });
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Advance half the delay, should still be processing
|
|
324
|
+
await act(async () => {
|
|
325
|
+
jest.advanceTimersByTime(delay / 2);
|
|
326
|
+
});
|
|
327
|
+
expect(container.querySelector('.droppable-processing')).toBeInTheDocument();
|
|
328
|
+
expect(container.querySelector('.droppable-complete')).not.toBeInTheDocument();
|
|
329
|
+
|
|
330
|
+
// Advance the rest of the delay
|
|
331
|
+
await act(async () => {
|
|
332
|
+
jest.advanceTimersByTime(delay / 2 + 10);
|
|
333
|
+
});
|
|
334
|
+
expect(container.querySelector('.droppable-complete')).toBeInTheDocument();
|
|
335
|
+
expect(container.querySelector('.droppable-processing')).not.toBeInTheDocument();
|
|
336
|
+
|
|
337
|
+
unmount();
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
runAnimationDelayTest('short', 50);
|
|
342
|
+
runAnimationDelayTest('long', 500);
|
|
343
|
+
|
|
344
|
+
test('should handle disabled state correctly', async () => {
|
|
345
|
+
const { container } = render(<Upload {...props} usDisabled />);
|
|
346
|
+
const droppableElement = await waitFor(() => container.querySelector('.droppable-area'));
|
|
347
|
+
|
|
348
|
+
const dataTransfer = new DataTransfer();
|
|
349
|
+
dataTransfer.items.add(TEST_FILE);
|
|
350
|
+
|
|
351
|
+
await act(async () => {
|
|
352
|
+
fireEvent.drop(droppableElement!, { dataTransfer });
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
expect(container.querySelector('.droppable-processing')).not.toBeInTheDocument();
|
|
356
|
+
expect(props.onStart).not.toHaveBeenCalled();
|
|
357
|
+
});
|
|
295
358
|
});
|
|
@@ -38,54 +38,3 @@ export const Basic: Story = {
|
|
|
38
38
|
onCancel: fn(),
|
|
39
39
|
},
|
|
40
40
|
} satisfies Story;
|
|
41
|
-
|
|
42
|
-
export const MaxSizes = () => {
|
|
43
|
-
const bKB = 1024;
|
|
44
|
-
const bMB = 1024 * bKB;
|
|
45
|
-
|
|
46
|
-
const dKB = 1000;
|
|
47
|
-
const dMB = 1000 * dKB;
|
|
48
|
-
|
|
49
|
-
const binarySizes = [
|
|
50
|
-
10 * bMB,
|
|
51
|
-
5 * bMB,
|
|
52
|
-
1 * bMB,
|
|
53
|
-
500 * bKB,
|
|
54
|
-
100 * bKB,
|
|
55
|
-
50 * bKB,
|
|
56
|
-
10 * bKB,
|
|
57
|
-
5 * bKB,
|
|
58
|
-
1 * bKB,
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
const decimalSizes = [
|
|
62
|
-
10 * dMB,
|
|
63
|
-
5 * dMB,
|
|
64
|
-
1 * dMB,
|
|
65
|
-
500 * dKB,
|
|
66
|
-
100 * dKB,
|
|
67
|
-
50 * dKB,
|
|
68
|
-
10 * dKB,
|
|
69
|
-
5 * dKB,
|
|
70
|
-
1 * dKB,
|
|
71
|
-
];
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<div style={{ display: 'flex', gap: '1rem' }}>
|
|
75
|
-
<div style={{ flex: 1 }}>
|
|
76
|
-
{binarySizes.map((maxSize) => (
|
|
77
|
-
<Field key={maxSize} label={`Max size: ${maxSize} bytes`}>
|
|
78
|
-
<Upload usLabel="Pick a file, any file" maxSize={maxSize} />
|
|
79
|
-
</Field>
|
|
80
|
-
))}
|
|
81
|
-
</div>
|
|
82
|
-
<div style={{ flex: 1 }}>
|
|
83
|
-
{decimalSizes.map((maxSize) => (
|
|
84
|
-
<Field key={maxSize} label={`Max size: ${maxSize} bytes`}>
|
|
85
|
-
<Upload usLabel="Pick a file, any file" maxSize={maxSize} />
|
|
86
|
-
</Field>
|
|
87
|
-
))}
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
);
|
|
91
|
-
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { fn } from '@storybook/test';
|
|
3
|
+
|
|
4
|
+
import Upload from '.';
|
|
5
|
+
import { MAX_SIZE_DEFAULT } from './Upload';
|
|
6
|
+
import { Field } from '../field/Field';
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
component: Upload,
|
|
10
|
+
title: 'Forms/Upload/Tests',
|
|
11
|
+
argTypes: {
|
|
12
|
+
maxSize: {
|
|
13
|
+
control: {
|
|
14
|
+
type: 'number',
|
|
15
|
+
min: 0,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
} satisfies Meta<typeof Upload>;
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
|
|
23
|
+
export const MaxSizes = () => {
|
|
24
|
+
const bKB = 1024;
|
|
25
|
+
const bMB = 1024 * bKB;
|
|
26
|
+
|
|
27
|
+
const dKB = 1000;
|
|
28
|
+
const dMB = 1000 * dKB;
|
|
29
|
+
|
|
30
|
+
const binarySizes = [
|
|
31
|
+
10 * bMB,
|
|
32
|
+
5 * bMB,
|
|
33
|
+
1 * bMB,
|
|
34
|
+
500 * bKB,
|
|
35
|
+
100 * bKB,
|
|
36
|
+
50 * bKB,
|
|
37
|
+
10 * bKB,
|
|
38
|
+
5 * bKB,
|
|
39
|
+
1 * bKB,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const decimalSizes = [
|
|
43
|
+
10 * dMB,
|
|
44
|
+
5 * dMB,
|
|
45
|
+
1 * dMB,
|
|
46
|
+
500 * dKB,
|
|
47
|
+
100 * dKB,
|
|
48
|
+
50 * dKB,
|
|
49
|
+
10 * dKB,
|
|
50
|
+
5 * dKB,
|
|
51
|
+
1 * dKB,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div style={{ display: 'flex', gap: '1rem' }}>
|
|
56
|
+
<div style={{ flex: 1 }}>
|
|
57
|
+
{binarySizes.map((maxSize) => (
|
|
58
|
+
<Field key={maxSize} label={`Max size: ${maxSize} bytes`}>
|
|
59
|
+
<Upload usLabel="Pick a file, any file" maxSize={maxSize} />
|
|
60
|
+
</Field>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
<div style={{ flex: 1 }}>
|
|
64
|
+
{decimalSizes.map((maxSize) => (
|
|
65
|
+
<Field key={maxSize} label={`Max size: ${maxSize} bytes`}>
|
|
66
|
+
<Upload usLabel="Pick a file, any file" maxSize={maxSize} />
|
|
67
|
+
</Field>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const AllVariants = () => {
|
|
75
|
+
return (
|
|
76
|
+
<div style={{ display: 'flex', gap: '1rem' }}>
|
|
77
|
+
<div style={{ flex: 1 }}>
|
|
78
|
+
<Field key="small" label="Small Upload">
|
|
79
|
+
<Upload usLabel="Pick a file, any file" size="sm" />
|
|
80
|
+
</Field>
|
|
81
|
+
<Field key="medium" label="Medium Upload">
|
|
82
|
+
<Upload usLabel="Pick a file, any file" size="md" />
|
|
83
|
+
</Field>
|
|
84
|
+
<Field key="large" label="Large Upload">
|
|
85
|
+
<Upload usLabel="Pick a file, any file" size="lg" />
|
|
86
|
+
</Field>
|
|
87
|
+
<Field key="disabled" label="Disabled Upload">
|
|
88
|
+
<Upload usLabel="Pick a file, any file" usDisabled />
|
|
89
|
+
</Field>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
};
|
package/src/upload/Upload.tsx
CHANGED
|
@@ -12,11 +12,6 @@ import { postData, asyncFileRead, isSizeValid, isTypeValid, getFileType } from '
|
|
|
12
12
|
import { PostDataFetcher, PostDataHTTPOptions, ResponseError } from './utils/postData/postData';
|
|
13
13
|
import { ProcessIndicatorStatus } from '../processIndicator';
|
|
14
14
|
|
|
15
|
-
/*
|
|
16
|
-
* This delay is required for the isError/isSuccess to be fired after isProcessing so the processIndicator, will be
|
|
17
|
-
* rendered first and then updated with the right status.
|
|
18
|
-
*/
|
|
19
|
-
const ANIMATION_FIX = 10;
|
|
20
15
|
export const MAX_SIZE_DEFAULT = 5000000;
|
|
21
16
|
|
|
22
17
|
export enum UploadStep {
|
|
@@ -85,7 +80,7 @@ export class Upload extends Component<UploadProps, UploadState> {
|
|
|
85
80
|
declare props: UploadProps & Required<Pick<UploadProps, keyof typeof Upload.defaultProps>>;
|
|
86
81
|
|
|
87
82
|
static defaultProps = {
|
|
88
|
-
animationDelay:
|
|
83
|
+
animationDelay: 300,
|
|
89
84
|
maxSize: MAX_SIZE_DEFAULT,
|
|
90
85
|
psButtonDisabled: false,
|
|
91
86
|
size: 'md',
|
|
@@ -160,41 +155,6 @@ export class Upload extends Component<UploadProps, UploadState> {
|
|
|
160
155
|
}
|
|
161
156
|
}
|
|
162
157
|
|
|
163
|
-
onAnimationCompleted = async (status: ProcessIndicatorStatus) => {
|
|
164
|
-
const { response, isProcessing, fileName } = this.state;
|
|
165
|
-
const { animationDelay } = this.props;
|
|
166
|
-
|
|
167
|
-
if (isProcessing && status === 'succeeded') {
|
|
168
|
-
const { onSuccess } = this.props;
|
|
169
|
-
this.timeouts = window.setTimeout(() => {
|
|
170
|
-
this.setState(
|
|
171
|
-
{
|
|
172
|
-
isProcessing: false,
|
|
173
|
-
isComplete: true,
|
|
174
|
-
},
|
|
175
|
-
onSuccess
|
|
176
|
-
? () => {
|
|
177
|
-
onSuccess(response as string | Response, fileName);
|
|
178
|
-
}
|
|
179
|
-
: undefined,
|
|
180
|
-
);
|
|
181
|
-
}, animationDelay);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (isProcessing && status === 'failed') {
|
|
185
|
-
const { onFailure } = this.props;
|
|
186
|
-
this.timeouts = window.setTimeout(() => {
|
|
187
|
-
this.setState(
|
|
188
|
-
{
|
|
189
|
-
isProcessing: false,
|
|
190
|
-
isComplete: true,
|
|
191
|
-
},
|
|
192
|
-
onFailure ? () => onFailure(response) : undefined,
|
|
193
|
-
);
|
|
194
|
-
}, animationDelay);
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
|
|
198
158
|
asyncPost = async (file: File) => {
|
|
199
159
|
const { httpOptions, fetcher } = this.props;
|
|
200
160
|
if (httpOptions == null) {
|
|
@@ -209,15 +169,37 @@ export class Upload extends Component<UploadProps, UploadState> {
|
|
|
209
169
|
return postData(httpOptions, formData, fetcher);
|
|
210
170
|
};
|
|
211
171
|
|
|
212
|
-
|
|
213
|
-
|
|
172
|
+
handleUploadComplete = (type: 'success' | 'error', response: unknown) => {
|
|
173
|
+
const { animationDelay, onSuccess, onFailure } = this.props;
|
|
174
|
+
const { fileName } = this.state;
|
|
175
|
+
|
|
176
|
+
window.clearTimeout(this.timeouts);
|
|
214
177
|
this.timeouts = window.setTimeout(() => {
|
|
215
|
-
this.setState(
|
|
178
|
+
this.setState(
|
|
179
|
+
{
|
|
180
|
+
isProcessing: false,
|
|
181
|
+
isComplete: true,
|
|
182
|
+
},
|
|
183
|
+
() => {
|
|
184
|
+
if (type === 'success') {
|
|
185
|
+
onSuccess?.(response as string | Response, fileName);
|
|
186
|
+
} else {
|
|
187
|
+
onFailure?.(response);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
}, animationDelay);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
asyncResponse = (response: unknown, type: 'success' | 'error') => {
|
|
195
|
+
this.setState(
|
|
196
|
+
{
|
|
216
197
|
response,
|
|
217
198
|
isError: type === 'error',
|
|
218
199
|
isSuccess: type === 'success',
|
|
219
|
-
}
|
|
220
|
-
|
|
200
|
+
},
|
|
201
|
+
() => this.handleUploadComplete(type, response),
|
|
202
|
+
);
|
|
221
203
|
};
|
|
222
204
|
|
|
223
205
|
handleOnClear: React.MouseEventHandler<HTMLButtonElement> = (event) => {
|
|
@@ -395,8 +377,6 @@ export class Upload extends Component<UploadProps, UploadState> {
|
|
|
395
377
|
/>
|
|
396
378
|
)}
|
|
397
379
|
|
|
398
|
-
{/* Starts render the step when isSuccess is true so markup is there when css transition kicks in
|
|
399
|
-
css transition to work properly */}
|
|
400
380
|
{(isSuccess || isComplete) && !isError && (
|
|
401
381
|
<CompleteStep
|
|
402
382
|
fileName={fileName}
|
|
@@ -443,7 +423,6 @@ export class Upload extends Component<UploadProps, UploadState> {
|
|
|
443
423
|
psButtonText={psButtonText || intl.formatMessage(messages.psButtonText)}
|
|
444
424
|
psProcessingText={psProcessingText || intl.formatMessage(messages.psProcessingText)}
|
|
445
425
|
psButtonDisabled={psButtonDisabled}
|
|
446
|
-
onAnimationCompleted={async (status) => this.onAnimationCompleted(status)}
|
|
447
426
|
onClear={(event) => this.handleOnClear(event)}
|
|
448
427
|
/>
|
|
449
428
|
)}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import Button from '../../../button';
|
|
2
2
|
import { Status, Typography } from '../../../common';
|
|
3
|
-
import ProcessIndicator
|
|
3
|
+
import ProcessIndicator from '../../../processIndicator';
|
|
4
4
|
import Title from '../../../title';
|
|
5
5
|
|
|
6
6
|
export interface ProcessingStepProps {
|
|
7
7
|
isComplete: boolean;
|
|
8
8
|
isError: boolean;
|
|
9
9
|
isSuccess: boolean;
|
|
10
|
-
onAnimationCompleted: (status: ProcessIndicatorStatus) => void;
|
|
11
10
|
onClear: React.MouseEventHandler<HTMLButtonElement>;
|
|
12
11
|
psButtonText: string;
|
|
13
12
|
psProcessingText: string;
|
|
@@ -18,7 +17,6 @@ export default function ProcessingStep({
|
|
|
18
17
|
isComplete,
|
|
19
18
|
isError,
|
|
20
19
|
isSuccess,
|
|
21
|
-
onAnimationCompleted,
|
|
22
20
|
onClear,
|
|
23
21
|
psButtonText,
|
|
24
22
|
psProcessingText,
|
|
@@ -35,10 +33,7 @@ export default function ProcessingStep({
|
|
|
35
33
|
return (
|
|
36
34
|
<div className="droppable-processing-card droppable-card" aria-hidden={isComplete}>
|
|
37
35
|
<div className="droppable-card-content">
|
|
38
|
-
<ProcessIndicator
|
|
39
|
-
status={processStatus}
|
|
40
|
-
onAnimationCompleted={(status) => onAnimationCompleted(status)}
|
|
41
|
-
/>
|
|
36
|
+
<ProcessIndicator status={processStatus} />
|
|
42
37
|
<Title className="m-y-2" type={Typography.TITLE_BODY} aria-live="polite">
|
|
43
38
|
{psProcessingText}
|
|
44
39
|
</Title>
|
|
@@ -161,6 +161,7 @@ const UploadInput = ({
|
|
|
161
161
|
}: UploadInputProps) => {
|
|
162
162
|
const inputAttributes = useInputAttributes({ nonLabelable: true });
|
|
163
163
|
const [markedFileForDelete, setMarkedFileForDelete] = useState<UploadedFile | null>(null);
|
|
164
|
+
const [lastAttemptedDeleteId, setLastAttemptedDeleteId] = useState<string | number | null>(null);
|
|
164
165
|
const [mounted, setMounted] = useState(false);
|
|
165
166
|
const { formatMessage } = useIntl();
|
|
166
167
|
const uploadInputRef = useRef<HTMLInputElement | null>(null);
|
|
@@ -321,7 +322,13 @@ const UploadInput = ({
|
|
|
321
322
|
}
|
|
322
323
|
}, [onFilesChange, uploadedFiles]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
323
324
|
|
|
324
|
-
|
|
325
|
+
type NextFocusable =
|
|
326
|
+
| HTMLDivElement
|
|
327
|
+
| UploadItemRef
|
|
328
|
+
| { ref: HTMLDivElement | UploadItemRef; target: 'button' | 'link' }
|
|
329
|
+
| null;
|
|
330
|
+
|
|
331
|
+
const [nextFocusable, setNextFocusable] = useState<NextFocusable>(
|
|
325
332
|
uploadInputRef.current,
|
|
326
333
|
);
|
|
327
334
|
|
|
@@ -331,7 +338,15 @@ const UploadInput = ({
|
|
|
331
338
|
});
|
|
332
339
|
|
|
333
340
|
const filesCount = fileRefs.length;
|
|
334
|
-
let next:
|
|
341
|
+
let next: UploadItemRef | HTMLDivElement | null = uploadInputRef.current;
|
|
342
|
+
let focusTarget: 'button' | 'link' = 'button';
|
|
343
|
+
|
|
344
|
+
// If there will be no files left after deletion, focus the upload button
|
|
345
|
+
if (filesCount === 1) {
|
|
346
|
+
next = uploadInputRef.current;
|
|
347
|
+
setNextFocusable(next);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
335
350
|
|
|
336
351
|
if (filesCount > 1) {
|
|
337
352
|
const currentFileIndex = fileRefs.findIndex((file) => file?.id === fileId);
|
|
@@ -344,15 +359,68 @@ const UploadInput = ({
|
|
|
344
359
|
} else {
|
|
345
360
|
next = fileRefs[currentFileIndex + 1];
|
|
346
361
|
}
|
|
362
|
+
|
|
363
|
+
// If next is an UploadItemRef, check if it has a URL (succeeded)
|
|
364
|
+
if (next && 'status' in next) {
|
|
365
|
+
// Find the file object for this ref
|
|
366
|
+
const fileObj = uploadedFiles.find(f => f.id === next?.id);
|
|
367
|
+
if (
|
|
368
|
+
fileObj &&
|
|
369
|
+
(fileObj.status === Status.SUCCEEDED || fileObj.status === Status.DONE) &&
|
|
370
|
+
fileObj.url
|
|
371
|
+
) {
|
|
372
|
+
focusTarget = 'link';
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
setNextFocusable(() => {
|
|
376
|
+
if (next && typeof (next as UploadItemRef).focus === 'function') {
|
|
377
|
+
return { ref: next, target: focusTarget };
|
|
378
|
+
}
|
|
379
|
+
return next;
|
|
380
|
+
});
|
|
347
381
|
}
|
|
348
|
-
setNextFocusable(next);
|
|
349
382
|
};
|
|
350
383
|
|
|
351
384
|
const handleRefocus = () => {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
385
|
+
const focusTarget = nextFocusable;
|
|
386
|
+
if (lastAttemptedDeleteId) {
|
|
387
|
+
setLastAttemptedDeleteId(null);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (focusTarget) {
|
|
391
|
+
// If there are no files left, focus the upload button
|
|
392
|
+
if (
|
|
393
|
+
uploadedFiles.length === 0 &&
|
|
394
|
+
uploadInputRef.current &&
|
|
395
|
+
typeof uploadInputRef.current.focus === 'function'
|
|
396
|
+
) {
|
|
397
|
+
setTimeout(() => {
|
|
398
|
+
uploadInputRef.current!.focus();
|
|
399
|
+
}, 0);
|
|
400
|
+
} else if (
|
|
401
|
+
typeof focusTarget === 'object' &&
|
|
402
|
+
'ref' in focusTarget &&
|
|
403
|
+
focusTarget.ref &&
|
|
404
|
+
typeof focusTarget.ref.focus === 'function'
|
|
405
|
+
) {
|
|
406
|
+
setTimeout(() => {
|
|
407
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
408
|
+
if (
|
|
409
|
+
focusTarget.ref &&
|
|
410
|
+
typeof (focusTarget.ref as UploadItemRef).focus === 'function'
|
|
411
|
+
) {
|
|
412
|
+
// @ts-expect-error: focus may not exist on all possible ref types, but is safe here
|
|
413
|
+
(focusTarget.ref as UploadItemRef).focus(focusTarget.target);
|
|
414
|
+
}
|
|
415
|
+
}, 0);
|
|
416
|
+
} else if (
|
|
417
|
+
focusTarget &&
|
|
418
|
+
typeof (focusTarget as UploadItemRef).focus === 'function'
|
|
419
|
+
) {
|
|
420
|
+
setTimeout(() => {
|
|
421
|
+
(focusTarget as UploadItemRef).focus();
|
|
422
|
+
}, 0);
|
|
423
|
+
}
|
|
356
424
|
}
|
|
357
425
|
};
|
|
358
426
|
|
|
@@ -391,10 +459,14 @@ const UploadInput = ({
|
|
|
391
459
|
onDelete={
|
|
392
460
|
file.status === Status.FAILED
|
|
393
461
|
? async () => {
|
|
462
|
+
setLastAttemptedDeleteId(file.id);
|
|
394
463
|
await removeFile(file);
|
|
395
464
|
handleRefocus();
|
|
396
465
|
}
|
|
397
|
-
: () =>
|
|
466
|
+
: () => {
|
|
467
|
+
setLastAttemptedDeleteId(file.id);
|
|
468
|
+
setMarkedFileForDelete(file);
|
|
469
|
+
}
|
|
398
470
|
}
|
|
399
471
|
onDownload={onDownload}
|
|
400
472
|
onFocus={() => handleFocus(file.id)}
|
|
@@ -451,6 +523,7 @@ const UploadInput = ({
|
|
|
451
523
|
void removeFile(markedFileForDelete);
|
|
452
524
|
}
|
|
453
525
|
setMarkedFileForDelete(null);
|
|
526
|
+
setLastAttemptedDeleteId(null);
|
|
454
527
|
}}
|
|
455
528
|
>
|
|
456
529
|
{deleteConfirm?.confirmText || formatMessage(MESSAGES.deleteModalConfirmButtonText)}
|