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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export interface TextInputGroupProps extends React.HTMLProps<HTMLDivElement> {
isDisabled?: boolean;
/** Flag to indicate the toggle has no border or background */
isPlain?: boolean;
/** Status variant of the text input group. */
validated?: 'success' | 'warning' | 'error';
/** @hide A reference object to attach to the input box */
innerRef?: React.RefObject<any>;
}
Expand All @@ -24,20 +26,22 @@ export const TextInputGroup: React.FunctionComponent<TextInputGroupProps> = ({
className,
isDisabled,
isPlain,
validated,
innerRef,
...props
}: TextInputGroupProps) => {
const ref = React.useRef(null);
const textInputGroupRef = innerRef || ref;

return (
<TextInputGroupContext.Provider value={{ isDisabled }}>
<TextInputGroupContext.Provider value={{ isDisabled, validated }}>
<div
ref={textInputGroupRef}
className={css(
styles.textInputGroup,
isDisabled && styles.modifiers.disabled,
isPlain && styles.modifiers.plain,
validated && styles.modifiers[validated],
className
)}
{...props}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from 'react';
import styles from '@patternfly/react-styles/css/components/TextInputGroup/text-input-group';
import { css } from '@patternfly/react-styles';

export interface TextInputGroupIconProps extends React.HTMLProps<HTMLSpanElement> {
/** Content rendered inside the text input group utilities div */
children?: React.ReactNode;
/** Additional classes applied to the text input group utilities container */
className?: string;
/** Flag indicating if the icon is a status icon and should inherit status styling. */
isStatus?: boolean;
}

export const TextInputGroupIcon: React.FunctionComponent<TextInputGroupIconProps> = ({
children,
className,
isStatus,
...props
}: TextInputGroupIconProps) => (
<span className={css(styles.textInputGroupIcon, isStatus && styles.modifiers.status, className)} {...props}>
{children}
</span>
);

TextInputGroupIcon.displayName = 'TextInputGroupIcon';
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as React from 'react';
import styles from '@patternfly/react-styles/css/components/TextInputGroup/text-input-group';
import { css } from '@patternfly/react-styles';
import { TextInputGroupContext } from './TextInputGroup';
import { TextInputGroupIcon } from './TextInputGroupIcon';
import { statusIcons, ValidatedOptions } from '../../helpers';

export interface TextInputGroupMainProps extends Omit<React.HTMLProps<HTMLDivElement>, 'onChange'> {
/** Content rendered inside the text input group main div */
Expand Down Expand Up @@ -78,9 +80,10 @@ const TextInputGroupMainBase: React.FunctionComponent<TextInputGroupMainProps> =
inputId,
...props
}: TextInputGroupMainProps) => {
const { isDisabled } = React.useContext(TextInputGroupContext);
const { isDisabled, validated } = React.useContext(TextInputGroupContext);
const ref = React.useRef(null);
const textInputGroupInputInputRef = innerRef || ref;
const StatusIcon = statusIcons[validated === ValidatedOptions.error ? 'danger' : validated];

const handleChange = (event: React.FormEvent<HTMLInputElement>) => {
onChange(event, event.currentTarget.value);
Expand All @@ -100,7 +103,7 @@ const TextInputGroupMainBase: React.FunctionComponent<TextInputGroupMainProps> =
id={inputId}
/>
)}
{icon && <span className={css(styles.textInputGroupIcon)}>{icon}</span>}
{icon && <TextInputGroupIcon>{icon}</TextInputGroupIcon>}
<input
ref={textInputGroupInputInputRef}
type={type}
Expand All @@ -119,6 +122,7 @@ const TextInputGroupMainBase: React.FunctionComponent<TextInputGroupMainProps> =
{...(isExpanded !== undefined && { 'aria-expanded': isExpanded })}
{...(ariaControls && { 'aria-controls': ariaControls })}
/>
{validated && <TextInputGroupIcon isStatus>{<StatusIcon aria-hidden="true" />}</TextInputGroupIcon>}
</span>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,42 @@ describe('TextInputGroup', () => {
expect(inputGroup).toHaveClass('custom-class');
});

it('does not render with the pf-m-disabled class when not disabled', () => {
it(`does not render with the ${styles.modifiers.disabled} class when not disabled`, () => {
render(<TextInputGroup>Test</TextInputGroup>);

const inputGroup = screen.getByText('Test');

expect(inputGroup).not.toHaveClass('pf-m-disabled');
expect(inputGroup).not.toHaveClass(styles.modifiers.disabled);
});

it('renders with the pf-m-disabled class when disabled', () => {
it(`renders with the ${styles.modifiers.disabled} class when disabled`, () => {
render(<TextInputGroup isDisabled>Test</TextInputGroup>);

const inputGroup = screen.getByText('Test');

expect(inputGroup).toHaveClass('pf-m-disabled');
expect(inputGroup).toHaveClass(styles.modifiers.disabled);
});

it(`renders with class ${styles.modifiers.success} when validated="success"`, () => {
render(<TextInputGroup validated="success">Test</TextInputGroup>);

const inputGroup = screen.getByText('Test');

expect(inputGroup).toHaveClass(styles.modifiers.success);
});
it(`renders with class ${styles.modifiers.warning} when validated="warning"`, () => {
render(<TextInputGroup validated="warning">Test</TextInputGroup>);

const inputGroup = screen.getByText('Test');

expect(inputGroup).toHaveClass(styles.modifiers.warning);
});
it(`renders with class ${styles.modifiers.error} when validated="error"`, () => {
render(<TextInputGroup validated="error">Test</TextInputGroup>);

const inputGroup = screen.getByText('Test');

expect(inputGroup).toHaveClass(styles.modifiers.error);
});

it('passes isDisabled=false to children via a context when isDisabled prop is not passed', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,16 @@ describe('TextInputGroupMain', () => {
expect(hintInput).toBeVisible();
});

it(`Renders status icon with class ${styles.modifiers.status} when a validated prop is passed`, () => {
render(
<TextInputGroupContext.Provider value={{ validated: 'success' }}>
<TextInputGroupMain />
</TextInputGroupContext.Provider>
);

expect(screen.getByRole('textbox').nextElementSibling).toHaveClass(styles.modifiers.status);
});

it('does not call onChange callback when the input does not change', () => {
const onChangeMock = jest.fn();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,31 @@ import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
### Basic

```ts file="./TextInputGroupBasic.tsx"

```

### Disabled

```ts file="./TextInputGroupDisabled.tsx"

```

### Utilities and icon

```ts file="./TextInputGroupUtilitiesAndIcon.tsx"

```

### With validation

You can add a validation status to a `<TextInputGroup>` by passing the `validated` property with a value of either "success", "warning", or "error".

```ts file="./TextInputGroupWithStatus.tsx"

```

### Filters

```ts file="./TextInputGroupFilters.tsx"

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react';
import {
TextInputGroup,
TextInputGroupMain,
TextInputGroupUtilities,
Button,
ValidatedOptions,
Flex,
FlexItem
} from '@patternfly/react-core';
import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';
import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';

export const TextInputGroupWithStatus: React.FunctionComponent = () => {
const [successValue, setSuccessValue] = React.useState('Success validation');
const [warningValue, setWarningValue] = React.useState('Warning validation with custom non-status icon at start');
const [errorValue, setErrorValue] = React.useState(
'Error validation with custom non-status icon at start and utilities'
);

/** show the input clearing button only when the input is not empty */
const showClearButton = !!errorValue;

/** render the utilities component only when a component it contains is being rendered */
const showUtilities = showClearButton;

/** callback for clearing the text input */
const clearInput = () => {
setErrorValue('');
};

return (
<Flex direction={{ default: 'column' }} rowGap={{ default: 'rowGapSm' }}>
<FlexItem>
<TextInputGroup validated={ValidatedOptions.success}>
<TextInputGroupMain value={successValue} onChange={(_event, value) => setSuccessValue(value)} />
</TextInputGroup>
</FlexItem>
<FlexItem>
<TextInputGroup validated={ValidatedOptions.warning}>
<TextInputGroupMain
icon={<SearchIcon />}
value={warningValue}
onChange={(_event, value) => setWarningValue(value)}
/>
</TextInputGroup>
</FlexItem>
<FlexItem>
<TextInputGroup validated={ValidatedOptions.error}>
<TextInputGroupMain
icon={<SearchIcon />}
value={errorValue}
onChange={(_event, value) => setErrorValue(value)}
/>
{showUtilities && (
<TextInputGroupUtilities>
{showClearButton && (
<Button variant="plain" onClick={clearInput} aria-label="Clear button and input" icon={<TimesIcon />} />
)}
</TextInputGroupUtilities>
)}
</TextInputGroup>
</FlexItem>
</Flex>
);
};