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
23 changes: 22 additions & 1 deletion packages/react-core/src/components/SearchInput/SearchInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export interface SearchInputProps extends Omit<React.HTMLProps<HTMLDivElement>,
innerRef?: React.RefObject<any>;
/** A callback for when the input value changes */
onChange?: (value: string, event: React.FormEvent<HTMLInputElement>) => void;
/** A suggestion for autocompleting */
hint?: string;

/** A callback for when the search button clicked changes */
onSearch?: (
Expand Down Expand Up @@ -83,6 +85,7 @@ const SearchInputBase: React.FunctionComponent<SearchInputProps> = ({
hasWordsAttrLabel = 'Has words',
advancedSearchDelimiter,
placeholder,
hint,
onChange,
onSearch,
onClear,
Expand Down Expand Up @@ -166,6 +169,15 @@ const SearchInputBase: React.FunctionComponent<SearchInputProps> = ({
}
};

const onClearInput = (e: React.SyntheticEvent<HTMLButtonElement>) => {
if (onClear) {
onClear(e);
}
if (searchInputInputRef && searchInputInputRef.current) {
searchInputInputRef.current.focus();
}
};

return (
<div className={css(className, styles.searchInput)} ref={searchInputRef} {...props}>
<InputGroup>
Expand All @@ -174,6 +186,15 @@ const SearchInputBase: React.FunctionComponent<SearchInputProps> = ({
<span className={css(styles.searchInputIcon)}>
<SearchIcon />
</span>
{hint && (
<input
className={css(styles.searchInputTextInput, styles.modifiers.hint)}
type="text"
disabled
aria-hidden="true"
value={hint}
/>
)}
<input
ref={searchInputInputRef}
className={css(styles.searchInputTextInput)}
Expand Down Expand Up @@ -213,7 +234,7 @@ const SearchInputBase: React.FunctionComponent<SearchInputProps> = ({
variant={ButtonVariant.plain}
isDisabled={isDisabled}
aria-label={resetButtonLabel}
onClick={onClear}
onClick={onClearInput}
>
<TimesIcon />
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ test('simple search input', () => {
expect(view.find('input')).toMatchSnapshot();
});

test('search input with hint', () => {
const view = mount(<SearchInput {...props} hint="test hint" 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ exports[`navigable search results 1`] = `
aria-disabled={false}
aria-label="Previous"
className="pf-c-button pf-m-plain"
data-ouia-component-id="OUIA-Generated-Button-plain-7"
data-ouia-component-id="OUIA-Generated-Button-plain-10"
data-ouia-component-type="PF4/Button"
data-ouia-safe={true}
disabled={false}
Expand Down Expand Up @@ -95,7 +95,7 @@ exports[`navigable search results 1`] = `
aria-disabled={false}
aria-label="Next"
className="pf-c-button pf-m-plain"
data-ouia-component-id="OUIA-Generated-Button-plain-8"
data-ouia-component-id="OUIA-Generated-Button-plain-11"
data-ouia-component-type="PF4/Button"
data-ouia-safe={true}
disabled={false}
Expand Down Expand Up @@ -149,6 +149,26 @@ exports[`result count 1`] = `
</span>
`;

exports[`search input with hint 1`] = `
Array [
<input
aria-hidden="true"
className="pf-c-search-input__text-input pf-m-hint"
disabled={true}
type="text"
value="test hint"
/>,
<input
aria-label="simple text input"
className="pf-c-search-input__text-input"
disabled={false}
onChange={[Function]}
onKeyDown={[Function]}
value="test input"
/>,
]
`;

exports[`simple search input 1`] = `
<input
aria-label="simple text input"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,186 @@
id: Search input
section: components
---
import {
Button,
Card,
CardBody,
CardFooter,
DatePicker,
Form,
FormGroup,
Grid,
GridItem,
Menu,
MenuContent,
MenuItem,
MenuList,
MenuToggle,
Popper,
SearchInput,
TextInput
} from '@patternfly/react-core';
import { words } from './words.js';

## Demos

### Search with autocomplete

This demo handles building the advanced search form using the composable Menu, and the `SearchInput`'s `hint` prop.
It also demonstrates wiring up the appropriate keyboard interactions, focus management, and general event handling.


```js
import React from 'react';
import {
Menu,
MenuContent,
MenuItem,
MenuList,
Popper,
SearchInput
} from '@patternfly/react-core';

import { words } from './words.js';

SearchAutocomplete = () => {
const [value, setValue] = React.useState('');
const [hint, setHint] = React.useState('');
const [autocompleteOptions, setAutocompleteOptions] = React.useState([]);

const [isAutocompleteOpen, setIsAutocompleteOpen] = React.useState(false);

const searchInputRef = React.useRef(null);
const autocompleteRef = React.useRef(null);

const onClear = () => {
setValue('');
};

const onChange = (newValue) => {
if (newValue !== '' && searchInputRef && searchInputRef.current && searchInputRef.current.contains(document.activeElement)) {
setIsAutocompleteOpen(true);

// When the value of the search input changes, build a list of no more than 10 autocomplete options.
// Options which start with the search input value are listed first, followed by options which contain
// the search input value.
let options = words.filter((option) => option.startsWith(newValue.toLowerCase())).map((option) => <MenuItem itemId={option} key={option}>{option}</MenuItem>);
if (options.length > 10) {
options = options.slice(0,10);
} else {
options = [...options, ...words.filter((option) => !option.startsWith(newValue.toLowerCase()) && option.includes(newValue.toLowerCase())).map((option) => <MenuItem itemId={option} key={option}>{option}</MenuItem>)].slice(0, 10)
}

// The hint is set whenever there is only one autocomplete option left.
setHint(options.length === 1? options[0].props.itemId : '');
// The menu is hidden if there are no options
setIsAutocompleteOpen(options.length > 0);
setAutocompleteOptions(options);
} else {
setIsAutocompleteOpen(false);
}
setValue(newValue);
};

// Whenever an autocomplete option is selected, set the search input value, close the menu, and put the browser
// focus back on the search input
const onSelect = (e, itemId) => {
e.stopPropagation();
setValue(itemId);
setIsAutocompleteOpen(false);
searchInputRef.current.focus();
};

const handleMenuKeys = event => {
// If there is a hint while the browser focus is on the search input, tab or right arrow will 'accept' the hint value
// and set it as the search input value
if (hint && (event.key === 'Tab' || event.key === 'ArrowRight') && searchInputRef.current === event.target) {
setValue(hint);
setHint('');
setIsAutocompleteOpen(false);
if (event.key === 'ArrowRight') {
event.preventDefault();
}
// if the autocomplete is open and the browser focus is on the search input,
} else if (isAutocompleteOpen && searchInputRef.current && searchInputRef.current === event.target) {
// the escape key closes the autocomplete menu and keeps the focus on the search input.
if (event.key === 'Escape') {
setIsAutocompleteOpen(false);
searchInputRef.current.focus();
// the up and down arrow keys move browser focus into the autocomplete menu
} else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
const firstElement = autocompleteRef.current.querySelector('li > button:not(:disabled)');
firstElement && firstElement.focus();
event.preventDefault(); // by default, the up and down arrow keys scroll the window
// the tab, enter, and space keys will close the menu, and the tab key will move browser
// focus forward one element (by default)
} else if (event.key === 'Tab'|| event.key === "Enter" || event.key === 'Space'){
setIsAutocompleteOpen(false);
if (event.key === "Enter" || event.key === 'Space') {
event.preventDefault();
}
}
// If the autocomplete is open and the browser focus is in the autocomplete menu
// hitting tab will close the autocomplete and but browser focus back on the search input.
} else if (isAutocompleteOpen && autocompleteRef.current.contains(event.target) && (event.key === 'Tab')) {
event.preventDefault();
setIsAutocompleteOpen(false);
searchInputRef.current.focus();
}

};

// The autocomplete menu should close if the user clicks outside the menu.
const handleClickOutside = event => {
if (isAutocompleteOpen && autocompleteRef && autocompleteRef.current && !autocompleteRef.current.contains(event.target)) {
setIsAutocompleteOpen(false);
}
};

React.useEffect(() => {
window.addEventListener('keydown', handleMenuKeys);
window.addEventListener('click', handleClickOutside);
return () => {
window.removeEventListener('keydown', handleMenuKeys);
window.removeEventListener('click', handleClickOutside);
};
}, [isAutocompleteOpen, hint, searchInputRef.current]);

const searchInput = (
<SearchInput
value={value}
onChange={onChange}
onClear={onClear}
ref={searchInputRef}
hint={hint}
id="autocomplete-search"
/>
);

const autocomplete = (
<Menu ref={autocompleteRef} onSelect={onSelect}>
<MenuContent>
<MenuList>
{autocompleteOptions}
</MenuList>
</MenuContent>
</Menu>
);


return (
<Popper
trigger={searchInput}
popper={autocomplete}
isVisible={isAutocompleteOpen}
enableFlip={false}
// append the autocomplete menu to the search input in the DOM for the sake of the keyboard navigation experience
appendTo={() => document.querySelector('#autocomplete-search')}
/>
);
};
```

### Composable advanced search

This demo handles building the advanced search form using the composable Menu, as well as wiring up a
Expand Down
Loading