Skip to content

Commit 54a37e7

Browse files
committed
feat: Adds disabled state to file input
1 parent 7fbd0fb commit 54a37e7

File tree

6 files changed

+135
-1
lines changed

6 files changed

+135
-1
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React from 'react';
4+
5+
import FileInput, { FileInputProps } from '~components/file-input';
6+
7+
import createPermutations from '../utils/permutations';
8+
import PermutationsView from '../utils/permutations-view';
9+
import ScreenshotArea from '../utils/screenshot-area';
10+
11+
const permutations = createPermutations<FileInputProps>([
12+
{
13+
value: [[]],
14+
onChange: [() => {}],
15+
variant: ['icon', 'button'],
16+
children: ['Upload file'],
17+
disabled: [false, true],
18+
},
19+
{
20+
value: [[]],
21+
onChange: [() => {}],
22+
variant: ['icon', 'button'],
23+
children: ['Upload file'],
24+
disabled: [true],
25+
disabledReason: ["You don't have access to upload files."],
26+
},
27+
]);
28+
29+
export default function ExpandableSectionPermutations() {
30+
return (
31+
<>
32+
<h1>File input permutations</h1>
33+
<ScreenshotArea disableAnimations={true}>
34+
<PermutationsView permutations={permutations} render={permutation => <FileInput {...permutation} />} />
35+
</ScreenshotArea>
36+
</>
37+
);
38+
}

