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) => ( - - {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) => ( - - {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 = ( - - - {autocompleteOptions} - - - ); - - 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 = ( - - - - 1 day - 3 days - 1 week - 2 weeks - 1 month - 2 months - 6 months - 1 year - - - - ); - - const dateWithinToggle = ( - - {dateWithin} - - ); - - const advancedForm = ( -
- - - -
- - { - setHasWords(value); - setValue(value); - }} - ref={firstAttrRef} - /> - - - - - - - - - - setDate(newValue)} - appendTo={() => document.querySelector('#datePicker')} - /> - - - - - - {!!onClear && ( - - )} - -
-
-
-
-
- ); +```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 = ( + + + + 1 day + 3 days + 1 week + 2 weeks + 1 month + 2 months + 6 months + 1 year + + + + ); + + const dateWithinToggle = ( + + {dateWithin} + + ); + + const advancedForm = ( +
+ + + +
+ + { + setHasWords(value); + setValue(value); + }} + ref={firstAttrRef} + /> + + + + + + + + + + setDate(newValue)} + appendTo={() => document.querySelector('#datePicker')} + /> + + + + + + {!!onClear && ( + + )} + +
+
+
+
+
+ ); + + // 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) => ( + + {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) => ( + + {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 = ( + + + {autocompleteOptions} + + + ); + + 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" - -
Bullseye ◎ layout
-
; ``` diff --git a/packages/react-core/src/layouts/Bullseye/examples/BullseyeBasic.tsx b/packages/react-core/src/layouts/Bullseye/examples/BullseyeBasic.tsx new file mode 100644 index 00000000000..203aa5c6aed --- /dev/null +++ b/packages/react-core/src/layouts/Bullseye/examples/BullseyeBasic.tsx @@ -0,0 +1,7 @@ +import { Bullseye } from '@patternfly/react-core'; + +export const BullseyeBasic: React.FunctionComponent = () => ( + +
Bullseye ◎ layout
+
+); diff --git a/packages/react-core/src/layouts/Level/examples/Level.md b/packages/react-core/src/layouts/Level/examples/Level.md index 9311b69f606..007d9f39111 100644 --- a/packages/react-core/src/layouts/Level/examples/Level.md +++ b/packages/react-core/src/layouts/Level/examples/Level.md @@ -11,24 +11,12 @@ import './level.css'; ### Basic -```js -import { Level, LevelItem } from '@patternfly/react-core'; - - - Level Item - Level Item - Level Item -; +```ts file="LevelBasic.tsx" + ``` ### With gutters -```js -import { Level, LevelItem } from '@patternfly/react-core'; +```ts file="LevelWithGutters.tsx" - - Level Item - Level Item - Level Item -; ``` diff --git a/packages/react-core/src/layouts/Level/examples/LevelBasic.tsx b/packages/react-core/src/layouts/Level/examples/LevelBasic.tsx new file mode 100644 index 00000000000..7e2c24306b4 --- /dev/null +++ b/packages/react-core/src/layouts/Level/examples/LevelBasic.tsx @@ -0,0 +1,9 @@ +import { Level, LevelItem } from '@patternfly/react-core'; + +export const LevelBasic: React.FunctionComponent = () => ( + + Level Item + Level Item + Level Item + +); diff --git a/packages/react-core/src/layouts/Level/examples/LevelWithGutters.tsx b/packages/react-core/src/layouts/Level/examples/LevelWithGutters.tsx new file mode 100644 index 00000000000..3358d15d669 --- /dev/null +++ b/packages/react-core/src/layouts/Level/examples/LevelWithGutters.tsx @@ -0,0 +1,9 @@ +import { Level, LevelItem } from '@patternfly/react-core'; + +export const LevelWithGutters: React.FunctionComponent = () => ( + + Level Item + Level Item + Level Item + +); diff --git a/packages/react-core/src/layouts/Split/examples/Split.md b/packages/react-core/src/layouts/Split/examples/Split.md index 501a3f24c66..49ad26e7b6d 100644 --- a/packages/react-core/src/layouts/Split/examples/Split.md +++ b/packages/react-core/src/layouts/Split/examples/Split.md @@ -11,47 +11,17 @@ import './split.css'; ### Basic -```js -import { Split, SplitItem } from '@patternfly/react-core'; - - - content - pf-m-fill - content -; +```ts file="SplitBasic.tsx" ``` ### With gutter -```js -import { Split, SplitItem } from '@patternfly/react-core'; +```ts file="SplitWithGutter.tsx" - - content - pf-m-fill - content -; ``` ### Wrappable -```js -import { Split, SplitItem } from '@patternfly/react-core'; - - - content - content - content - content - content - content - content - content - content - content - content - content - content - content -; +```ts file="SplitWrappable.tsx" + ``` diff --git a/packages/react-core/src/layouts/Split/examples/SplitBasic.tsx b/packages/react-core/src/layouts/Split/examples/SplitBasic.tsx new file mode 100644 index 00000000000..a530ff22c37 --- /dev/null +++ b/packages/react-core/src/layouts/Split/examples/SplitBasic.tsx @@ -0,0 +1,9 @@ +import { Split, SplitItem } from '@patternfly/react-core'; + +export const SplitBasic: React.FunctionComponent = () => ( + + content + pf-m-fill + content + +); diff --git a/packages/react-core/src/layouts/Split/examples/SplitWithGutter.tsx b/packages/react-core/src/layouts/Split/examples/SplitWithGutter.tsx new file mode 100644 index 00000000000..9243346bd41 --- /dev/null +++ b/packages/react-core/src/layouts/Split/examples/SplitWithGutter.tsx @@ -0,0 +1,9 @@ +import { Split, SplitItem } from '@patternfly/react-core'; + +export const SplitWithGutter: React.FunctionComponent = () => ( + + content + pf-m-fill + content + +); diff --git a/packages/react-core/src/layouts/Split/examples/SplitWrappable.tsx b/packages/react-core/src/layouts/Split/examples/SplitWrappable.tsx new file mode 100644 index 00000000000..d4eaaf4c2e0 --- /dev/null +++ b/packages/react-core/src/layouts/Split/examples/SplitWrappable.tsx @@ -0,0 +1,20 @@ +import { Split, SplitItem } from '@patternfly/react-core'; + +export const SplitWrappable: React.FunctionComponent = () => ( + + content + content + content + content + content + content + content + content + content + content + content + content + content + content + +);