Skip to content

Commit

Permalink
Updated examples and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
thatblindgeye committed Jan 24, 2024
1 parent b198f0b commit cffb89e
Show file tree
Hide file tree
Showing 10 changed files with 93 additions and 74 deletions.
37 changes: 25 additions & 12 deletions packages/react-core/src/components/HelperText/HelperTextItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,39 @@ import ExclamationTriangleIcon from '@patternfly/react-icons/dist/esm/icons/excl
import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon';
import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';

export enum HelperTextItemVariant {
default = 'default',
warning = 'warning',
error = 'error',
success = 'success'
}

export interface HelperTextItemProps extends React.HTMLProps<HTMLDivElement | HTMLLIElement> {
/** Content rendered inside the helper text item. */
children?: React.ReactNode;
/** Additional classes applied to the helper text item. */
className?: string;
/** Sets the component type of the helper text item. */
component?: 'div' | 'li';
/** Status styling of the helper text item. Will also render a default icon, which can be overridden
/** Variant styling of the helper text item. Will also render a default icon, which can be overridden
* with the icon prop.
*/
status?: 'indeterminate' | 'warning' | 'success' | 'error';
/** Custom icon prefixing the helper text. This property will override the default icon when the status property is passed in. */
variant?: 'default' | 'indeterminate' | 'warning' | 'success' | 'error';
/** Custom icon prefixing the helper text. This property will override the default icon when the variant property is passed in. */
icon?: React.ReactNode;
/** ID for the helper text item. The value of this prop can be passed into a form component's
* aria-describedby prop when you intend for only specific helper text items to be announced to
* assistive technologies.
*/
id?: string;
/** Text that is only accessible to screen readers in order to announce the status of a helper text item.
* This prop can only be used when the status prop is also passed in.
/** Text that is only accessible to screen readers in order to announce the variant of a helper text item.
* This prop can only be used when the variant prop has a value other than "default".
*/
screenReaderText?: string;
}

