Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: FileInput component (internal) #2964

Merged
merged 2 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

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
Loading