diff --git a/packages/react-core/src/demos/TextInputGroupDemo.md b/packages/react-core/src/demos/TextInputGroupDemo.md
index b77a5ecced6..f18377f51c4 100644
--- a/packages/react-core/src/demos/TextInputGroupDemo.md
+++ b/packages/react-core/src/demos/TextInputGroupDemo.md
@@ -24,255 +24,17 @@ Additionally, attributes can be selected by typing the full (case sensitive) nam
Attributes can be deselected (returning you to attribute selection mode) by hitting `escape`, or by hitting `backspace` when the only text in the text input is the attribute.
-```js
-import React from 'react';
-import {
- TextInputGroup,
- TextInputGroupMain,
- TextInputGroupUtilities,
- Button,
- Menu,
- MenuContent,
- MenuList,
- MenuItem,
- Popper,
- Chip,
- ChipGroup,
- Divider
-} 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 KeyValueFiltering = () => {
- const [inputValue, setInputValue] = React.useState('');
- const [selectedKey, setSelectedKey] = React.useState('');
- const [menuIsOpen, setMenuIsOpen] = React.useState(false);
- const [currentChips, setCurrentChips] = React.useState([]);
-
- /** key and value data to be shown in the menu */
- const data = {
- Cluster: ['acmeqe-managed-1', 'local-cluster'],
- Kind: ['Template', 'ReplicationController', 'ReplicaSet', 'Deployment'],
- Label: ['release', 'environment', 'partition'],
- Name: ['backup-1', 'backup-2', 'production-1', 'production-2', 'testing'],
- Namespace: ['default', 'public'],
- Status: ['running', 'idle', 'stopped']
- };
- const keyNames = ['Cluster', 'Kind', 'Label', 'Name', 'Namespace', 'Status'];
- const [menuItemsText, setMenuItemsText] = React.useState(keyNames);
- const [menuItems, setMenuItems] = React.useState([]);
-
- /** refs used to detect when clicks occur inside vs outside of the textInputGroup and menu popper */
- const menuRef = React.useRef();
- const textInputGroupRef = React.useRef();
-
- /** callback for updating the inputValue state in this component so that the input can be controlled */
- const handleInputChange = (value, _event) => {
- setInputValue(value);
- };
-
- /** callback for removing a chip from the chip selections */
- const deleteChip = chipToDelete => {
- const newChips = currentChips.filter(chip => !Object.is(chip, chipToDelete));
- setCurrentChips(newChips);
- };
-
- /** reset state hooks associated with key selection */
- const clearSelectedKey = () => {
- setInputValue('');
- setSelectedKey('');
- setMenuItemsText(keyNames);
- };
-
- /** callback for clearing all selected chips, the text input, and any selected keys */
- const clearChipsAndInput = () => {
- setCurrentChips([]);
- clearSelectedKey();
- };
-
- React.useEffect(() => {
- /** in the menu only show items that include the text in the input */
- const filteredMenuItems = menuItemsText
- .filter(
- item =>
- !inputValue ||
- item.toLowerCase().includes(
- inputValue
- .toString()
- .slice(selectedKey.length && selectedKey.length + 2)
- .toLowerCase()
- )
- )
- .map((currentValue, index) => (
-
- ));
-
- /** in the menu show a disabled "no result" when all menu items are filtered out */
- if (filteredMenuItems.length === 0) {
- const noResultItem = (
-
- );
- setMenuItems([noResultItem]);
- return;
- }
-
- /** determine the menu heading text based on key selection; or lack thereof */
- const headingItem = (
-
- );
-
- const divider = ;
-
- setMenuItems([headingItem, divider, ...filteredMenuItems]);
- }, [inputValue]);
-
- /** add selected key/value pair as a chip in the chip group */
- const selectValue = selectedValue => {
- setCurrentChips([...currentChips, `${selectedKey}: ${selectedValue}`]);
- clearSelectedKey();
- };
-
- /** update the input to show the selected key and the menu to show the values associated with that specific key */
- const selectKey = selectedText => {
- setInputValue(`${selectedText}: `);
- setSelectedKey(selectedText);
- setMenuItemsText(data[selectedText]);
- };
-
- const handleEnter = () => {
- /** do nothing if the menu contains no real results */
- if (menuItems.length === 1) {
- return;
- }
-
- /** perform the appropriate action based on key selection state */
- if (selectedKey.length) {
- selectValue(menuItems[2].props.children);
- } else {
- selectKey(menuItems[2].props.children);
- }
- };
-
- /** allow the user to backspace at the selected key name to drop the currently selected key */
- const handleBackspace = () => {
- if (selectedKey.length && inputValue === `${selectedKey}: `) {
- clearSelectedKey();
- }
- };
-
- /** allow the user to select a key by simply typing it and entering a colon, exact (case sensitive) matches only */
- const handleColon = () => {
- if (!selectedKey.length && keyNames.includes(inputValue)) {
- selectKey(inputValue);
- event.preventDefault();
- }
- };
-
- /** allow the user to focus on the menu and navigate using the arrow keys */
- const handleArrowKey = () => {
- if (menuRef.current) {
- const firstElement = menuRef.current.querySelector('li > button:not(:disabled)');
- firstElement && firstElement.focus();
- }
- };
-
- /** enable keyboard only usage */
- const handleTextInputKeyDown = event => {
- switch (event.key) {
- case 'Enter':
- handleEnter();
- break;
- case 'Escape':
- clearSelectedKey();
- break;
- case 'Backspace':
- handleBackspace();
- break;
- case ':':
- handleColon();
- break;
- case 'ArrowUp':
- case 'ArrowDown':
- handleArrowKey();
- break;
- }
- };
-
- /** perform the proper key or value selection when a menu item is selected */
- const onSelect = (event, _itemId) => {
- const selectedText = event.target.innerText;
-
- if (selectedKey.length) {
- selectValue(selectedText);
- } else {
- selectKey(selectedText);
- }
- event.stopPropagation();
- textInputGroupRef.current.querySelector('input').focus();
- };
-
- /** close the menu when a click occurs outside of the menu or text input group */
- const handleClick = event => {
- if (
- menuRef.current &&
- !menuRef.current.contains(event.target) &&
- !textInputGroupRef.current.contains(event.target)
- ) {
- setMenuIsOpen(false);
- }
- };
+```js file="./examples/TextInputGroup/AttributeValueFiltering.js"
+```
+### Auto-complete search
- /** only show the search icon when no chips are selected */
- const showSearchIcon = !currentChips.length;
+This demo showcases a search input with suggestions, which filters possible selections based on the text you've entered. Unlike the attribute-value filtering demo, it allows creation of new chip items when the text entered is not available in the list of suggestions.
- /** only show the clear button when there is something that can be cleared */
- const showClearButton = inputValue || !!currentChips.length;
+The current text in the input can be converted to a chip at any time by hitting `enter`. Auto-complete suggestions can be chosen by clicking the corresponding entry in the menu, or by navigating to an entry using the up/down arrow keys and selecting it with `enter`.
- const inputGroup = (
-
+ );
+
+ return ;
+};
diff --git a/packages/react-core/src/demos/examples/TextInputGroup/AutoCompleteSearch.js b/packages/react-core/src/demos/examples/TextInputGroup/AutoCompleteSearch.js
new file mode 100644
index 00000000000..90226d7b967
--- /dev/null
+++ b/packages/react-core/src/demos/examples/TextInputGroup/AutoCompleteSearch.js
@@ -0,0 +1,230 @@
+import React from 'react';
+import {
+ TextInputGroup,
+ TextInputGroupMain,
+ TextInputGroupUtilities,
+ Button,
+ Menu,
+ MenuContent,
+ MenuList,
+ MenuItem,
+ Popper,
+ Chip,
+ ChipGroup,
+ Divider
+} 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 AutoCompleteSearch = () => {
+ const [inputValue, setInputValue] = React.useState('');
+ const [menuIsOpen, setMenuIsOpen] = React.useState(false);
+ const [currentChips, setCurrentChips] = React.useState([]);
+
+ /** auto-completing suggestion text items to be shown in the menu */
+ const suggestionItems = ['Cluster', 'Kind', 'Label', 'Name', 'Namespace', 'Status'];
+ const [menuItems, setMenuItems] = React.useState([]);
+
+ /** refs used to detect when clicks occur inside vs outside of the textInputGroup and menu popper */
+ const menuRef = React.useRef();
+ const textInputGroupRef = React.useRef();
+
+ /** callback for updating the inputValue state in this component so that the input can be controlled */
+ const handleInputChange = (value, _event) => {
+ setInputValue(value);
+ };
+
+ /** callback for removing a chip from the chip selections */
+ const deleteChip = chipToDelete => {
+ const newChips = currentChips.filter(chip => !Object.is(chip, chipToDelete));
+ setCurrentChips(newChips);
+ };
+
+ /** callback for clearing all selected chips and the text input */
+ const clearChipsAndInput = () => {
+ setCurrentChips([]);
+ setInputValue('');
+ };
+
+ React.useEffect(() => {
+ /** in the menu only show items that include the text in the input */
+ const filteredMenuItems = suggestionItems
+ .filter(item => !inputValue || item.toLowerCase().includes(inputValue.toString().toLowerCase()))
+ .map((currentValue, index) => (
+
+ ));
+
+ /** in the menu show a disabled "no result" when all menu items are filtered out */
+ if (filteredMenuItems.length === 0) {
+ const noResultItem = (
+
+ );
+ setMenuItems([noResultItem]);
+ return;
+ }
+
+ /** add a heading to the menu */
+ const headingItem = (
+
+ );
+
+ const divider = ;
+
+ setMenuItems([headingItem, divider, ...filteredMenuItems]);
+ }, [inputValue]);
+
+ /** add the given string as a chip in the chip group and clear the input */
+ const addChip = newChipText => {
+ setCurrentChips([...currentChips, `${newChipText}`]);
+ setInputValue('');
+ };
+
+ /** add the current input value as a chip */
+ const handleEnter = () => {
+ if (inputValue.length) {
+ addChip(inputValue);
+ }
+ };
+
+ const handleTab = event => {
+ if (menuItems.length === 3) {
+ setInputValue(menuItems[2].props.children);
+ event.preventDefault();
+ }
+ };
+
+ /** close the menu when escape is hit */
+ const handleEscape = () => {
+ setMenuIsOpen(false);
+ };
+
+ /** allow the user to focus on the menu and navigate using the arrow keys */
+ const handleArrowKey = () => {
+ if (menuRef.current) {
+ const firstElement = menuRef.current.querySelector('li > button:not(:disabled)');
+ firstElement && firstElement.focus();
+ }
+ };
+
+ /** reopen the menu if it's closed and any un-designated keys are hit */
+ const handleDefault = () => {
+ if (!menuIsOpen) {
+ setMenuIsOpen(true);
+ }
+ };
+
+ /** enable keyboard only usage while focused on the text input */
+ const handleTextInputKeyDown = event => {
+ switch (event.key) {
+ case 'Enter':
+ handleEnter();
+ break;
+ case 'Escape':
+ handleEscape();
+ break;
+ case 'Tab':
+ handleTab(event);
+ break;
+ case 'ArrowUp':
+ case 'ArrowDown':
+ handleArrowKey();
+ break;
+ default:
+ handleDefault();
+ }
+ };
+
+ /** apply focus to the text input */
+ const focusTextInput = () => {
+ textInputGroupRef.current.querySelector('input').focus();
+ };
+
+ /** add the text of the selected item as a new chip */
+ const onSelect = (event, _itemId) => {
+ const selectedText = event.target.innerText;
+ addChip(selectedText);
+ event.stopPropagation();
+ focusTextInput();
+ };
+
+ /** close the menu when a click occurs outside of the menu or text input group */
+ const handleClick = event => {
+ if (
+ menuRef.current &&
+ !menuRef.current.contains(event.target) &&
+ !textInputGroupRef.current.contains(event.target)
+ ) {
+ setMenuIsOpen(false);
+ }
+ };
+
+ /** enable keyboard only usage while focused on the menu */
+ const handleMenuKeyDown = event => {
+ if (event.key === 'Escape') {
+ setInputValue('');
+ focusTextInput();
+ setMenuIsOpen(false);
+ }
+ };
+
+ /** only show the search icon when no chips are selected */
+ const showSearchIcon = !currentChips.length;
+
+ /** only show the clear button when there is something that can be cleared */
+ const showClearButton = inputValue || !!currentChips.length;
+
+ const inputGroup = (
+