Skip to content

Commit

Permalink
Revert "revert: "feat: FileInput component (internal) (#2912)" (#2959)"
Browse files Browse the repository at this point in the history
This reverts commit 3327bab.
  • Loading branch information
katiegeorge authored Oct 31, 2024
1 parent 02a8ba3 commit 104e09f
Show file tree
Hide file tree
Showing 10 changed files with 402 additions and 154 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,6 @@ exports[`test-utils selectors 1`] = `
"awsui_file-option-thumbnail-image_ezgb4",
"awsui_hints_1ubbm",
"awsui_root_1ubbm",
"awsui_upload-button_4xu1k",
"awsui_upload-input_4xu1k",
],
"flashbar": [
"awsui_action-button_1q84n",
Expand Down Expand Up @@ -339,6 +337,8 @@ exports[`test-utils selectors 1`] = `
"awsui_description_1wepg",
"awsui_disabled_15o6u",
"awsui_dropdown_qwoo0",
"awsui_file-input-button_181f9",
"awsui_file-input_181f9",
"awsui_filter-container_z5mul",
"awsui_filtering-match-highlight_1p2cx",
"awsui_handle_sdha6",
Expand All @@ -359,6 +359,7 @@ exports[`test-utils selectors 1`] = `
"awsui_placeholder_18eso",
"awsui_recovery_vrgzu",
"awsui_root_11n0s",
"awsui_root_181f9",
"awsui_root_1fcus",
"awsui_root_1kjc7",
"awsui_root_1qprf",
Expand Down
113 changes: 0 additions & 113 deletions src/file-upload/file-input/index.tsx

This file was deleted.

30 changes: 0 additions & 30 deletions src/file-upload/file-input/styles.scss

This file was deleted.

12 changes: 6 additions & 6 deletions src/file-upload/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useFormFieldContext } from '../contexts/form-field';
import { ConstraintText, FormFieldError, FormFieldWarning } from '../form-field/internal';
import { getBaseProps } from '../internal/base-component';
import InternalFileDropzone, { useFilesDragging } from '../internal/components/file-dropzone';
import InternalFileInput from '../internal/components/file-input';
import TokenList from '../internal/components/token-list';
import { fireNonCancelableEvent } from '../internal/events';
import checkControlled from '../internal/hooks/check-controlled';
Expand All @@ -22,12 +23,11 @@ import { useUniqueId } from '../internal/hooks/use-unique-id';
import { joinStrings } from '../internal/utils/strings';
import InternalSpaceBetween from '../space-between/internal';
import { Token } from '../token-group/token';
import FileInput from './file-input';
import { FileOption } from './file-option';
import { FileUploadProps } from './interfaces';

import fileInputStyles from '../internal/components/file-input/styles.css.js';
import tokenListStyles from '../internal/components/token-list/styles.css.js';
import fileInputStyles from './file-input/styles.css.js';
import styles from './styles.css.js';

type InternalFileUploadProps = FileUploadProps & InternalBaseComponentProps;
Expand Down Expand Up @@ -65,7 +65,7 @@ function InternalFileUpload(
},
listItemSelector: `.${tokenListStyles['list-item']}`,
showMoreSelector: `.${tokenListStyles.toggle}`,
fallbackSelector: `.${fileInputStyles['upload-input']}`,
fallbackSelector: `.${fileInputStyles['file-input']}`,
});

const baseProps = getBaseProps(restProps);
Expand Down Expand Up @@ -128,19 +128,19 @@ function InternalFileUpload(
{i18nStrings.dropzoneText(multiple)}
</InternalFileDropzone>
) : (
<FileInput
<InternalFileInput
ref={ref}
accept={accept}
ariaRequired={ariaRequired}
multiple={multiple}
onChange={handleFilesChange}
onChange={event => handleFilesChange(event.detail.value)}
value={value}
{...restProps}
ariaDescribedby={ariaDescribedBy}
invalid={invalid}
>
{i18nStrings.uploadButtonText(multiple)}
</FileInput>
</InternalFileInput>
)}

{(constraintText || errorText || warningText) && (
Expand Down
157 changes: 157 additions & 0 deletions src/internal/components/file-input/__tests__/file-input.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { fireEvent, render as testingLibraryRender, screen } from '@testing-library/react';

import { warnOnce } from '@cloudscape-design/component-toolkit/internal';

import '../../../../__a11y__/to-validate-a11y';
import InternalFileInput, { FileInputProps } from '../../../../../lib/components/internal/components/file-input';
import FileInputWrapper from '../../../../../lib/components/test-utils/dom/internal/file-input';

jest.mock('@cloudscape-design/component-toolkit/internal', () => ({
...jest.requireActual('@cloudscape-design/component-toolkit/internal'),
warnOnce: jest.fn(),
}));

jest.mock('../../../../../lib/components/internal/utils/date-time', () => ({
formatDateTime: () => '2020-06-01T00:00:00',
}));

const onChange = jest.fn();

afterEach(() => {
(warnOnce as jest.Mock).mockReset();
onChange.mockReset();
});

const defaultProps: FileInputProps = {
value: [],
onChange,
};

const file1 = new File([new Blob(['Test content 1'], { type: 'text/plain' })], 'test-file-1.txt', {
type: 'text/plain',
lastModified: 1590962400000,
});
const file2 = new File([new Blob(['Test content 2'], { type: 'text/plain' })], 'test-file-2.txt', {
type: 'image/png',
lastModified: 1590962400000,
});

function render(props: Partial<FileInputProps>) {
const renderResult = testingLibraryRender(
<div>
<InternalFileInput {...{ ...defaultProps, ...props }}>Choose files</InternalFileInput>
<div id="test-label">Test label</div>
</div>
);
const element = renderResult.container.querySelector<HTMLElement>(`.${FileInputWrapper.rootSelector}`)!;
return new FileInputWrapper(element)!;
}

describe('FileInput input', () => {
test('`multiple` property is assigned', () => {
expect(render({ multiple: false }).findNativeInput().getElement()).not.toHaveAttribute('multiple');
expect(render({ multiple: true }).findNativeInput().getElement()).toHaveAttribute('multiple');
});

test('`accept` property is assigned', () => {
expect(render({ accept: 'custom' }).findNativeInput().getElement()).toHaveAttribute('accept', 'custom');
});

test('`ariaRequired` property is assigned', () => {
expect(render({ ariaRequired: false }).findNativeInput().getElement()).not.toHaveAttribute('aria-required');
expect(render({ ariaRequired: true }).findNativeInput().getElement()).toHaveAttribute('aria-required');
});

test('`ariaLabelledby` property is assigned', () => {
render({ ariaLabelledby: 'test-label' });
expect(screen.getByLabelText('Test label')).toBeDefined();
});

test('`ariaLabelledby` is joined with `uploadButtonText`', () => {
const wrapper = render({ ariaLabelledby: 'test-label' });
expect(wrapper.findNativeInput().getElement()).toHaveAccessibleName('Test label Choose files');
});

test('`ariaDescribedby` property is assigned', () => {
const uploadButton = render({ ariaDescribedby: 'test-label' }).findNativeInput().getElement();
expect(uploadButton).toHaveAccessibleDescription('Test label');
});

test('`invalid` property is assigned', () => {
expect(render({ invalid: false }).findNativeInput().getElement()).not.toBeInvalid();
expect(render({ invalid: true }).findNativeInput().getElement()).toBeInvalid();
});

test('`text` property is assigned', () => {
expect(render({}).findTrigger().getElement()).toHaveTextContent('Choose files');
});

test('uses `text` as aria label when `ariaLabel` is undefined', () => {
expect(render({}).findNativeInput().getElement()).toHaveAttribute('aria-label', 'Choose files');
});

test('`ariaLabel` takes precedence if both `ariaLabel` and `text` are defined', () => {
expect(render({ ariaLabel: 'aria label' }).findNativeInput().getElement()).toHaveAttribute(
'aria-label',
'aria label'
);
});

test('dev warning is issued when `variant` is icon and `ariaLabel` is undefined', () => {
render({ variant: 'icon' });
expect(warnOnce).toHaveBeenCalledTimes(1);
expect(warnOnce).toHaveBeenCalledWith('FileInput', 'Aria label is required with icon variant.');
});

test('dev warning is issued when `onChange` handler is missing', () => {
render({ onChange: undefined });

expect(warnOnce).toHaveBeenCalledTimes(1);
expect(warnOnce).toHaveBeenCalledWith(
'FileInput',
'You provided `value` prop without an `onChange` handler. This will render a read-only component. If the component should be mutable, set an `onChange` handler.'
);
});

test('file upload button can be assigned aria-invalid', () => {
const wrapper = render({ invalid: true });
expect(wrapper.findNativeInput().getElement()).toHaveAttribute('aria-invalid', 'true');
});

test('file input fires onChange with files in details', () => {
const wrapper = render({ multiple: true });
const input = wrapper.findNativeInput().getElement();
Object.defineProperty(input, 'files', { value: [file1, file2] });
fireEvent(input, new CustomEvent('change', { bubbles: true }));

expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ detail: { value: [file1, file2] } }));
// additional equality check, because `expect.objectContaining` above thinks file1 === file2
expect((onChange as jest.Mock).mock.lastCall[0].detail.value[0]).toBe(file1);
expect((onChange as jest.Mock).mock.lastCall[0].detail.value[1]).toBe(file2);
});
});

describe('a11y', () => {
test('multiple empty', async () => {
const wrapper = render({ multiple: true, value: [] });
await expect(wrapper.getElement()).toValidateA11y();
});

test('single', async () => {
const wrapper = render({
value: [file1],
});
await expect(wrapper.getElement()).toValidateA11y();
});

test('multiple', async () => {
const wrapper = render({
multiple: true,
value: [file1, file2],
});
await expect(wrapper.getElement()).toValidateA11y();
});
});
Loading

0 comments on commit 104e09f

Please sign in to comment.