Skip to content
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
4 changes: 4 additions & 0 deletions packages/react-core/src/components/FileUpload/FileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export interface FileUploadFieldProps extends Omit<React.HTMLProps<HTMLDivElemen
'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 field container element. */
Expand Down Expand Up @@ -105,6 +109,7 @@ export const FileUploadField: React.FunctionComponent<FileUploadFieldProps> = ({
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<HTMLDivElement>,
Expand Down Expand Up @@ -138,16 +143,15 @@ export const FileUploadField: React.FunctionComponent<FileUploadFieldProps> = ({
name={`${id}-filename`}
aria-label={filenameAriaLabel}
placeholder={filenamePlaceholder}
aria-describedby={`${id}-browse-button`}
value={filename}
/>
</InputGroupItem>
<InputGroupItem>
<Button
id={`${id}-browse-button`}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could keep this in, but if we're going to expose an ID on the browse button then we should consider allowing an ID to be passed to all other input elements within the component. For now I included this in the codemod issue patternfly/pf-codemods#551

variant={ButtonVariant.control}
onClick={onBrowseButtonClick}
isDisabled={isDisabled}
aria-describedby={browseButtonAriaDescribedby}
>
{browseButtonText}
</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement> {
/** 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<FileUploadHelperTextProps> = ({
children,
className,
...props
}: FileUploadHelperTextProps) => (
<div className={css(`${styles.fileUpload}__helper-text`, className)} {...props}>
{children}
</div>
);
FileUploadHelperText.displayName = 'FileUploadHelperText';
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -25,3 +25,19 @@ test('simple fileuploadfield', () => {
);
expect(asFragment()).toMatchSnapshot();
});

test('Renders without aria-describedby on browse button by default', () => {
render(<FileUploadField id="file-upload-field" browseButtonText="Upload" />);

expect(screen.getByRole('button', { name: 'Upload' })).not.toHaveAccessibleDescription();
});

test('Renders without aria-describedby on browse button by default', () => {
render(
<FileUploadField id="file-upload-field" browseButtonText="Upload" browseButtonAriaDescribedby="helper-text">
<div id="helper-text">Helper text</div>
</FileUploadField>
);

expect(screen.getByRole('button', { name: 'Upload' })).toHaveAccessibleDescription('Helper text');
});
Original file line number Diff line number Diff line change
@@ -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(<FileUploadHelperText>Content</FileUploadHelperText>);

expect(screen.getByText('Content')).toHaveClass(`${styles.fileUpload}__helper-text`, { exact: true });
});

test(`Renders with custom class when className is passed in`, () => {
render(<FileUploadHelperText className="test">Content</FileUploadHelperText>);

expect(screen.getByText('Content')).toHaveClass('test');
});

test(`Spreads props when passed in`, () => {
render(<FileUploadHelperText id="test-id">Content</FileUploadHelperText>);

expect(screen.getByText('Content')).toHaveAttribute('id', 'test-id');
});

test('Matches the snapshot', () => {
const { asFragment } = render(<FileUploadHelperText>Content</FileUploadHelperText>);
expect(asFragment()).toMatchSnapshot();
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ exports[`simple fileupload 1`] = `
class="pf-v5-c-form-control pf-m-readonly"
>
<input
aria-describedby="simple-text-file-browse-button"
aria-invalid="false"
aria-label="Drag a file here or browse to upload"
data-ouia-component-id="OUIA-Generated-TextInputBase-1"
Expand All @@ -43,7 +42,6 @@ exports[`simple fileupload 1`] = `
data-ouia-component-id="OUIA-Generated-Button-control-1"
data-ouia-component-type="PF5/Button"
data-ouia-safe="true"
id="simple-text-file-browse-button"
type="button"
>
Browse...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ exports[`simple fileuploadfield 1`] = `
class="pf-v5-c-form-control pf-m-readonly"
>
<input
aria-describedby="custom-file-upload-browse-button"
aria-invalid="false"
aria-label="Do something custom with this!"
data-ouia-component-id="OUIA-Generated-TextInputBase-1"
Expand All @@ -42,7 +41,6 @@ exports[`simple fileuploadfield 1`] = `
data-ouia-component-id="OUIA-Generated-Button-control-1"
data-ouia-component-type="PF5/Button"
data-ouia-safe="true"
id="custom-file-upload-browse-button"
type="button"
>
Browse...
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Matches the snapshot 1`] = `
<DocumentFragment>
<div
class="pf-v5-c-file-upload__helper-text"
>
Content
</div>
</DocumentFragment>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 `<FileUploadHelperText` sub-component via the `children` property to `<FileUpload>`.

```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"
```

Expand Down
Original file line number Diff line number Diff line change
@@ -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<File>();
Expand Down Expand Up @@ -29,7 +28,7 @@ export const CustomPreviewFileUpload: React.FunctionComponent = () => {
browseButtonText="Upload"
>
{value && (
<div className={spacing.mMd}>
<div>
<FileUploadIcon width="2em" height="2em" /> Custom preview here for your {value.size}-byte file named{' '}
{value.name}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = [
Expand All @@ -9,7 +8,8 @@ export const CustomPreviewFileUpload: React.FunctionComponent = () => {
'isDragActive',
'isLoading',
'hideDefaultPreview',
'children',
'hasCustomFilePreview',
'hasHelperText',
'hasPlaceholderText'
];

Expand All @@ -19,15 +19,17 @@ 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,
isClearButtonDisabled,
isLoading,
isDragActive,
hideDefaultPreview,
children,
hasCustomFilePreview,
hasHelperText,
hasPlaceholderText
]);

Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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 && (
<div className={spacing.mMd}>(A custom preview of the uploaded file can be passed as children)</div>
{hasCustomFilePreview && <div>(A custom preview of the uploaded file can be passed as children)</div>}
{hasHelperText && (
<FileUploadHelperText>
<HelperText>
<HelperTextItem id="custom-upload-helpText">Upload a CSV file</HelperTextItem>
</HelperText>
</FileUploadHelperText>
)}
</FileUploadField>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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('');
Expand Down Expand Up @@ -68,14 +70,25 @@ export const TextFileUploadWithRestrictions: React.FunctionComponent = () => {
}}
validated={isRejected ? 'error' : 'default'}
browseButtonText="Upload"
/>
<FormHelperText>
<HelperText>
<HelperTextItem variant={isRejected ? 'error' : 'default'}>
{isRejected ? 'Must be a CSV file no larger than 1 KB' : 'Upload a CSV file'}
</HelperTextItem>
</HelperText>
</FormHelperText>
browseButtonAriaDescribedby="restricted-file-example-helpText"
>
<FileUploadHelperText>
<HelperText isLiveRegion>
<HelperTextItem id="restricted-file-example-helpText" variant={isRejected ? 'error' : 'default'}>
{isRejected ? (
<>
<Icon status="danger">
<ExclamationCircleIcon />
</Icon>
Must be a CSV file no larger than 1 KB
</>
) : (
'Upload a CSV file'
)}
</HelperTextItem>
</HelperText>
</FileUploadHelperText>
</FileUpload>
</FormGroup>
</Form>
);
Expand Down
Loading