src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8870,6 +8870,19 @@ is provided by its parent form field component.",
88708870
"optional": true,
88718871
"type": "string",
88728872
},
8873+
{
8874+
"description": "Renders the file input as disabled and file selection.",
8875+
"name": "disabled",
8876+
"optional": true,
8877+
"type": "boolean",
8878+
},
8879+
{
8880+
"description": "Provides a reason why the file input is disabled (only when \`disabled\` is \`true\`).
8881+
If provided, the file input becomes focusable.",
8882+
"name": "disabledReason",
8883+
"optional": true,
8884+
"type": "string",
8885+
},
88738886
{
88748887
"deprecatedTag": "The usage of the \`id\` attribute is reserved for internal use cases. For testing and other use cases,
88758888
use [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). If you must
@@ -8969,6 +8982,13 @@ Make sure that you add a listener to this event to update your application state
89698982
"optional": true,
89708983
"type": "string",
89718984
},
8985+
{
8986+
"description": "Specifies if the control is disabled.
8987+
This prevents the user from modifying the value. A disabled control is not focusable.",
8988+
"name": "disabled",
8989+
"optional": true,
8990+
"type": "boolean",
8991+
},
89728992
{
89738993
"description": "An object containing all the localized strings required by the component:
89748994
* \`removeFileAriaLabel\` (function): A function to render the ARIA label for file token remove button.

src/file-input/__tests__/file-input.test.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,21 @@ describe('FileInput input', () => {
133133
expect((onChange as jest.Mock).mock.lastCall[0].detail.value[0]).toBe(file1);
134134
expect((onChange as jest.Mock).mock.lastCall[0].detail.value[1]).toBe(file2);
135135
});
136+
137+
test('disables the native input and the trigger button when `disabled` prop is assigned', () => {
138+
const wrapper = render({ disabled: true });
139+
const input = wrapper.findNativeInput().getElement();
140+
141+
expect(input).toBeDisabled();
142+
expect(wrapper.findTrigger().isDisabled()).toBe(true);
143+
});
144+
145+
test('shows the disabled upon focus when `disabledReason` prop is assigned', () => {
146+
const wrapper = render({ disabled: true, disabledReason: 'Test disabled reason' });
147+
wrapper.findTrigger().focus();
148+
149+
expect(wrapper.findTrigger().findDisabledReason()!.getElement()).toHaveTextContent('Test disabled reason');
150+
});
136151
});
137152

138153
describe('ref', () => {
@@ -172,4 +187,42 @@ describe('a11y', () => {
172187
});
173188
await expect(wrapper.getElement()).toValidateA11y();
174189
});
190+
191+
describe('when disabled', () => {
192+
test('decorative button cannot catch the focus', () => {
193+
const wrapper = render({ disabled: true });
194+
195+
// Asserting against attributes rather than checking the actual focusing behavior.
196+
// Reason: JSDom ignores `disabled` and `tabIndex` attributes when focusing on buttons.
197+
// Ref: https://github.com/jsdom/jsdom/issues/3029
198+
expect(wrapper.findTrigger().getElement()).toHaveAttribute('disabled');
199+
expect(wrapper.findTrigger().getElement()).toHaveAttribute('tabIndex', '-1');
200+
});
201+
202+
test('native input cannot catch the focus', () => {
203+
const wrapper = render({ disabled: true });
204+
wrapper.findNativeInput().focus();
205+
expect(wrapper.findNativeInput().getElement()).not.toHaveFocus();
206+
});
207+
});
208+
209+
describe('when disabled with disabledReason', () => {
210+
test('decorative button can catch the focus', () => {
211+
const wrapper = render({ disabled: true, disabledReason: 'Test disabled reason' });
212+
wrapper.findTrigger().focus();
213+
expect(wrapper.findTrigger().getElement()).toHaveFocus();
214+
215+
// Asserting against attributes rather than checking the actual focusing behavior.
216+
// Reason: JSDom ignores `disabled` and `tabIndex` attributes when focusing on buttons.
217+
// Ref: https://github.com/jsdom/jsdom/issues/3029
218+
expect(wrapper.findTrigger().getElement()).not.toHaveAttribute('disabled');
219+
expect(wrapper.findTrigger().getElement()).not.toHaveAttribute('tabIndex');
220+
});
221+
222+
test('native input cannot catch the focus', () => {
223+
const wrapper = render({ disabled: true, disabledReason: 'Test disabled reason' });
224+
wrapper.findNativeInput().focus();
225+
expect(wrapper.findNativeInput().getElement()).not.toHaveFocus();
226+
});
227+
});
175228
});

src/file-input/interfaces.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ export interface FileInputProps extends BaseComponentProps, FormFieldCommonValid
4848
* If you want to clear the selection, use empty array.
4949
*/
5050
value: ReadonlyArray<File>;
51+
52+
/**
53+
* Renders the file input as disabled and file selection.
54+
*/
55+
disabled?: boolean;
56+
57+
/**
58+
* Provides a reason why the file input is disabled (only when `disabled` is `true`).
59+
* If provided, the file input becomes focusable.
60+
*/
61+
disabledReason?: string;
5162
}
5263

5364
export namespace FileInputProps {

src/file-input/internal.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ const InternalFileInput = React.forwardRef(
4242
onChange,
4343
variant = 'button',
4444
children,
45+
disabled,
46+
disabledReason,
4547
__internalRootRef = null,
4648
__inputClassName,
4749
__inputNativeAttributes,
@@ -76,10 +78,12 @@ const InternalFileInput = React.forwardRef(
7678

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

81+
const shouldDecorativeButtonGetFocus = disabled && Boolean(disabledReason);
7982
const nativeAttributes: React.HTMLAttributes<HTMLInputElement> = {
8083
'aria-label': ariaLabel || children,
8184
'aria-labelledby': joinStrings(formFieldContext.ariaLabelledby, uploadButtonLabelId),
8285
'aria-describedby': formFieldContext.ariaDescribedby,
86+
'aria-hidden': shouldDecorativeButtonGetFocus,
8387
...__inputNativeAttributes,
8488
};
8589
if (formFieldContext.invalid) {
@@ -137,6 +141,7 @@ const InternalFileInput = React.forwardRef(
137141
hidden={false}
138142
multiple={multiple}
139143
accept={accept}
144+
disabled={disabled}
140145
onChange={onUploadInputChange}
141146
onFocus={onUploadInputFocus}
142147
onBlur={onUploadInputBlur}
@@ -151,12 +156,14 @@ const InternalFileInput = React.forwardRef(
151156
iconName="upload"
152157
variant={variant === 'icon' ? 'icon' : undefined}
153158
formAction="none"
159+
disabled={disabled}
160+
disabledReason={disabledReason}
154161
onClick={onUploadButtonClick}
155162
className={clsx(styles['file-input-button'], {
156163
[styles['force-focus-outline-button']]: isFocused && variant === 'button',
157164
[styles['force-focus-outline-icon']]: isFocused && variant === 'icon',
158165
})}
159-
__nativeAttributes={{ tabIndex: -1, 'aria-hidden': true }}
166+
__nativeAttributes={shouldDecorativeButtonGetFocus ? { tabIndex } : { tabIndex: -1, 'aria-hidden': true }}
160167
>
161168
{variant === 'button' && children}
162169
</InternalButton>

src/file-token-group/interfaces.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ export interface FileTokenGroupProps extends BaseComponentProps {
5858
* user from modifying the value. A read-only control is still focusable.
5959
*/
6060
readOnly?: boolean;
61+
/**
62+
* Specifies if the control is disabled.
63+
* This prevents the user from modifying the value. A disabled control is not focusable.
64+
*/
65+
disabled?: boolean;
6166
/**
6267
* An object containing all the localized strings required by the component:
6368
* * `removeFileAriaLabel` (function): A function to render the ARIA label for file token remove button.

0 commit comments

Comments
 (0)