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: Make file input public #2982

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
40 changes: 40 additions & 0 deletions pages/file-input/simple.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useState } from 'react';

import { Box, ColumnLayout, FileInput } from '~components';

export default function DateInputScenario() {
const [files, setFiles] = useState<File[]>([]);
const [files2, setFiles2] = useState<File[]>([]);

return (
<Box padding="l">
<h1>File input</h1>
<ColumnLayout columns={2}>
<div>
<FileInput
multiple={true}
ariaLabel="prompt file input"
variant="icon"
value={files}
onChange={event => setFiles(event.detail.value)}
/>

{files.map((file, index) => (
<div key={index}>{file.name}</div>
))}
</div>
<div>
<FileInput multiple={true} value={files2} onChange={event => setFiles2(event.detail.value)}>
Choose files
</FileInput>

{files2.map((file, index) => (
<div key={index}>{file.name}</div>
))}
</div>
</ColumnLayout>
</Box>
);
}
151 changes: 151 additions & 0 deletions src/__tests__/__snapshots__/documenter.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -7361,6 +7361,157 @@ use the \`id\` attribute, consider setting it on a parent element instead.",
}
`;

exports[`Documenter definition for file-input matches the snapshot: file-input 1`] = `
{
"events": [
{
"cancelable": false,
"description": "Called when the user selects new file(s), or removes a file.
The event \`detail\` contains the current value of the component.",
"detailInlineType": {
"name": "FileInputProps.ChangeDetail",
"properties": [
{
"name": "value",
"optional": false,
"type": "Array<File>",
},
],
"type": "object",
},
"detailType": "FileInputProps.ChangeDetail",
"name": "onChange",
},
],
"functions": [
{
"description": "Sets focus on the file upload button.",
"name": "focus",
"parameters": [],
"returnType": "void",
},
],
"name": "FileInput",
"properties": [
{
"description": "Specifies the native file input \`accept\` attribute to describe the allow-list of file types.",
"name": "accept",
"optional": true,
"type": "string",
},
{
"description": "Adds \`aria-describedby\` to the component. If you're using this component within a form field,
don't set this property because the form field component automatically sets it.
Use this property if the component isn't surrounded by a form field, or you want to override the value
automatically set by the form field (for example, if you have two components within a single form field).

To use it correctly, define an ID for each element that you want to use as a description
and set the property to a string of each ID separated by spaces (for example, \`"id1 id2 id3"\`).
",
"name": "ariaDescribedby",
"optional": true,
"type": "string",
},
{
"description": "Adds \`aria-label\` to the file input element. Use this to provide an accessible name for file inputs
that don't have visible text, and to distinguish between multiple file inputs with identical visible text.",
"name": "ariaLabel",
"optional": true,
"type": "string",
},
{
"description": "Adds \`aria-labelledby\` to the component. If you're using this component within a form field,
don't set this property because the form field component automatically sets it.
Use this property if the component isn't surrounded by a form field, or you want to override the value
automatically set by the form field (for example, if you have two components within a single form field).

To use it correctly, define an ID for the element you want to use as label and set the property to that ID.
",
"name": "ariaLabelledby",
"optional": true,
"type": "string",
},
{
"description": "Specifies whether to add aria-required to the file upload control.",
"name": "ariaRequired",
"optional": true,
"type": "boolean",
},
{
"description": "Text displayed in the file input component. Used as the aria label if ariaLabel is not defined.",
"name": "children",
"optional": true,
"type": "string",
},
{
"deprecatedTag": "Custom CSS is not supported. For testing and other use cases, use [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes).",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid having deprecated className and id in new components

"description": "Adds the specified classes to the root element of the component.",
"name": "className",
"optional": true,
"type": "string",
},
{
"description": "Specifies the ID of the native form element. You can use it to relate
a label element's \`for\` attribute to this control.
It defaults to an automatically generated ID that
is provided by its parent form field component.
",
"name": "controlId",
"optional": true,
"type": "string",
},
{
"deprecatedTag": "The usage of the \`id\` attribute is reserved for internal use cases. For testing and other use cases,
use [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). If you must
use the \`id\` attribute, consider setting it on a parent element instead.",
"description": "Adds the specified ID to the root element of the component.",
"name": "id",
"optional": true,
"type": "string",
},
{
"description": "Overrides the invalidation state. Usually the invalid state
comes from the parent \`FormField\`component,
however sometimes you need to override its
state when you have more than one input within a
single form field.",
"name": "invalid",
"optional": true,
"type": "boolean",
},
{
"description": "Specifies the native file input \`multiple\` attribute to allow users entering more than one file.",
"name": "multiple",
"optional": true,
"type": "boolean",
},
{
"description": "Specifies the currently selected file(s).
If you want to clear the selection, use empty array.",
"name": "value",
"optional": false,
"type": "ReadonlyArray<File>",
},
{
"description": "Variant of the file input. Defaults to "button".",
"inlineType": {
"name": "",
"type": "union",
"values": [
"button",
"icon",
],
},
"name": "variant",
"optional": true,
"type": "string",
},
],
"regions": [],
"releaseStatus": "stable",
}
`;

