diff --git a/packages/react-core/src/demos/SearchInput/SearchInput.md b/packages/react-core/src/demos/SearchInput/SearchInput.md
index d00bdd63349..3a1fd98cc2e 100644
--- a/packages/react-core/src/demos/SearchInput/SearchInput.md
+++ b/packages/react-core/src/demos/SearchInput/SearchInput.md
@@ -31,172 +31,8 @@ import { words } from './words.js';
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 { useEffect, useRef, useState } from 'react';
-import { Menu, MenuContent, MenuItem, MenuList, Popper, SearchInput } from '@patternfly/react-core';
-
-import { words } from './words.js';
-
-SearchAutocomplete = () => {
- const [value, setValue] = useState('');
- const [hint, setHint] = useState('');
- const [autocompleteOptions, setAutocompleteOptions] = useState([]);
-
- const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false);
-
- const searchInputRef = useRef(null);
- const autocompleteRef = useRef(null);
-
- const onClear = () => {
- setValue('');
- };
-
- const onChange = (_event, 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) => (
-
- ));
- 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) => (
-
- ))
- ].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);
- };
+```ts file="examples/SearchInputAutocomplete.tsx"
- // 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 === ' ') {
- setIsAutocompleteOpen(false);
- if (event.key === 'Enter' || event.key === ' ') {
- 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);
- }
- };
-
- useEffect(() => {
- window.addEventListener('keydown', handleMenuKeys);
- window.addEventListener('click', handleClickOutside);
- return () => {
- window.removeEventListener('keydown', handleMenuKeys);
- window.removeEventListener('click', handleClickOutside);
- };
- }, [isAutocompleteOpen, hint, searchInputRef.current]);
-
- const searchInput = (
-
- );
-
- const autocomplete = (
-
- );
-
- return (
- document.querySelector('#autocomplete-search')}
- />
- );
-};
```
### Composable advanced search
@@ -207,327 +43,6 @@ keyboard interactions, focus management, and general event handling.
Note: This demo and its handling of 'date within' and a date picker is modeled after the gmail advanced search form.
-```js
-import { useEffect, useRef, useState } from 'react';
-import {
- ActionGroup,
- Button,
- DatePicker,
- Form,
- FormGroup,
- Grid,
- GridItem,
- isValidDate,
- Menu,
- MenuContent,
- MenuItem,
- MenuList,
- MenuToggle,
- Panel,
- PanelMain,
- PanelMainBody,
- Popper,
- SearchInput,
- TextInput,
- yyyyMMddFormat
-} from '@patternfly/react-core';
-
-AdvancedComposableSearchInput = () => {
- const [value, setValue] = useState('');
- const [hasWords, setHasWords] = useState('');
- const [dateWithin, setDateWithin] = useState('1 day');
- const [date, setDate] = useState();
-
- const [isAdvancedSearchOpen, setIsAdvancedSearchOpen] = useState(false);
- const [isDateWithinOpen, setIsDateWithinOpen] = useState(false);
-
- const isInitialMount = useRef(true);
- const firstAttrRef = useRef(null);
- const searchInputRef = useRef(null);
- const advancedSearchPaneRef = useRef(null);
- const dateWithinToggleRef = useRef(undefined);
- const dateWithinMenuRef = useRef(undefined);
-
- const onClear = () => {
- setValue('');
- setHasWords('');
- setDateWithin('');
- setDate('');
- };
-
- const onChange = (_event, value) => {
- if (value.length <= hasWords.length + 1) {
- setValue(value);
- setHasWords(value);
- } else {
- setValue(hasWords);
- }
- };
-
- // After initial page load, whenever the advanced search menu is opened, the browser focus should be placed on the
- // first advanced search form input. Whenever the advanced search menu is closed, the browser focus should
- // be returned to the search input.
- useEffect(() => {
- if (isInitialMount.current) {
- isInitialMount.current = false;
- } else {
- if (isAdvancedSearchOpen && firstAttrRef && firstAttrRef.current) {
- firstAttrRef.current.focus();
- } else if (!isAdvancedSearchOpen && searchInputRef) {
- searchInputRef.current.focus();
- }
- }
- }, [isAdvancedSearchOpen]);
-
- // If a menu is open and has browser focus, then the escape key closes them and puts the browser focus onto their
- // respective toggle. The 'date within' menu also needs to close when the 'tab' key is hit. However, hitting tab while
- // focus is in the advanced search form should move the focus to the next form input, not close the advanced search
- // menu.
- const handleMenuKeys = (event) => {
- if (isDateWithinOpen && dateWithinMenuRef.current && dateWithinMenuRef.current.contains(event.target)) {
- if (event.key === 'Escape' || event.key === 'Tab') {
- setIsDateWithinOpen(!isDateWithinOpen);
- dateWithinToggleRef.current.focus();
- }
- }
- if (isAdvancedSearchOpen && advancedSearchPaneRef.current && advancedSearchPaneRef.current.contains(event.target)) {
- if (
- event.key === 'Escape' ||
- (event.key === 'Tab' &&
- !event.shiftKey &&
- advancedSearchPaneRef.current.querySelector('button[type=reset]') === event.target)
- ) {
- setIsAdvancedSearchOpen(!isAdvancedSearchOpen);
- searchInputRef.current.focus();
- }
- }
- };
-
- // If a menu is open and has browser focus, then clicking outside the menu should close it.
- const handleClickOutside = (event) => {
- if (
- isDateWithinOpen &&
- dateWithinMenuRef &&
- dateWithinMenuRef.current &&
- !dateWithinMenuRef.current.contains(event.target)
- ) {
- setIsDateWithinOpen(false);
- }
- if (
- isAdvancedSearchOpen &&
- advancedSearchPaneRef &&
- advancedSearchPaneRef.current &&
- !advancedSearchPaneRef.current.contains(event.target)
- ) {
- setIsAdvancedSearchOpen(false);
- }
- };
-
- useEffect(() => {
- window.addEventListener('keydown', handleMenuKeys);
- window.addEventListener('click', handleClickOutside);
- return () => {
- window.removeEventListener('keydown', handleMenuKeys);
- window.removeEventListener('click', handleClickOutside);
- };
- }, [dateWithinMenuRef.current, advancedSearchPaneRef.current, isAdvancedSearchOpen, isDateWithinOpen]);
-
- // This demo and its handling of 'date within' and a date picker is modeled after the gmail advanced search form.
- const onSubmit = (event, value) => {
- event.preventDefault();
-
- if (isValidDate(new Date(date)) && dateWithin) {
- let afterDate = new Date(date);
- let toDate = new Date(date);
- switch (dateWithin) {
- case '1 day':
- afterDate.setDate(afterDate.getDate());
- toDate.setDate(toDate.getDate() + 2);
- break;
- case '3 days':
- afterDate.setDate(afterDate.getDate() - 2);
- toDate.setDate(toDate.getDate() + 4);
- break;
- case '1 week':
- afterDate.setDate(afterDate.getDate() - 6);
- toDate.setDate(toDate.getDate() + 8);
- break;
- case '2 weeks':
- afterDate.setDate(afterDate.getDate() - 13);
- toDate.setDate(toDate.getDate() + 15);
- break;
- case '1 month':
- afterDate.setMonth(afterDate.getMonth() - 1);
- afterDate.setDate(afterDate.getDate() + 1);
- toDate.setMonth(toDate.getMonth() + 1);
- toDate.setDate(toDate.getDate() + 1);
- break;
- case '2 months':
- afterDate.setMonth(afterDate.getMonth() - 2);
- afterDate.setDate(afterDate.getDate() + 1);
- toDate.setMonth(toDate.getMonth() + 2);
- toDate.setDate(toDate.getDate() + 1);
- break;
- case '6 months':
- afterDate.setMonth(afterDate.getMonth() - 6);
- afterDate.setDate(afterDate.getDate() + 1);
- toDate.setMonth(toDate.getMonth() + 6);
- toDate.setDate(toDate.getDate() + 1);
- break;
- case '1 year':
- afterDate.setFullYear(afterDate.getFullYear() - 1);
- afterDate.setDate(afterDate.getDate() + 1);
- toDate.setFullYear(toDate.getFullYear() + 1);
- toDate.setDate(toDate.getDate() + 1);
- break;
- }
- setValue(`${hasWords && hasWords + ' '}after:${yyyyMMddFormat(afterDate)} to:${yyyyMMddFormat(toDate)}`);
- } else {
- setValue(hasWords);
- }
-
- setIsAdvancedSearchOpen(false);
- };
-
- const searchInput = (
- {
- e.stopPropagation();
- setIsAdvancedSearchOpen(isOpen);
- }}
- isAdvancedSearchOpen={isAdvancedSearchOpen}
- onClear={onClear}
- onSearch={onSubmit}
- ref={searchInputRef}
- id="custom-advanced-search"
- aria-label='Composable advanced search'
- />
- );
-
- // Clicking the 'date within' toggle should open its associated menu and then place the browser
- // focus on the first menu item.
- const toggleDateWithinMenu = (ev) => {
- ev.stopPropagation(); // Stop handleClickOutside from handling
- setTimeout(() => {
- if (dateWithinMenuRef.current) {
- const firstElement = dateWithinMenuRef.current.querySelector('li > button:not(:disabled)');
- firstElement && firstElement.focus();
- }
- }, 0);
- setIsDateWithinOpen(!isDateWithinOpen);
- };
-
- // Selecting a date within option closes the menu, sets the value of date within, and puts browser focus back
- // on the date within toggle.
- const onDateWithinSelect = (e, itemId) => {
- e.stopPropagation();
- setIsDateWithinOpen(false);
- setDateWithin(itemId);
- if (dateWithinToggleRef && dateWithinToggleRef.current) {
- dateWithinToggleRef.current.focus();
- }
- };
-
- const dateWithinOptions = (
-
- );
-
- const dateWithinToggle = (
-
- {dateWithin}
-
- );
-
- const advancedForm = (
-
-
-
-
-
-
-
-
-
- );
+```ts file="examples/SearchInputAdvancedComposable.tsx"
- // Popper is just one way to build a relationship between a toggle and a menu.
- return (
- document.querySelector('#custom-advanced-search')}
- />
- );
-};
```
diff --git a/packages/react-core/src/demos/SearchInput/examples/SearchInputAdvancedComposable.tsx b/packages/react-core/src/demos/SearchInput/examples/SearchInputAdvancedComposable.tsx
new file mode 100644
index 00000000000..6c784d6e873
--- /dev/null
+++ b/packages/react-core/src/demos/SearchInput/examples/SearchInputAdvancedComposable.tsx
@@ -0,0 +1,322 @@
+import { useEffect, useRef, useState } from 'react';
+import {
+ ActionGroup,
+ Button,
+ DatePicker,
+ Form,
+ FormGroup,
+ Grid,
+ GridItem,
+ isValidDate,
+ Menu,
+ MenuContent,
+ MenuItem,
+ MenuList,
+ MenuToggle,
+ Panel,
+ PanelMain,
+ PanelMainBody,
+ Popper,
+ SearchInput,
+ TextInput,
+ yyyyMMddFormat
+} from '@patternfly/react-core';
+
+export const SearchInputAdvancedComposable: React.FunctionComponent = () => {
+ const [value, setValue] = useState('');
+ const [hasWords, setHasWords] = useState('');
+ const [dateWithin, setDateWithin] = useState('1 day');
+ const [date, setDate] = useState();
+
+ const [isAdvancedSearchOpen, setIsAdvancedSearchOpen] = useState(false);
+ const [isDateWithinOpen, setIsDateWithinOpen] = useState(false);
+
+ const isInitialMount = useRef(true);
+ const firstAttrRef = useRef(null);
+ const searchInputRef = useRef(null);
+ const advancedSearchPaneRef = useRef(null);
+ const dateWithinToggleRef = useRef(undefined);
+ const dateWithinMenuRef = useRef(undefined);
+
+ const onClear = () => {
+ setValue('');
+ setHasWords('');
+ setDateWithin('');
+ setDate('');
+ };
+
+ const onChange = (_event, value) => {
+ if (value.length <= hasWords.length + 1) {
+ setValue(value);
+ setHasWords(value);
+ } else {
+ setValue(hasWords);
+ }
+ };
+
+ // After initial page load, whenever the advanced search menu is opened, the browser focus should be placed on the
+ // first advanced search form input. Whenever the advanced search menu is closed, the browser focus should
+ // be returned to the search input.
+ useEffect(() => {
+ if (isInitialMount.current) {
+ isInitialMount.current = false;
+ } else {
+ if (isAdvancedSearchOpen && firstAttrRef && firstAttrRef.current) {
+ firstAttrRef.current.focus();
+ } else if (!isAdvancedSearchOpen && searchInputRef) {
+ searchInputRef.current.focus();
+ }
+ }
+ }, [isAdvancedSearchOpen]);
+
+ // If a menu is open and has browser focus, then the escape key closes them and puts the browser focus onto their
+ // respective toggle. The 'date within' menu also needs to close when the 'tab' key is hit. However, hitting tab while
+ // focus is in the advanced search form should move the focus to the next form input, not close the advanced search
+ // menu.
+ const handleMenuKeys = (event) => {
+ if (isDateWithinOpen && dateWithinMenuRef.current && dateWithinMenuRef.current.contains(event.target)) {
+ if (event.key === 'Escape' || event.key === 'Tab') {
+ setIsDateWithinOpen(!isDateWithinOpen);
+ dateWithinToggleRef.current.focus();
+ }
+ }
+ if (isAdvancedSearchOpen && advancedSearchPaneRef.current && advancedSearchPaneRef.current.contains(event.target)) {
+ if (
+ event.key === 'Escape' ||
+ (event.key === 'Tab' &&
+ !event.shiftKey &&
+ advancedSearchPaneRef.current.querySelector('button[type=reset]') === event.target)
+ ) {
+ setIsAdvancedSearchOpen(!isAdvancedSearchOpen);
+ searchInputRef.current.focus();
+ }
+ }
+ };
+
+ // If a menu is open and has browser focus, then clicking outside the menu should close it.
+ const handleClickOutside = (event) => {
+ if (
+ isDateWithinOpen &&
+ dateWithinMenuRef &&
+ dateWithinMenuRef.current &&
+ !dateWithinMenuRef.current.contains(event.target)
+ ) {
+ setIsDateWithinOpen(false);
+ }
+ if (
+ isAdvancedSearchOpen &&
+ advancedSearchPaneRef &&
+ advancedSearchPaneRef.current &&
+ !advancedSearchPaneRef.current.contains(event.target)
+ ) {
+ setIsAdvancedSearchOpen(false);
+ }
+ };
+
+ useEffect(() => {
+ window.addEventListener('keydown', handleMenuKeys);
+ window.addEventListener('click', handleClickOutside);
+ return () => {
+ window.removeEventListener('keydown', handleMenuKeys);
+ window.removeEventListener('click', handleClickOutside);
+ };
+ }, [dateWithinMenuRef.current, advancedSearchPaneRef.current, isAdvancedSearchOpen, isDateWithinOpen]);
+
+ // This demo and its handling of 'date within' and a date picker is modeled after the gmail advanced search form.
+ const onSubmit = (event, _value) => {
+ event.preventDefault();
+
+ if (isValidDate(new Date(date)) && dateWithin) {
+ const afterDate = new Date(date);
+ const toDate = new Date(date);
+ switch (dateWithin) {
+ case '1 day':
+ afterDate.setDate(afterDate.getDate());
+ toDate.setDate(toDate.getDate() + 2);
+ break;
+ case '3 days':
+ afterDate.setDate(afterDate.getDate() - 2);
+ toDate.setDate(toDate.getDate() + 4);
+ break;
+ case '1 week':
+ afterDate.setDate(afterDate.getDate() - 6);
+ toDate.setDate(toDate.getDate() + 8);
+ break;
+ case '2 weeks':
+ afterDate.setDate(afterDate.getDate() - 13);
+ toDate.setDate(toDate.getDate() + 15);
+ break;
+ case '1 month':
+ afterDate.setMonth(afterDate.getMonth() - 1);
+ afterDate.setDate(afterDate.getDate() + 1);
+ toDate.setMonth(toDate.getMonth() + 1);
+ toDate.setDate(toDate.getDate() + 1);
+ break;
+ case '2 months':
+ afterDate.setMonth(afterDate.getMonth() - 2);
+ afterDate.setDate(afterDate.getDate() + 1);
+ toDate.setMonth(toDate.getMonth() + 2);
+ toDate.setDate(toDate.getDate() + 1);
+ break;
+ case '6 months':
+ afterDate.setMonth(afterDate.getMonth() - 6);
+ afterDate.setDate(afterDate.getDate() + 1);
+ toDate.setMonth(toDate.getMonth() + 6);
+ toDate.setDate(toDate.getDate() + 1);
+ break;
+ case '1 year':
+ afterDate.setFullYear(afterDate.getFullYear() - 1);
+ afterDate.setDate(afterDate.getDate() + 1);
+ toDate.setFullYear(toDate.getFullYear() + 1);
+ toDate.setDate(toDate.getDate() + 1);
+ break;
+ }
+ setValue(`${hasWords && hasWords + ' '}after:${yyyyMMddFormat(afterDate)} to:${yyyyMMddFormat(toDate)}`);
+ } else {
+ setValue(hasWords);
+ }
+
+ setIsAdvancedSearchOpen(false);
+ };
+
+ const searchInput = (
+ {
+ e.stopPropagation();
+ setIsAdvancedSearchOpen(isOpen);
+ }}
+ isAdvancedSearchOpen={isAdvancedSearchOpen}
+ onClear={onClear}
+ onSearch={onSubmit}
+ ref={searchInputRef}
+ id="custom-advanced-search"
+ aria-label="Composable advanced search"
+ />
+ );
+
+ // Clicking the 'date within' toggle should open its associated menu and then place the browser
+ // focus on the first menu item.
+ const toggleDateWithinMenu = (ev) => {
+ ev.stopPropagation(); // Stop handleClickOutside from handling
+ setTimeout(() => {
+ if (dateWithinMenuRef.current) {
+ const firstElement = dateWithinMenuRef.current.querySelector('li > button:not(:disabled)');
+ firstElement && firstElement.focus();
+ }
+ }, 0);
+ setIsDateWithinOpen(!isDateWithinOpen);
+ };
+
+ // Selecting a date within option closes the menu, sets the value of date within, and puts browser focus back
+ // on the date within toggle.
+ const onDateWithinSelect = (e, itemId) => {
+ e.stopPropagation();
+ setIsDateWithinOpen(false);
+ setDateWithin(itemId);
+ if (dateWithinToggleRef && dateWithinToggleRef.current) {
+ dateWithinToggleRef.current.focus();
+ }
+ };
+
+ const dateWithinOptions = (
+
+ );
+
+ const dateWithinToggle = (
+
+ {dateWithin}
+
+ );
+
+ const advancedForm = (
+
+
+
+
+
+
+
+
+
+ );
+
+ // Popper is just one way to build a relationship between a toggle and a menu.
+ return (
+ document.querySelector('#custom-advanced-search')}
+ />
+ );
+};
diff --git a/packages/react-core/src/demos/SearchInput/examples/SearchInputAutocomplete.tsx b/packages/react-core/src/demos/SearchInput/examples/SearchInputAutocomplete.tsx
new file mode 100644
index 00000000000..6fbbde55611
--- /dev/null
+++ b/packages/react-core/src/demos/SearchInput/examples/SearchInputAutocomplete.tsx
@@ -0,0 +1,165 @@
+import { useEffect, useRef, useState } from 'react';
+import { Menu, MenuContent, MenuItem, MenuList, Popper, SearchInput } from '@patternfly/react-core';
+
+import { words } from '../words';
+
+export const SearchInputAutocomplete: React.FunctionComponent = () => {
+ const [value, setValue] = useState('');
+ const [hint, setHint] = useState('');
+ const [autocompleteOptions, setAutocompleteOptions] = useState([]);
+
+ const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false);
+
+ const searchInputRef = useRef(null);
+ const autocompleteRef = useRef(null);
+
+ const onClear = () => {
+ setValue('');
+ };
+
+ const onChange = (_event, 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) => (
+
+ ));
+ 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) => (
+
+ ))
+ ].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 === ' ') {
+ setIsAutocompleteOpen(false);
+ if (event.key === 'Enter' || event.key === ' ') {
+ 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);
+ }
+ };
+
+ useEffect(() => {
+ window.addEventListener('keydown', handleMenuKeys);
+ window.addEventListener('click', handleClickOutside);
+ return () => {
+ window.removeEventListener('keydown', handleMenuKeys);
+ window.removeEventListener('click', handleClickOutside);
+ };
+ }, [isAutocompleteOpen, hint, searchInputRef.current]);
+
+ const searchInput = (
+
+ );
+
+ const autocomplete = (
+
+ );
+
+ return (
+ document.querySelector('#autocomplete-search')}
+ />
+ );
+};
diff --git a/packages/react-core/src/helpers/OUIA/OUIA.md b/packages/react-core/src/helpers/OUIA/OUIA.md
index da2167535c0..4b67e58ca0a 100644
--- a/packages/react-core/src/helpers/OUIA/OUIA.md
+++ b/packages/react-core/src/helpers/OUIA/OUIA.md
@@ -40,16 +40,8 @@ component.
### Example
-```js
-import { Fragment } from 'react';
-import { Button } from '@patternfly/react-core';
-
-
-
-
-
-
-
+```ts file="examples/OuiaExample.tsx"
+
```
## OUIA-compliant PatternFly 5 components
diff --git a/packages/react-core/src/helpers/OUIA/examples/OuiaExample.tsx b/packages/react-core/src/helpers/OUIA/examples/OuiaExample.tsx
new file mode 100644
index 00000000000..5511f98dd50
--- /dev/null
+++ b/packages/react-core/src/helpers/OUIA/examples/OuiaExample.tsx
@@ -0,0 +1,11 @@
+import { Fragment } from 'react';
+import { Button } from '@patternfly/react-core';
+
+export const OuiaExample: React.FunctionComponent = () => (
+
+
+
+
+
+
+);
diff --git a/packages/react-core/src/layouts/Bullseye/examples/Bullseye.md b/packages/react-core/src/layouts/Bullseye/examples/Bullseye.md
index fbafa25a1f9..dce64822131 100644
--- a/packages/react-core/src/layouts/Bullseye/examples/Bullseye.md
+++ b/packages/react-core/src/layouts/Bullseye/examples/Bullseye.md
@@ -11,10 +11,6 @@ import './bullseye.css';
### Basic
-```js
-import { Bullseye } from '@patternfly/react-core';
+```ts file="BullseyeBasic.tsx"
-
-