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
92 changes: 92 additions & 0 deletions packages/react-core/src/components/SearchInput/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as React from 'react';
import styles from '@patternfly/react-styles/css/components/SearchInput/search-input';
import { css } from '@patternfly/react-styles';
import { Button, ButtonVariant } from '../Button';
import { Badge } from '../Badge';
import AngleDownIcon from '@patternfly/react-icons/dist/js/icons/angle-down-icon';
import AngleUpIcon from '@patternfly/react-icons/dist/js/icons/angle-up-icon';
import TimesIcon from '@patternfly/react-icons/dist/js/icons/times-icon';
import SearchIcon from '@patternfly/react-icons/dist/js/icons/search-icon';

export interface SearchInputProps extends Omit<React.HTMLProps<HTMLDivElement>, 'onChange' | 'results'> {
/** Additional classes added to the banner */
className?: string;
/** Value of the search input */
value?: string;
/** The number of search results returned. Either a total number of results,
* or a string representing the current result over the total number of results. i.e. "1 / 5" */
resultsCount?: number | string;
/** An accessible label for the search input */
'aria-label'?: string;
/** placeholder text of the search input */
placeholder?: string;
/** A callback for when the input value changes. */
onChange?: (value: string, event: React.FormEvent<HTMLInputElement>) => void;
/** A callback for when the user clicks the clear button */
onClear?: (event: React.SyntheticEvent<HTMLButtonElement>) => void;
/** Function called when user clicks to navigate to next result */
onNextClick?: (event: React.SyntheticEvent<HTMLButtonElement>) => void;
/** Function called when user clicks to navigate to previous result */
onPreviousClick?: (event: React.SyntheticEvent<HTMLButtonElement>) => void;
}

export const SearchInput: React.FunctionComponent<SearchInputProps> = ({
className,
value = '',
placeholder,
onChange,
onClear,
resultsCount,
onNextClick,
onPreviousClick,
'aria-label': ariaLabel = 'Search input',
...props
}: SearchInputProps) => {
const onChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
if (onChange) {
onChange(event.currentTarget.value, event);
}
};

return (
<div className={css(className, styles.searchInput)} {...props}>
<span className={css(styles.searchInputText)}>
<span className={css(styles.searchInputIcon)}>
<SearchIcon />
</span>
<input
className={css(styles.searchInputTextInput)}
value={value}
placeholder={placeholder}
aria-label={ariaLabel}
onChange={onChangeHandler}
/>
</span>
{value && (
<span className={css(styles.searchInputUtilities)}>
{resultsCount && (
<span className={css(styles.searchInputCount)}>
<Badge isRead>{resultsCount}</Badge>
</span>
)}
{!!onNextClick && !!onPreviousClick && (
<span className={css(styles.searchInputNav)}>
<Button variant={ButtonVariant.plain} aria-label="Previous" onClick={onPreviousClick}>
<AngleUpIcon />
</Button>
<Button variant={ButtonVariant.plain} aria-label="Next" onClick={onNextClick}>
<AngleDownIcon />
</Button>
</span>
)}
<span className="pf-c-search-input__clear">
<Button variant={ButtonVariant.plain} aria-label="Clear" onClick={onClear}>
<TimesIcon />
</Button>
</span>
</span>
)}
</div>
);
};
SearchInput.displayName = 'SearchInput';
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
import { SearchInput } from '../SearchInput';


const props = {
onChange: jest.fn(),
value: 'test input',
onNextClick: jest.fn(),
onPreviousClick: jest.fn(),
onClear: jest.fn()
};

test('input passes value and event to onChange handler', () => {
const newValue = 'new test input';
const event = {
currentTarget: { value: newValue }
};
const view = shallow(<SearchInput {...props} aria-label="test input" />);

view.find('input').simulate('change', event);
expect(props.onChange).toBeCalledWith(newValue, event);
});

test('simple search input', () => {
const view = mount(<SearchInput {...props} aria-label="simple text input" />);
expect(view.find('input')).toMatchSnapshot();
});

test('result count', () => {
const view = mount(<SearchInput {...props} resultsCount={3} aria-label="simple text input" />);
expect(view.find('.pf-c-badge')).toMatchSnapshot();
});

test('navigable search results', () => {
const view = mount(
<SearchInput
{...props}
resultsCount='3 / 7'
aria-label="simple text input"
/>);
expect(view.find('.pf-c-search-input__nav')).toMatchSnapshot();
expect(view.find('.pf-c-badge')).toMatchSnapshot();

view.find('.pf-c-button').at(0).simulate('click', {});
expect(props.onPreviousClick).toBeCalled();
view.find('.pf-c-button').at(1).simulate('click', {});
expect(props.onNextClick).toBeCalled();
view.find('.pf-c-button').at(2).simulate('click', {});
expect(props.onClear).toBeCalled();
});



Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`navigable search results 1`] = `
<span
className="pf-c-search-input__nav"
>
<Button
aria-label="Previous"
onClick={[MockFunction]}
variant="plain"
>
<button
aria-disabled={false}
aria-label="Previous"
className="pf-c-button pf-m-plain"
data-ouia-component-id={6}
data-ouia-component-type="PF4/Button"
data-ouia-safe={true}
disabled={false}
onClick={[MockFunction]}
type="button"
>
<AngleUpIcon
color="currentColor"
noVerticalAlign={false}
size="sm"
>
<svg
aria-hidden={true}
aria-labelledby={null}
fill="currentColor"
height="1em"
role="img"
style={
Object {
"verticalAlign": "-0.125em",
}
}
viewBox="0 0 320 512"
width="1em"
>
<path
d="M177 159.7l136 136c9.4 9.4 9.4 24.6 0 33.9l-22.6 22.6c-9.4 9.4-24.6 9.4-33.9 0L160 255.9l-96.4 96.4c-9.4 9.4-24.6 9.4-33.9 0L7 329.7c-9.4-9.4-9.4-24.6 0-33.9l136-136c9.4-9.5 24.6-9.5 34-.1z"
transform=""
/>
</svg>
</AngleUpIcon>
</button>
</Button>
<Button
aria-label="Next"
onClick={[MockFunction]}
variant="plain"
>
<button
aria-disabled={false}
aria-label="Next"
className="pf-c-button pf-m-plain"
data-ouia-component-id={7}
data-ouia-component-type="PF4/Button"
data-ouia-safe={true}
disabled={false}
onClick={[MockFunction]}
type="button"
>
<AngleDownIcon
color="currentColor"
noVerticalAlign={false}
size="sm"
>
<svg
aria-hidden={true}
aria-labelledby={null}
fill="currentColor"
height="1em"
role="img"
style={
Object {
"verticalAlign": "-0.125em",
}
}
viewBox="0 0 320 512"
width="1em"
>
<path
d="M143 352.3L7 216.3c-9.4-9.4-9.4-24.6 0-33.9l22.6-22.6c9.4-9.4 24.6-9.4 33.9 0l96.4 96.4 96.4-96.4c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9l-136 136c-9.2 9.4-24.4 9.4-33.8 0z"
transform=""
/>
</svg>
</AngleDownIcon>
</button>
</Button>
</span>
`;

exports[`navigable search results 2`] = `
<span
className="pf-c-badge pf-m-read"
>
3 / 7
</span>
`;

exports[`result count 1`] = `
<span
className="pf-c-badge pf-m-read"
>
3
</span>
`;

exports[`simple search input 1`] = `
<input
aria-label="simple text input"
className="pf-c-search-input__text-input"
onChange={[Function]}
value="test input"
/>
`;
Loading