const defaultStatusIcons = {
const defaultVariantIcons = {
default: <></>,
indeterminate: <MinusIcon />,
warning: <ExclamationTriangleIcon />,
success: <CheckCircleIcon />,
Expand All @@ -41,24 +49,29 @@ export const HelperTextItem: React.FunctionComponent<HelperTextItemProps> = ({
children,
className,
component = 'div',
status,
variant = 'default',
icon,
id,
screenReaderText = `${status} status`,
screenReaderText = `${variant} status`,
...props
}: HelperTextItemProps) => {
const Component = component as any;
const isNotDefaultVariant = variant !== 'default';
return (
<Component className={css(styles.helperTextItem, styles.modifiers[status], className)} id={id} {...props}>
{(status || icon) && (
<Component
className={css(styles.helperTextItem, isNotDefaultVariant && styles.modifiers[variant], className)}
id={id}
{...props}
>
{(isNotDefaultVariant || icon) && (
<span className={css(styles.helperTextItemIcon)} aria-hidden>
{icon || defaultStatusIcons[status]}
{icon || defaultVariantIcons[variant]}
</span>
)}

<span className={css(styles.helperTextItemText)}>
{children}
{status && <span className="pf-v5-screen-reader">: {screenReaderText};</span>}
{isNotDefaultVariant && <span className="pf-v5-screen-reader">: {screenReaderText};</span>}
</span>
</Component>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ test('Renders custom className', () => {
expect(screen.getByText('help test text 1').parentElement).toHaveClass('custom');
});

test('Does not render screen reader text by default', () => {
render(<HelperTextItem>help test text 1</HelperTextItem>);

expect(screen.queryByText('help test text 1')?.querySelector('.pf-v5-screen-reader')).not.toBeInTheDocument();
});

Object.values(['indeterminate', 'warning', 'success', 'error']).forEach((variant) => {
test(`Renders with class ${styles.modifiers[variant]} when variant = ${variant}`, () => {
render(
Expand All @@ -39,6 +45,15 @@ Object.values(['indeterminate', 'warning', 'success', 'error']).forEach((variant
);
expect(screen.getByText('text').parentElement).toHaveClass(styles.modifiers[variant]);
});

test(`Renders default screenreader text when variant = ${variant}`, () => {
render(
<HelperTextItem variant={variant as 'default' | 'indeterminate' | 'warning' | 'success' | 'error'}>
text
</HelperTextItem>
);
expect(screen.getByText('text').querySelector('span')).toHaveTextContent(`: ${variant} status;`);
});
});

test('Renders id when id is passed', () => {
Expand All @@ -56,38 +71,26 @@ test('Renders with element passed to component prop', () => {
expect(screen.getByText('help test text 1').parentElement?.tagName).toBe('LI');
});

test('Renders custom icon', () => {
render(<HelperTextItem icon={<div>test</div>}>help test text</HelperTextItem>);
expect(screen.getByText('test').parentElement).toHaveClass(styles.helperTextItemIcon);
});

test('Renders default icon when hasIcon = true and icon is not passed', () => {
render(<HelperTextItem hasIcon>help test text</HelperTextItem>);
expect(screen.getByText('help test text').parentElement?.querySelector('span')).toHaveClass(
styles.helperTextItemIcon
);
test('Does not render an icon by default', () => {
render(<HelperTextItem>help test text</HelperTextItem>);
expect(screen.queryByText('help test text')?.previousSibling).not.toBeInTheDocument();
});

test('Renders custom icon when icon is passed and hasIcon = true', () => {
render(
<HelperTextItem hasIcon icon={<div>test</div>}>
help test text
</HelperTextItem>
);
expect(screen.getByText('test').parentElement).toHaveClass(styles.helperTextItemIcon);
test('Renders a default icon when variant is passed and icon is not passed', () => {
render(<HelperTextItem variant="success">help test text</HelperTextItem>);
expect(screen.getByText('help test text').previousSibling).toHaveClass(styles.helperTextItemIcon);
});

test('Renders dynamic helper text', () => {
render(<HelperTextItem isDynamic>help test text</HelperTextItem>);
expect(screen.getByText('help test text').parentElement).toHaveClass(styles.modifiers.dynamic);
expect(screen.getByText('help test text').querySelector('span')).toHaveClass('pf-v5-screen-reader');
test('Renders custom icon when icon prop is passed', () => {
render(<HelperTextItem icon={<div>icon content</div>}>help test text</HelperTextItem>);
expect(screen.getByText('icon content').parentElement).toHaveClass(styles.helperTextItemIcon);
});

test('Renders custom screenreader text when isDynamic = true and screenReaderText is passed', () => {
test('Renders custom icon instead of variant icon when icon and variant are passed', () => {
render(
<HelperTextItem isDynamic screenReaderText="sr test">
<HelperTextItem icon={<div>icon content</div>} variant="success">
help test text
</HelperTextItem>
);
expect(screen.getByText('help test text').querySelector('span')).toHaveTextContent('sr test');
expect(screen.getByText('icon content').parentElement).toHaveClass(styles.helperTextItemIcon);
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ export const HelperTextBasic: React.FunctionComponent = () => (
<HelperTextItem>This is default helper text</HelperTextItem>
</HelperText>
<HelperText>
<HelperTextItem status="indeterminate">This is indeterminate helper text</HelperTextItem>
<HelperTextItem variant="indeterminate">This is indeterminate helper text</HelperTextItem>
</HelperText>
<HelperText>
<HelperTextItem status="warning">This is warning helper text</HelperTextItem>
<HelperTextItem variant="warning">This is warning helper text</HelperTextItem>
</HelperText>
<HelperText>
<HelperTextItem status="success">This is success helper text</HelperTextItem>
<HelperTextItem variant="success">This is success helper text</HelperTextItem>
</HelperText>
<HelperText>
<HelperTextItem status="error">This is error helper text</HelperTextItem>
<HelperTextItem variant="error">This is error helper text</HelperTextItem>
</HelperText>
</React.Fragment>
);
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,22 @@ export const HelperTextWithCustomIcon: React.FunctionComponent = () => (
<HelperTextItem icon={<InfoIcon />}>This is default helper text</HelperTextItem>
</HelperText>
<HelperText>
<HelperTextItem status="indeterminate" icon={<QuestionIcon />}>
<HelperTextItem variant="indeterminate" icon={<QuestionIcon />}>
This is indeterminate helper text
</HelperTextItem>
</HelperText>
<HelperText>
<HelperTextItem status="warning" icon={<ExclamationIcon />}>
<HelperTextItem variant="warning" icon={<ExclamationIcon />}>
This is warning helper text
</HelperTextItem>
</HelperText>
<HelperText>
<HelperTextItem status="success" icon={<CheckIcon />}>
<HelperTextItem variant="success" icon={<CheckIcon />}>
This is success helper text
</HelperTextItem>
</HelperText>
<HelperText>
<HelperTextItem status="error" icon={<TimesIcon />}>
<HelperTextItem variant="error" icon={<TimesIcon />}>
This is error helper text
</HelperTextItem>
</HelperText>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const MultipleFileUploadBasic: React.FunctionComponent = () => {
if (fileUploadShouldFail) {
const corruptedFiles = files.map((file) => ({ ...file, lastModified: 'foo' as unknown as number }));
// eslint-disable-next-line
setCurrentFiles((prevFiles) => [...prevFiles, ...corruptedFiles as any]);
setCurrentFiles((prevFiles) => [...prevFiles, ...(corruptedFiles as any)]);
} else {
setCurrentFiles((prevFiles) => [...prevFiles, ...files]);
}
Expand Down Expand Up @@ -101,7 +101,7 @@ export const MultipleFileUploadBasic: React.FunctionComponent = () => {
if (fileResult?.loadError) {
return (
<HelperText isLiveRegion>
<HelperTextItem variant={'error'}>{fileResult.loadError.toString()}</HelperTextItem>
<HelperTextItem variant="error">{fileResult.loadError.toString()}</HelperTextItem>
</HelperText>
);
}
Expand Down
25 changes: 14 additions & 11 deletions packages/react-core/src/demos/HelperText.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,40 @@ import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';

## Demos

### Static variant with static text
### Static helper text

In this demo, the static variant of the helper text item component (the default) is used, and the text itself will always be visible to users and will never change.
In this demo the text content of the helper text item will always be visible to users and will never change.

The `aria-describedby` attribute is passed into the text input component and is linked to the `id` of the helper text component. This allows assistive technologies to notify users of the helper text content when the input receives focus, which can be helpful if a user navigates away from and then back to the input.

Note that this demo does not validate the text input component. When it would need to be validated, there are other steps that would need to be taken to make it accessible, such as passing in `aria-invalid` and `aria-live` attributes to the appropriate components.

```ts file='./examples/HelperText/HelperTextStaticVariantStaticText.tsx'
```ts file='./examples/HelperText/HelperTextStatic.tsx'

```

### Static variant with dynamic text
### Dynamic helper text

In this demo, the static variant of the helper text item component (the default) is used with the `hasIcon` prop passed in when there is an error, and the text itself dynamically updates based on the input value. When the input has a value of `johndoe`, an error is rendered to simulate a username already being taken, while an empty input renders other helper text. When the input is valid, no helper text is rendered.
In this demo the text content of the helper text item dynamically updates based on the input value. When the input has a value of `johndoe`, an error is rendered to simulate a username already being taken, while an empty input renders default text. When the input is valid, no helper text is rendered.

The `aria-describedby` attribute is passed into the text input component and is linked to the `id` of the helper text component. Similar to the static variant with static text demo, this allows assistive technologies to notify users of the helper text content when the navigating to the input.

An `aria-live` region is passed into the helper text component, which allows assistive technologies to announce to users when any dynamic content within it updates, such as when the text content changes or gets rendered. Without this attribute, a user would have to navigate out of and back into the input field multiple times to check the status of their input.

The `aria-invalid` attribute is also passed into the text input, which allows assistive technologies to notify users that an input is invalid. When this attribute is true, it's important that users are notified of what is causing the input to be invalid; in this case, `aria-describedby` and `aria-live` help accomplish this.

```ts file='./examples/HelperText/HelperTextStaticVariantDynamicText.tsx'
```ts file='./examples/HelperText/HelperTextDynamic.tsx'

```

### Dynamic variant with static text
### Static text and dynamic status

In this demo the text content of the helper text item remains static and never changes, but the icons and styling will change as the validation of the input changes.

In this demo, the helper text item components have the `isDynamic` prop passed in. While the text content of the components is static, the icons and styling will change as the validation of the input changes.
The `aria-describedby` attribute is passed into the text input component and is linked to the `id` attribute of a helper text item that results in an invalid input. This will allow assistive technologies to only be notified of any outstanding criteria that has not been met when the input receives focus.

The `aria-describedby` attribute is passed into the text input component and is linked to the id attribute of a helper text item that results in an invalid input. This will allow assistive technologies to only be notified of any outstanding criteria that has not been met when the input receives focus.
Similar to the [with dynamic text example](/components/helper-text/react-demos#with-dynamic-text), the `aria-invalid` attribute is passed in, allowing assistive technologies to announce to users when at least 1 item is causing the input to be invalid.

Similar to the static variant with dynamic text example, the `aria-invalid` attribute is passed in, allowing assistive technologies to announce to users when at least 1 item is causing the input to be invalid.
```ts file='./examples/HelperText/HelperTextStaticTextDynamicVariant.tsx'

```ts file='./examples/HelperText/HelperTextDynamicVariantStaticText.tsx'
```
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { Form, FormGroup, FormHelperText, TextInput, HelperText, HelperTextItem } from '@patternfly/react-core';

export const HelperTextStaticVariantDynamicText: React.FunctionComponent = () => {
export const HelperTextDynamic: React.FunctionComponent = () => {
const [value, setValue] = React.useState('');
const [inputValidation, setInputValidation] = React.useState('default');

Expand Down Expand Up @@ -35,7 +35,7 @@ export const HelperTextStaticVariantDynamicText: React.FunctionComponent = () =>
<FormHelperText>
<HelperText id="helper-text2" aria-live="polite">
{inputValidation !== 'success' && (
<HelperTextItem variant={inputValidation as any} hasIcon={inputValidation === 'error'}>
<HelperTextItem variant={inputValidation as any}>
{inputValidation === 'default' ? 'Please enter a username' : 'Username already exists'}
</HelperTextItem>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { Form, FormGroup, FormHelperText, TextInput, HelperText, HelperTextItem } from '@patternfly/react-core';

export const HelperTextStaticVariantStaticText: React.FunctionComponent = () => {
export const HelperTextStaticText: React.FunctionComponent = () => {
const [value, setValue] = React.useState('');

const handleInputChange = (_event, inputValue: string) => {
Expand All @@ -21,7 +21,7 @@ export const HelperTextStaticVariantStaticText: React.FunctionComponent = () =>
/>
<FormHelperText>
<HelperText id="helper-text1">
<HelperTextItem variant={'default'}>Enter your middle name or your middle initial</HelperTextItem>
<HelperTextItem>Enter your middle name or your middle initial</HelperTextItem>
</HelperText>
</FormHelperText>
</FormGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { Form, FormGroup, FormHelperText, TextInput, HelperText, HelperTextItem } from '@patternfly/react-core';

export const HelperTextDynamicVariantDynamicText: React.FunctionComponent = () => {
export const HelperTextStaticTextDynamicVariant: React.FunctionComponent = () => {
const [value, setValue] = React.useState('');
const [inputValidation, setInputValidation] = React.useState({
ruleLength: 'indeterminate',
Expand All @@ -10,8 +10,8 @@ export const HelperTextDynamicVariantDynamicText: React.FunctionComponent = () =
const { ruleLength, ruleCharacterTypes } = inputValidation;

React.useEffect(() => {
let lengthStatus = ruleLength;
let typeStatus = ruleCharacterTypes;
let lengthVariant = ruleLength;
let typeVariant = ruleCharacterTypes;

if (value === '') {
setInputValidation({
Expand All @@ -22,18 +22,18 @@ export const HelperTextDynamicVariantDynamicText: React.FunctionComponent = () =
}

if (!/\d/g.test(value)) {
typeStatus = 'error';
typeVariant = 'error';
} else {
typeStatus = 'success';
typeVariant = 'success';
}

if (value.length < 5) {
lengthStatus = 'error';
lengthVariant = 'error';
} else {
lengthStatus = 'success';
lengthVariant = 'success';
}

setInputValidation({ ruleLength: lengthStatus, ruleCharacterTypes: typeStatus });
setInputValidation({ ruleLength: lengthVariant, ruleCharacterTypes: typeVariant });
}, [value, ruleLength, ruleCharacterTypes]);

const handleInputChange = (_event, inputValue: string) => {
Expand All @@ -57,10 +57,10 @@ export const HelperTextDynamicVariantDynamicText: React.FunctionComponent = () =
/>
<FormHelperText>
<HelperText component="ul">
<HelperTextItem component="li" id="ruleLength" status={ruleLength as any}>
<HelperTextItem component="li" id="ruleLength" variant={ruleLength as any}>
Must be at least 5 characters in length
</HelperTextItem>
<HelperTextItem component="li" id="ruleCharacterTypes" status={ruleCharacterTypes as any}>
<HelperTextItem component="li" id="ruleCharacterTypes" variant={ruleCharacterTypes as any}>
Must include at least 1 number
</HelperTextItem>
</HelperText>
Expand Down
Loading

0 comments on commit cffb89e

Please sign in to comment.