Skip to content

Commit

Permalink
feat: File token group [internal] (#2950)
Browse files Browse the repository at this point in the history
Co-authored-by: Katie George <[email protected]>
  • Loading branch information
katiegeorge and Katie George authored Nov 11, 2024
1 parent 89c8f43 commit 6539f15
Show file tree
Hide file tree
Showing 29 changed files with 1,059 additions and 373 deletions.
3 changes: 2 additions & 1 deletion pages/file-upload/permutations.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const permutations = createPermutations<Omit<FileUploadProps, 'dismissAriaLabel'
value: [
[
new File([new Blob(['demo content 1'])], 'demo file 1', { type: 'image/*' }),
new File([new Blob(['demo content 2'])], 'demo file 2', { type: 'image/*' }),
new File([new Blob(['demo content 2'])], 'demo file 2 with long file name', { type: 'image/*' }),
],
],
showFileSize: [true],
Expand All @@ -37,6 +37,7 @@ const permutations = createPermutations<Omit<FileUploadProps, 'dismissAriaLabel'
constraintText: ['File size must not exceed 1 MB'],
fileErrors: [undefined, ['File size is above 1 MB', 'File size is above 1 MB']],
fileWarnings: [undefined, ['File type is image', 'File type is image']],
fileTokenAlignment: ['vertical', 'horizontal'],
},
]);

