diff --git a/packages/react-core/src/components/FileUpload/FileUpload.tsx b/packages/react-core/src/components/FileUpload/FileUpload.tsx index 47afa1a3d04..ce4c8e04f33 100644 --- a/packages/react-core/src/components/FileUpload/FileUpload.tsx +++ b/packages/react-core/src/components/FileUpload/FileUpload.tsx @@ -16,6 +16,10 @@ export interface FileUploadProps 'aria-label'?: string; /** Text for the browse button. */ browseButtonText?: string; + /** ID or ID's of elements that describe the browse button. Typically this should refer + * to elements such as helper text when there are file restrictions. + */ + browseButtonAriaDescribedby?: string; /** Additional children to render after (or instead of) the file preview. */ children?: React.ReactNode; /** Additional classes added to the file upload container element. */ diff --git a/packages/react-core/src/components/FileUpload/FileUploadField.tsx b/packages/react-core/src/components/FileUpload/FileUploadField.tsx index 1ca3cae4c9a..8b215bf6674 100644 --- a/packages/react-core/src/components/FileUpload/FileUploadField.tsx +++ b/packages/react-core/src/components/FileUpload/FileUploadField.tsx @@ -19,6 +19,10 @@ export interface FileUploadFieldProps extends Omit = ({ filenamePlaceholder = 'Drag a file here or browse to upload', filenameAriaLabel = filename ? 'Read only filename' : filenamePlaceholder, browseButtonText = 'Browse...', + browseButtonAriaDescribedby, clearButtonText = 'Clear', isClearButtonDisabled = !filename && !value, containerRef = null as React.Ref, @@ -138,16 +143,15 @@ export const FileUploadField: React.FunctionComponent = ({ name={`${id}-filename`} aria-label={filenameAriaLabel} placeholder={filenamePlaceholder} - aria-describedby={`${id}-browse-button`} value={filename} /> diff --git a/packages/react-core/src/components/FileUpload/FileUploadHelperText.tsx b/packages/react-core/src/components/FileUpload/FileUploadHelperText.tsx new file mode 100644 index 00000000000..9a045fb1071 --- /dev/null +++ b/packages/react-core/src/components/FileUpload/FileUploadHelperText.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import styles from '@patternfly/react-styles/css/components/FileUpload/file-upload'; +import { css } from '@patternfly/react-styles'; + +/** A container for helper text content. This sub-component should be passed as a child to + * the main file upload or file upload field component. + */ + +export interface FileUploadHelperTextProps extends React.HTMLProps { + /** Content to render inside the file upload helper text container. Typically this will be + * the helper text component. + */ + children: React.ReactNode; + /** Additional classes added to the file upload helper text container element. */ + className?: string; +} + +export const FileUploadHelperText: React.FunctionComponent = ({ + children, + className, + ...props +}: FileUploadHelperTextProps) => ( +
+ {children} +
+); +FileUploadHelperText.displayName = 'FileUploadHelperText'; diff --git a/packages/react-core/src/components/FileUpload/__tests__/FileUploadField.test.tsx b/packages/react-core/src/components/FileUpload/__tests__/FileUploadField.test.tsx index 925394a559e..ceb28ef83b9 100644 --- a/packages/react-core/src/components/FileUpload/__tests__/FileUploadField.test.tsx +++ b/packages/react-core/src/components/FileUpload/__tests__/FileUploadField.test.tsx @@ -1,6 +1,6 @@ import { FileUploadField } from '../FileUploadField'; import * as React from 'react'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; test('simple fileuploadfield', () => { const browserBtnClickHandler = jest.fn(); @@ -25,3 +25,19 @@ test('simple fileuploadfield', () => { ); expect(asFragment()).toMatchSnapshot(); }); + +test('Renders without aria-describedby on browse button by default', () => { + render(); + + expect(screen.getByRole('button', { name: 'Upload' })).not.toHaveAccessibleDescription(); +}); + +test('Renders without aria-describedby on browse button by default', () => { + render( + +
Helper text
+
+ ); + + expect(screen.getByRole('button', { name: 'Upload' })).toHaveAccessibleDescription('Helper text'); +}); diff --git a/packages/react-core/src/components/FileUpload/__tests__/FileUploadHelperText.test.tsx b/packages/react-core/src/components/FileUpload/__tests__/FileUploadHelperText.test.tsx new file mode 100644 index 00000000000..566d62bff07 --- /dev/null +++ b/packages/react-core/src/components/FileUpload/__tests__/FileUploadHelperText.test.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { FileUploadHelperText } from '../FileUploadHelperText'; +import { render, screen } from '@testing-library/react'; +import styles from '@patternfly/react-styles/css/components/FileUpload/file-upload'; + +test(`Renders only with class ${styles.fileUpload}__helper-text by default`, () => { + render(Content); + + expect(screen.getByText('Content')).toHaveClass(`${styles.fileUpload}__helper-text`, { exact: true }); +}); + +test(`Renders with custom class when className is passed in`, () => { + render(Content); + + expect(screen.getByText('Content')).toHaveClass('test'); +}); + +test(`Spreads props when passed in`, () => { + render(Content); + + expect(screen.getByText('Content')).toHaveAttribute('id', 'test-id'); +}); + +test('Matches the snapshot', () => { + const { asFragment } = render(Content); + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/components/FileUpload/__tests__/__snapshots__/FileUpload.test.tsx.snap b/packages/react-core/src/components/FileUpload/__tests__/__snapshots__/FileUpload.test.tsx.snap index ce14fa674b2..13cca6c068a 100644 --- a/packages/react-core/src/components/FileUpload/__tests__/__snapshots__/FileUpload.test.tsx.snap +++ b/packages/react-core/src/components/FileUpload/__tests__/__snapshots__/FileUpload.test.tsx.snap @@ -19,7 +19,6 @@ exports[`simple fileupload 1`] = ` class="pf-v5-c-form-control pf-m-readonly" > Browse... diff --git a/packages/react-core/src/components/FileUpload/__tests__/__snapshots__/FileUploadField.test.tsx.snap b/packages/react-core/src/components/FileUpload/__tests__/__snapshots__/FileUploadField.test.tsx.snap index 587e4c18e29..6e237b6edca 100644 --- a/packages/react-core/src/components/FileUpload/__tests__/__snapshots__/FileUploadField.test.tsx.snap +++ b/packages/react-core/src/components/FileUpload/__tests__/__snapshots__/FileUploadField.test.tsx.snap @@ -18,7 +18,6 @@ exports[`simple fileuploadfield 1`] = ` class="pf-v5-c-form-control pf-m-readonly" > Browse... diff --git a/packages/react-core/src/components/FileUpload/__tests__/__snapshots__/FileUploadHelperText.test.tsx.snap b/packages/react-core/src/components/FileUpload/__tests__/__snapshots__/FileUploadHelperText.test.tsx.snap new file mode 100644 index 00000000000..ba2b660c797 --- /dev/null +++ b/packages/react-core/src/components/FileUpload/__tests__/__snapshots__/FileUploadHelperText.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches the snapshot 1`] = ` + +
+ Content +
+
+`; diff --git a/packages/react-core/src/components/FileUpload/examples/FileUpload.md b/packages/react-core/src/components/FileUpload/examples/FileUpload.md index c20a2d18d10..d0c47828308 100644 --- a/packages/react-core/src/components/FileUpload/examples/FileUpload.md +++ b/packages/react-core/src/components/FileUpload/examples/FileUpload.md @@ -7,6 +7,7 @@ subsection: file-upload --- import FileUploadIcon from '@patternfly/react-icons/dist/esm/icons/file-upload-icon'; +import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; ## Examples @@ -22,11 +23,18 @@ Pressing _Clear_ button triggers `onClearClick` event. ```ts file="./FileUploadSimpleText.tsx" ``` -A user can always type instead of selecting a file, but by default, once a user selects a text file from their disk they are not allowed to edit it (to prevent unintended changes to a format-sensitive file). This behavior can be changed with the `allowEditingUploadedText` prop. -Typing/pasting text in the box will call `onTextChange` with a string, and a string value is expected for the `value` prop. : +### With helper text + +You can pass in the ``. + +```ts file="./FileUploadWithHelperText.tsx" +``` ### Text file with edits allowed +A user can always type instead of selecting a file, but by default, once a user selects a text file from their disk they are not allowed to edit it (to prevent unintended changes to a format-sensitive file). This behavior can be changed with the `allowEditingUploadedText` prop. +Typing/pasting text in the box will call `onTextChange` with a string, and a string value is expected for the `value` prop. : + ```ts file="./FileUploadTextWithEdits.tsx" ``` diff --git a/packages/react-core/src/components/FileUpload/examples/FileUploadCustomPreview.tsx b/packages/react-core/src/components/FileUpload/examples/FileUploadCustomPreview.tsx index 33d37b663e5..845ee9df96d 100644 --- a/packages/react-core/src/components/FileUpload/examples/FileUploadCustomPreview.tsx +++ b/packages/react-core/src/components/FileUpload/examples/FileUploadCustomPreview.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { FileUpload } from '@patternfly/react-core'; import FileUploadIcon from '@patternfly/react-icons/dist/esm/icons/file-upload-icon'; -import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing'; export const CustomPreviewFileUpload: React.FunctionComponent = () => { const [value, setValue] = React.useState(); @@ -29,7 +28,7 @@ export const CustomPreviewFileUpload: React.FunctionComponent = () => { browseButtonText="Upload" > {value && ( -
+
Custom preview here for your {value.size}-byte file named{' '} {value.name}
diff --git a/packages/react-core/src/components/FileUpload/examples/FileUploadCustomUpload.tsx b/packages/react-core/src/components/FileUpload/examples/FileUploadCustomUpload.tsx index fbee8fcaff1..274fecd045c 100644 --- a/packages/react-core/src/components/FileUpload/examples/FileUploadCustomUpload.tsx +++ b/packages/react-core/src/components/FileUpload/examples/FileUploadCustomUpload.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { FileUploadField, Checkbox } from '@patternfly/react-core'; -import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing'; +import { FileUploadField, FileUploadHelperText, HelperText, HelperTextItem, Checkbox } from '@patternfly/react-core'; export const CustomPreviewFileUpload: React.FunctionComponent = () => { const properties = [ @@ -9,7 +8,8 @@ export const CustomPreviewFileUpload: React.FunctionComponent = () => { 'isDragActive', 'isLoading', 'hideDefaultPreview', - 'children', + 'hasCustomFilePreview', + 'hasHelperText', 'hasPlaceholderText' ]; @@ -19,7 +19,8 @@ export const CustomPreviewFileUpload: React.FunctionComponent = () => { const [isLoading, setIsLoading] = React.useState(false); const [isDragActive, setIsDragActive] = React.useState(false); const [hideDefaultPreview, setHideDefaultPreview] = React.useState(false); - const [children, setChildren] = React.useState(false); + const [hasCustomFilePreview, setHasCustomFilePreview] = React.useState(false); + const [hasHelperText, setHasHelperText] = React.useState(false); const [hasPlaceholderText, setHasPlaceholderText] = React.useState(false); const [checkedState, setCheckedState] = React.useState([ filename, @@ -27,7 +28,8 @@ export const CustomPreviewFileUpload: React.FunctionComponent = () => { isLoading, isDragActive, hideDefaultPreview, - children, + hasCustomFilePreview, + hasHelperText, hasPlaceholderText ]); @@ -68,8 +70,12 @@ export const CustomPreviewFileUpload: React.FunctionComponent = () => { } break; - case 'children': - checked ? setChildren(true) : setChildren(false); + case 'hasCustomFilePreview': + checked ? setHasCustomFilePreview(true) : setHasCustomFilePreview(false); + break; + + case 'hasHelperText': + checked ? setHasHelperText(true) : setHasHelperText(false); break; case 'hasPlaceholderText': @@ -108,10 +114,16 @@ export const CustomPreviewFileUpload: React.FunctionComponent = () => { isDragActive={isDragActive} hideDefaultPreview={hideDefaultPreview} browseButtonText="Upload" + browseButtonAriaDescribedby={hasHelperText ? 'custom-upload-helpText' : undefined} textAreaPlaceholder={hasPlaceholderText ? 'File preview' : ''} > - {children && ( -
(A custom preview of the uploaded file can be passed as children)
+ {hasCustomFilePreview &&
(A custom preview of the uploaded file can be passed as children)
} + {hasHelperText && ( + + + Upload a CSV file + + )}
diff --git a/packages/react-core/src/components/FileUpload/examples/FileUploadTextWithRestrictions.tsx b/packages/react-core/src/components/FileUpload/examples/FileUploadTextWithRestrictions.tsx index 9679e99d2e1..15858108488 100644 --- a/packages/react-core/src/components/FileUpload/examples/FileUploadTextWithRestrictions.tsx +++ b/packages/react-core/src/components/FileUpload/examples/FileUploadTextWithRestrictions.tsx @@ -1,13 +1,15 @@ import React from 'react'; import { FileUpload, + FileUploadHelperText, Form, FormGroup, - FormHelperText, HelperText, HelperTextItem, - DropEvent + DropEvent, + Icon } from '@patternfly/react-core'; +import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; export const TextFileUploadWithRestrictions: React.FunctionComponent = () => { const [value, setValue] = React.useState(''); @@ -68,14 +70,25 @@ export const TextFileUploadWithRestrictions: React.FunctionComponent = () => { }} validated={isRejected ? 'error' : 'default'} browseButtonText="Upload" - /> - - - - {isRejected ? 'Must be a CSV file no larger than 1 KB' : 'Upload a CSV file'} - - - + browseButtonAriaDescribedby="restricted-file-example-helpText" + > + + + + {isRejected ? ( + <> + + + + Must be a CSV file no larger than 1 KB + + ) : ( + 'Upload a CSV file' + )} + + + + ); diff --git a/packages/react-core/src/components/FileUpload/examples/FileUploadWithHelperText.tsx b/packages/react-core/src/components/FileUpload/examples/FileUploadWithHelperText.tsx new file mode 100644 index 00000000000..9757bc0b7b8 --- /dev/null +++ b/packages/react-core/src/components/FileUpload/examples/FileUploadWithHelperText.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { FileUpload, FileUploadHelperText, HelperText, HelperTextItem, DropEvent } from '@patternfly/react-core'; + +export const FileUploadWithHelperText: React.FunctionComponent = () => { + const [value, setValue] = React.useState(''); + const [filename, setFilename] = React.useState(''); + const [isLoading, setIsLoading] = React.useState(false); + + const handleFileInputChange = (_, file: File) => { + setFilename(file.name); + }; + + const handleTextChange = (_event: React.ChangeEvent, value: string) => { + setValue(value); + }; + + const handleDataChange = (_event: DropEvent, value: string) => { + setValue(value); + }; + + const handleClear = (_event: React.MouseEvent) => { + setFilename(''); + setValue(''); + }; + + const handleFileReadStarted = (_event: DropEvent, _fileHandle: File) => { + setIsLoading(true); + }; + + const handleFileReadFinished = (_event: DropEvent, _fileHandle: File) => { + setIsLoading(false); + }; + + return ( + + + + Upload a CSV file + + + + ); +}; diff --git a/packages/react-core/src/components/FileUpload/index.ts b/packages/react-core/src/components/FileUpload/index.ts index d40c11df5ec..b4b330a8988 100644 --- a/packages/react-core/src/components/FileUpload/index.ts +++ b/packages/react-core/src/components/FileUpload/index.ts @@ -1,2 +1,3 @@ export * from './FileUploadField'; export * from './FileUpload'; +export * from './FileUploadHelperText';