exports[`Documenter definition for file-upload matches the snapshot: file-upload 1`] = `
{
"events": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,11 @@ exports[`test-utils selectors 1`] = `
"awsui_icon-container_gwq0h",
"awsui_root_gwq0h",
],
"file-input": [
"awsui_file-input-button_1wp4s",
"awsui_file-input_1wp4s",
"awsui_root_1wp4s",
],
"file-upload": [
"awsui_file-option-last-modified_ezgb4",
"awsui_file-option-name_ezgb4",
Expand Down Expand Up @@ -337,8 +342,6 @@ 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,7 +362,6 @@ 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
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ import { fireEvent, render as testingLibraryRender, screen } from '@testing-libr

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';
import '../../__a11y__/to-validate-a11y';
import InternalFileInput, { FileInputProps } from '../../../lib/components/file-input';
import FileInputWrapper from '../../../lib/components/test-utils/dom/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', () => ({
jest.mock('../../../lib/components/internal/utils/date-time', () => ({
formatDateTime: () => '2020-06-01T00:00:00',
}));

Expand Down
22 changes: 22 additions & 0 deletions src/file-input/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import useBaseComponent from '../internal/hooks/use-base-component';
import { applyDisplayName } from '../internal/utils/apply-display-name';
import { FileInputProps } from './interfaces';
import InternalFileInput from './internal';

export { FileInputProps };

const FileInput = React.forwardRef(({ multiple, ...props }: FileInputProps, ref: React.Ref<FileInputProps.Ref>) => {
const baseComponentProps = useBaseComponent('FileInput', {
props: {
multiple,
},
});
return <InternalFileInput multiple={multiple} {...props} {...baseComponentProps} ref={ref} />;
});

applyDisplayName(FileInput, 'FileInput');
export default FileInput;
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { BaseComponentProps } from '../../base-component';
import { FormFieldCommonValidationControlProps } from '../../context/form-field-context';
import { NonCancelableEventHandler } from '../../events';
import { BaseComponentProps } from '../internal/base-component';
import { FormFieldCommonValidationControlProps } from '../internal/context/form-field-context';
import { NonCancelableEventHandler } from '../internal/events';

export interface FileInputProps extends BaseComponentProps, FormFieldCommonValidationControlProps {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ import clsx from 'clsx';

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

import InternalButton from '../../../button/internal';
import { useFormFieldContext } from '../../../contexts/form-field';
import ScreenreaderOnly from '../../components/screenreader-only';
import { fireNonCancelableEvent } from '../../events';
import checkControlled from '../../hooks/check-controlled';
import useForwardFocus from '../../hooks/forward-focus';
import { useUniqueId } from '../../hooks/use-unique-id';
import { joinStrings } from '../../utils/strings';
import InternalButton from '../button/internal';
import { useFormFieldContext } from '../contexts/form-field';
import { getBaseProps } from '../internal/base-component/index.js';
import ScreenreaderOnly from '../internal/components/screenreader-only';
import { fireNonCancelableEvent } from '../internal/events';
import checkControlled from '../internal/hooks/check-controlled';
import useForwardFocus from '../internal/hooks/forward-focus';
import { InternalBaseComponentProps } from '../internal/hooks/use-base-component/index.js';
import { useUniqueId } from '../internal/hooks/use-unique-id';
import { joinStrings } from '../internal/utils/strings';
import { FileInputProps } from './interfaces';

import styles from './styles.css.js';

export { FileInputProps };

const InternalFileInput = React.forwardRef(
(
{
Expand All @@ -31,10 +31,12 @@ const InternalFileInput = React.forwardRef(
onChange,
variant = 'button',
children,
__internalRootRef = null,
...restProps
}: FileInputProps,
}: FileInputProps & InternalBaseComponentProps,
ref: Ref<FileInputProps.Ref>
) => {
const baseProps = getBaseProps(restProps);
const uploadInputRef = useRef<HTMLInputElement>(null);
const uploadButtonLabelId = useUniqueId('upload-button-label');
const formFieldContext = useFormFieldContext(restProps);
Expand Down Expand Up @@ -86,7 +88,7 @@ const InternalFileInput = React.forwardRef(
}, [value]);

return (
<div className={clsx(styles.root)}>
<div {...baseProps} ref={__internalRootRef} className={clsx(baseProps.className, styles.root)}>
{/* This is the actual interactive and accessible file-upload element. */}
{/* It is visually hidden to achieve the desired UX design. */}
<input
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
@use '../../styles/tokens' as awsui;
@use '../../styles' as styles;
@use '../internal/styles/tokens' as awsui;
@use '../internal/styles' as styles;
@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;

.root,
Expand Down
7 changes: 4 additions & 3 deletions src/file-upload/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import { warnOnce } from '@cloudscape-design/component-toolkit/internal';
import InternalBox from '../box/internal';
import { ButtonProps } from '../button/interfaces';
import { useFormFieldContext } from '../contexts/form-field';
import InternalFileInput from '../file-input/internal';
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 @@ -26,7 +26,7 @@ import { Token } from '../token-group/token';
import { FileOption } from './file-option';
import { FileUploadProps } from './interfaces';

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

Expand Down Expand Up @@ -135,7 +135,8 @@ function InternalFileUpload(
multiple={multiple}
onChange={event => handleFilesChange(event.detail.value)}
value={value}
{...restProps}
ariaLabelledby={restProps.ariaLabelledby}
controlId={restProps.controlId}
ariaDescribedby={ariaDescribedBy}
invalid={invalid}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ComponentWrapper, ElementWrapper } from '@cloudscape-design/test-utils-

import ButtonWrapper from '../button';

import selectors from '../../../internal/components/file-input/styles.selectors.js';
import selectors from '../../../file-input/styles.selectors.js';

export default class FileInputWrapper extends ComponentWrapper<HTMLElement> {
static rootSelector: string = selectors.root;
Expand Down
Loading
Loading