Expand Down
6 changes: 6 additions & 0 deletions pages/file-upload/scenario-standalone.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { validateContractFiles } from './validations';
export default function FileUploadScenarioStandalone() {
const contractsRef = useRef<FileUploadProps.Ref>(null);
const [acceptMultiple, setAcceptMultiple] = useState(true);
const [verticalAlignment, setVerticalAlignment] = useState(true);
const formState = useContractFilesForm();

const contractsValidationErrors = validateContractFiles(formState.files);
Expand Down Expand Up @@ -40,11 +41,16 @@ export default function FileUploadScenarioStandalone() {
Accept multiple files
</Checkbox>

<Checkbox checked={verticalAlignment} onChange={event => setVerticalAlignment(event.detail.checked)}>
Vertical alignment
</Checkbox>

<FormField
label={acceptMultiple ? 'Contracts' : 'Contract'}
description={acceptMultiple ? 'Upload your contract with all amendments' : 'Upload your contract'}
>
<FileUpload
fileTokenAlignment={verticalAlignment ? 'vertical' : 'horizontal'}
ref={contractsRef}
multiple={acceptMultiple}
tokenLimit={3}
Expand Down
14 changes: 14 additions & 0 deletions src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -7453,6 +7453,20 @@ is provided by its parent form field component.
"optional": true,
"type": "ReadonlyArray<null | string>",
},
{
"description": "Alignment of the file tokens. Defaults to "vertical".",
"inlineType": {
"name": "FileUploadProps.FileTokenAlignment",
"type": "union",
"values": [
"vertical",
"horizontal",
],
},
"name": "fileTokenAlignment",
"optional": true,
"type": "string",
},
{
"description": "An array of file warnings corresponding to the files in the \`value\`.",
"name": "fileWarnings",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,10 +250,6 @@ exports[`test-utils selectors 1`] = `
"awsui_root_gwq0h",
],
"file-upload": [
"awsui_file-option-last-modified_ezgb4",
"awsui_file-option-name_ezgb4",
"awsui_file-option-size_ezgb4",
"awsui_file-option-thumbnail-image_ezgb4",
"awsui_hints_1ubbm",
"awsui_root_1ubbm",
],
Expand Down Expand Up @@ -339,6 +335,10 @@ exports[`test-utils selectors 1`] = `
"awsui_dropdown_qwoo0",
"awsui_file-input-button_181f9",
"awsui_file-input_181f9",
"awsui_file-option-last-modified_ofwwb",
"awsui_file-option-name_ofwwb",
"awsui_file-option-size_ofwwb",
"awsui_file-option-thumbnail_ofwwb",
"awsui_filter-container_z5mul",
"awsui_filtering-match-highlight_1p2cx",
"awsui_handle_sdha6",
Expand All @@ -365,6 +365,7 @@ exports[`test-utils selectors 1`] = `
"awsui_root_1qprf",
"awsui_root_1t44z",
"awsui_root_1tk3k",
"awsui_root_9f1dn",
"awsui_root_qwoo0",
"awsui_root_vrgzu",
"awsui_selectable-item_15o6u",
Expand All @@ -374,6 +375,7 @@ exports[`test-utils selectors 1`] = `
"awsui_ticks--y_f0fot",
"awsui_title_1kjc7",
"awsui_toggle_gfwv3",
"awsui_token_ofwwb",
"awsui_value_10ipo",
"awsui_wrapper_1wepg",
],
Expand Down
118 changes: 0 additions & 118 deletions src/file-upload/__tests__/file-upload.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import FileUpload, { FileUploadProps } from '../../../lib/components/file-upload
import createWrapper from '../../../lib/components/test-utils/dom';
import FileDropzoneWrapper from '../../../lib/components/test-utils/dom/internal/file-dropzone';

import tokenListSelectors from '../../../lib/components/internal/components/token-list/styles.selectors.js';

jest.mock('@cloudscape-design/component-toolkit/internal', () => ({
...jest.requireActual('@cloudscape-design/component-toolkit/internal'),
warnOnce: jest.fn(),
Expand Down Expand Up @@ -214,11 +212,6 @@ describe('FileUpload input', () => {
});

describe('File upload tokens', () => {
test('token list is not rendered when `multiple=false`', () => {
const wrapper = render({ multiple: false, value: [file1] });
expect(wrapper.find(tokenListSelectors.root)).toBeNull();
});

test(`when multiple=true all file tokens are shown`, () => {
const wrapper = render({ multiple: true, value: [file1, file2] });

Expand All @@ -231,100 +224,6 @@ describe('File upload tokens', () => {
expect(wrapper.findFileToken(2)!.getElement()).toHaveTextContent('test-file-2.txt');
});

test('dev warning is issued when using multiple files with a singular file upload', () => {
render({ value: [file1, file2] });

expect(warnOnce).toHaveBeenCalledTimes(1);
expect(warnOnce).toHaveBeenCalledWith('FileUpload', 'Value must be an array of size 0 or 1 when `multiple=false`.');
});

test('file token remove button has ARIA label set', () => {
const wrapper = render({ value: [file1] });
expect(wrapper.findFileToken(1)!.findRemoveButton()!.getElement()).toHaveAccessibleName('Remove file 1');
});

test('selected file can be removed', () => {
const wrapperSingular = render({ value: [file1] });
wrapperSingular.findFileToken(1)!.findRemoveButton()!.click();

expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ detail: { value: [] } }));

const wrapperMultiple = render({ multiple: true, value: [file1, file2] });
wrapperMultiple.findFileToken(1)!.findRemoveButton()!.click();

expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ detail: { value: [file2] } }));
});

test('file token only shows name by default', () => {
const wrapper = render({ value: [file1] });

expect(wrapper.findFileToken(1)!.findFileName().getElement()).toHaveTextContent('test-file-1.txt');
expect(wrapper.findFileToken(1)!.findFileSize()).toBe(null);
expect(wrapper.findFileToken(1)!.findFileLastModified()).toBe(null);
expect(wrapper.findFileToken(1)!.findFileThumbnail()).toBe(null);
});

test('file token metadata can be opt-in', () => {
const wrapper = render({
value: [file1],
showFileSize: true,
showFileLastModified: true,
});
expect(wrapper.findFileToken(1)!.findFileName().getElement()).toHaveTextContent('test-file-1.txt');
expect(wrapper.findFileToken(1)!.findFileSize()!.getElement()).toHaveTextContent('0.01 KB');
expect(wrapper.findFileToken(1)!.findFileLastModified()!.getElement()).toHaveTextContent('2020-06-01T00:00:00');
});

test('thumbnail is only shown when file type starts with "image"', () => {
expect(
render({ value: [file1], showFileThumbnail: true })
.findFileToken(1)!
.findFileThumbnail()
).toBe(null);

expect(
render({ value: [file2], showFileThumbnail: true })
.findFileToken(1)!
.findFileThumbnail()
).not.toBe(null);
});

test('selected file size can be customized', () => {
const wrapper = render({
value: [file1],
showFileSize: true,
i18nStrings: {
...defaultProps.i18nStrings,
formatFileSize: sizeInBytes => `${sizeInBytes} bytes`,
},
});
expect(wrapper.findFileToken(1)!.findFileSize()!.getElement()).toHaveTextContent('14 bytes');
});

test('selected file last update timestamp can be customized', () => {
const wrapper = render({
value: [file1],
showFileLastModified: true,
i18nStrings: {
...defaultProps.i18nStrings,
formatFileLastModified: date => `${date.getFullYear()} year`,
},
});
expect(wrapper.findFileToken(1)!.findFileLastModified()!.getElement()).toHaveTextContent('2020 year');
});

test('the `tokenLimit` property controls the number of tokens shown by default', () => {
const wrapper = render({ multiple: true, value: [file1, file2], tokenLimit: 1 });
expect(wrapper.findFileTokens()).toHaveLength(1);
expect(wrapper.getElement().textContent).toContain('Show more files');
});

test('file tokens have aria labels set to file names', () => {
const wrapper = render({ multiple: true, value: [file1, file2] });
expect(wrapper.findFileToken(1)!.getElement()).toHaveAttribute('aria-label', file1.name);
expect(wrapper.findFileToken(2)!.getElement()).toHaveAttribute('aria-label', file2.name);
});

test('file errors are associated to file tokens', () => {
const wrapper = render({ multiple: true, value: [file1, file2], fileErrors: ['Error 1', 'Error 2'] });
expect(wrapper.findFileToken(1)!.getElement()).toHaveAccessibleDescription('Error 1');
Expand Down Expand Up @@ -358,23 +257,6 @@ describe('File upload dropzone', () => {
});

describe('Focusing behavior', () => {
test.each([1, 2])(
`Focus is dispatched to the next token when the token before it is removed, tokenLimit=%s`,
tokenLimit => {
const wrapper = renderStateful({ multiple: true, value: [file1, file2], tokenLimit });
wrapper.findFileToken(1)!.findRemoveButton().click();

expect(wrapper.findFileToken(1)!.findRemoveButton().getElement()).toHaveFocus();
}
);

test('Focus is dispatched to the previous token when removing the token at the end', () => {
const wrapper = renderStateful({ multiple: true, value: [file1, file2] });
wrapper.findFileToken(2)!.findRemoveButton().click();

expect(wrapper.findFileToken(1)!.findRemoveButton().getElement()).toHaveFocus();
});

test.each([false, true])(
'Focus is dispatched to the file input when the last token is removed, multiple=%s',
multiple => {
Expand Down
47 changes: 0 additions & 47 deletions src/file-upload/file-option/index.tsx

This file was deleted.

34 changes: 0 additions & 34 deletions src/file-upload/file-option/styles.scss

This file was deleted.

6 changes: 6 additions & 0 deletions src/file-upload/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export interface FileUploadProps extends BaseComponentProps, FormFieldCommonVali
* An array of file warnings corresponding to the files in the `value`.
*/
fileWarnings?: ReadonlyArray<null | string>;
/**
* Alignment of the file tokens. Defaults to "vertical".
*/
fileTokenAlignment?: FileUploadProps.FileTokenAlignment;
/**
* An object containing all the localized strings required by the component:
* * `uploadButtonText` (function): A function to render the text of the file upload button. It takes `multiple` attribute to define plurality.
Expand All @@ -89,6 +93,8 @@ export namespace FileUploadProps {
file: File;
}

export type FileTokenAlignment = 'vertical' | 'horizontal';

export interface I18nStrings {
uploadButtonText: (multiple: boolean) => string;
dropzoneText: (multiple: boolean) => string;
Expand Down
Loading

0 comments on commit 6539f15

Please sign in to comment.