diff --git a/packages/react-core/src/components/TextInputGroup/TextInputGroup.tsx b/packages/react-core/src/components/TextInputGroup/TextInputGroup.tsx index 1df27886894..13fab93e0fc 100644 --- a/packages/react-core/src/components/TextInputGroup/TextInputGroup.tsx +++ b/packages/react-core/src/components/TextInputGroup/TextInputGroup.tsx @@ -11,6 +11,8 @@ export interface TextInputGroupProps extends React.HTMLProps { 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; } @@ -24,6 +26,7 @@ export const TextInputGroup: React.FunctionComponent = ({ className, isDisabled, isPlain, + validated, innerRef, ...props }: TextInputGroupProps) => { @@ -31,13 +34,14 @@ export const TextInputGroup: React.FunctionComponent = ({ const textInputGroupRef = innerRef || ref; return ( - +
{ + /** 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 = ({ + children, + className, + isStatus, + ...props +}: TextInputGroupIconProps) => ( + + {children} + +); + +TextInputGroupIcon.displayName = 'TextInputGroupIcon'; diff --git a/packages/react-core/src/components/TextInputGroup/TextInputGroupMain.tsx b/packages/react-core/src/components/TextInputGroup/TextInputGroupMain.tsx index 268dab37d7e..cc7542ea4a5 100644 --- a/packages/react-core/src/components/TextInputGroup/TextInputGroupMain.tsx +++ b/packages/react-core/src/components/TextInputGroup/TextInputGroupMain.tsx @@ -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, 'onChange'> { /** Content rendered inside the text input group main div */ @@ -78,9 +80,10 @@ const TextInputGroupMainBase: React.FunctionComponent = 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) => { onChange(event, event.currentTarget.value); @@ -100,7 +103,7 @@ const TextInputGroupMainBase: React.FunctionComponent = id={inputId} /> )} - {icon && {icon}} + {icon && {icon}} = {...(isExpanded !== undefined && { 'aria-expanded': isExpanded })} {...(ariaControls && { 'aria-controls': ariaControls })} /> + {validated && {}
); diff --git a/packages/react-core/src/components/TextInputGroup/__tests__/TextInputGroup.test.tsx b/packages/react-core/src/components/TextInputGroup/__tests__/TextInputGroup.test.tsx index 3f9ba1fe0ed..f4c06d3e72c 100644 --- a/packages/react-core/src/components/TextInputGroup/__tests__/TextInputGroup.test.tsx +++ b/packages/react-core/src/components/TextInputGroup/__tests__/TextInputGroup.test.tsx @@ -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(Test); 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(Test); 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(Test); + + const inputGroup = screen.getByText('Test'); + + expect(inputGroup).toHaveClass(styles.modifiers.success); + }); + it(`renders with class ${styles.modifiers.warning} when validated="warning"`, () => { + render(Test); + + const inputGroup = screen.getByText('Test'); + + expect(inputGroup).toHaveClass(styles.modifiers.warning); + }); + it(`renders with class ${styles.modifiers.error} when validated="error"`, () => { + render(Test); + + 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', () => { diff --git a/packages/react-core/src/components/TextInputGroup/__tests__/TextInputGroupMain.test.tsx b/packages/react-core/src/components/TextInputGroup/__tests__/TextInputGroupMain.test.tsx index 0cfa3e0299c..34747a047b0 100644 --- a/packages/react-core/src/components/TextInputGroup/__tests__/TextInputGroupMain.test.tsx +++ b/packages/react-core/src/components/TextInputGroup/__tests__/TextInputGroupMain.test.tsx @@ -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( + + + + ); + + 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(); diff --git a/packages/react-core/src/components/TextInputGroup/examples/TextInputGroup.md b/packages/react-core/src/components/TextInputGroup/examples/TextInputGroup.md index 0dbf2094410..3d3cc5ebced 100644 --- a/packages/react-core/src/components/TextInputGroup/examples/TextInputGroup.md +++ b/packages/react-core/src/components/TextInputGroup/examples/TextInputGroup.md @@ -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 `` by passing the `validated` property with a value of either "success", "warning", or "error". + +```ts file="./TextInputGroupWithStatus.tsx" + ``` ### Filters ```ts file="./TextInputGroupFilters.tsx" + ``` diff --git a/packages/react-core/src/components/TextInputGroup/examples/TextInputGroupWithStatus.tsx b/packages/react-core/src/components/TextInputGroup/examples/TextInputGroupWithStatus.tsx new file mode 100644 index 00000000000..9e8ee8c1a17 --- /dev/null +++ b/packages/react-core/src/components/TextInputGroup/examples/TextInputGroupWithStatus.tsx @@ -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 ( + + + + setSuccessValue(value)} /> + + + + + } + value={warningValue} + onChange={(_event, value) => setWarningValue(value)} + /> + + + + + } + value={errorValue} + onChange={(_event, value) => setErrorValue(value)} + /> + {showUtilities && ( + + {showClearButton && ( +