Skip to content
Draft
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/permutations.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 from 'react';

import FileInput, { FileInputProps } from '~components/file-input';

import createPermutations from '../utils/permutations';
import PermutationsView from '../utils/permutations-view';
import ScreenshotArea from '../utils/screenshot-area';

const permutations = createPermutations<FileInputProps>([
{
value: [[]],
onChange: [() => {}],
ariaLabel: ['prompt file input'],
variant: ['icon', 'button'],
children: ['Upload file'],
disabled: [false, true],
},
{
value: [[]],
onChange: [() => {}],
ariaLabel: ['prompt file input'],
variant: ['icon', 'button'],
children: ['Upload file'],
disabled: [true],
disabledReason: ["You don't have access to upload files."],
},
]);

export default function ExpandableSectionPermutations() {
return (
<>
<h1>File input permutations</h1>
<ScreenshotArea disableAnimations={true}>
<PermutationsView permutations={permutations} render={permutation => <FileInput {...permutation} />} />
</ScreenshotArea>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8870,6 +8870,19 @@ is provided by its parent form field component.",
"optional": true,
"type": "string",
},
{
"description": "Renders the file input as disabled and file selection.",
"name": "disabled",
"optional": true,
"type": "boolean",
},
{
"description": "Provides a reason why the file input is disabled (only when \`disabled\` is \`true\`).
If provided, the file input becomes focusable.",
"name": "disabledReason",
"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
Expand Down Expand Up @@ -8969,6 +8982,13 @@ Make sure that you add a listener to this event to update your application state
"optional": true,
"type": "string",
},
{
"description": "Specifies if the control is disabled.
This prevents the user from modifying the value. A disabled control is not focusable.",
"name": "disabled",
"optional": true,
"type": "boolean",
},
{
"description": "An object containing all the localized strings required by the component:
* \`removeFileAriaLabel\` (function): A function to render the ARIA label for file token remove button.
Expand Down
47 changes: 47 additions & 0 deletions src/file-input/__tests__/file-input.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,21 @@ describe('FileInput input', () => {
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);
});

test('disables the native input and the trigger button when `disabled` prop is assigned', () => {
const wrapper = render({ disabled: true });
const input = wrapper.findNativeInput().getElement();

expect(input).toBeDisabled();
expect(wrapper.findTrigger().isDisabled()).toBe(true);
});

test('shows the disabled upon focus when `disabledReason` prop is assigned', () => {
const wrapper = render({ disabled: true, disabledReason: 'Test disabled reason' });
wrapper.findTrigger().focus();

expect(wrapper.findTrigger().findDisabledReason()!.getElement()).toHaveTextContent('Test disabled reason');
});
});

describe('ref', () => {
Expand Down Expand Up @@ -172,4 +187,36 @@ describe('a11y', () => {
});
await expect(wrapper.getElement()).toValidateA11y();
});

describe('when disabled', () => {
test('decorative button is not focusable by keyboard', () => {
const wrapper = render({ disabled: true });

expect(wrapper.findTrigger().getElement()).toHaveAttribute('disabled');
expect(wrapper.findTrigger().getElement()).toHaveAttribute('tabIndex', '-1');
});

test('native input is not focusable', () => {
const wrapper = render({ disabled: true });
wrapper.findNativeInput().focus();
expect(wrapper.findNativeInput().getElement()).not.toHaveFocus();
});
});

describe('when disabled with disabledReason', () => {
test('decorative button is focusable by keyboard', () => {
const wrapper = render({ disabled: true, disabledReason: 'Test disabled reason' });
wrapper.findTrigger().focus();
expect(wrapper.findTrigger().getElement()).toHaveFocus();

expect(wrapper.findTrigger().getElement()).not.toHaveAttribute('disabled');
expect(wrapper.findTrigger().getElement()).not.toHaveAttribute('tabIndex');
});

test('native input is not focusable', () => {
const wrapper = render({ disabled: true, disabledReason: 'Test disabled reason' });
wrapper.findNativeInput().focus();
expect(wrapper.findNativeInput().getElement()).not.toHaveFocus();
});
});
});
11 changes: 11 additions & 0 deletions src/file-input/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ export interface FileInputProps extends BaseComponentProps, FormFieldCommonValid
* If you want to clear the selection, use empty array.
*/
value: ReadonlyArray<File>;

/**
* Renders the file input as disabled and file selection.
*/
disabled?: boolean;

/**
* Provides a reason why the file input is disabled (only when `disabled` is `true`).
* If provided, the file input becomes focusable.
*/
disabledReason?: string;
}

export namespace FileInputProps {
Expand Down
10 changes: 9 additions & 1 deletion src/file-input/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ const InternalFileInput = React.forwardRef(
onChange,
variant = 'button',
children,
disabled,
disabledReason,
__internalRootRef = null,
__inputClassName,
__inputNativeAttributes,
Expand Down Expand Up @@ -76,10 +78,12 @@ const InternalFileInput = React.forwardRef(

checkControlled('FileInput', 'value', value, 'onChange', onChange);

const shouldDecorativeButtonGetFocus = disabled && Boolean(disabledReason);
const nativeAttributes: React.HTMLAttributes<HTMLInputElement> = {
'aria-label': ariaLabel || children,
'aria-labelledby': joinStrings(formFieldContext.ariaLabelledby, uploadButtonLabelId),
'aria-describedby': formFieldContext.ariaDescribedby,
'aria-hidden': shouldDecorativeButtonGetFocus,
...__inputNativeAttributes,
};
if (formFieldContext.invalid) {
Expand Down Expand Up @@ -137,6 +141,7 @@ const InternalFileInput = React.forwardRef(
hidden={false}
multiple={multiple}
accept={accept}
disabled={disabled}
onChange={onUploadInputChange}
onFocus={onUploadInputFocus}
onBlur={onUploadInputBlur}
Expand All @@ -151,12 +156,15 @@ const InternalFileInput = React.forwardRef(
iconName="upload"
variant={variant === 'icon' ? 'icon' : undefined}
formAction="none"
disabled={disabled}
ariaLabel={ariaLabel}
disabledReason={disabledReason}
onClick={onUploadButtonClick}
className={clsx(styles['file-input-button'], {
[styles['force-focus-outline-button']]: isFocused && variant === 'button',
[styles['force-focus-outline-icon']]: isFocused && variant === 'icon',
})}
__nativeAttributes={{ tabIndex: -1, 'aria-hidden': true }}
__nativeAttributes={shouldDecorativeButtonGetFocus ? { tabIndex } : { tabIndex: -1, 'aria-hidden': true }}
>
{variant === 'button' && children}
</InternalButton>
Expand Down
5 changes: 5 additions & 0 deletions src/file-token-group/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ export interface FileTokenGroupProps extends BaseComponentProps {
* user from modifying the value. A read-only control is still focusable.
*/
readOnly?: boolean;
/**
* Specifies if the control is disabled.
* This prevents the user from modifying the value. A disabled control is not focusable.
*/
disabled?: boolean;
/**
* An object containing all the localized strings required by the component:
* * `removeFileAriaLabel` (function): A function to render the ARIA label for file token remove button.
Expand Down