diff --git a/packages/react-core/src/components/Menu/Menu.tsx b/packages/react-core/src/components/Menu/Menu.tsx index 01eb36031fe..c3de96009a9 100644 --- a/packages/react-core/src/components/Menu/Menu.tsx +++ b/packages/react-core/src/components/Menu/Menu.tsx @@ -71,6 +71,7 @@ export interface MenuState { transitionMoveTarget: HTMLElement; flyoutRef: React.Ref | null; disableHover: boolean; + currentDrilldownMenuId: string; } class MenuBase extends React.Component { @@ -98,7 +99,8 @@ class MenuBase extends React.Component { searchInputValue: '', transitionMoveTarget: null, flyoutRef: null, - disableHover: false + disableHover: false, + currentDrilldownMenuId: this.props.id }; allowTabFirstItem() { @@ -155,9 +157,19 @@ class MenuBase extends React.Component { this.setState({ transitionMoveTarget: null }); } else { const nextMenu = current.querySelector('#' + this.props.activeMenu) || current || null; - const nextTarget = Array.from(nextMenu.getElementsByTagName('UL')[0].children).filter( + const nextMenuChildren = Array.from(nextMenu.getElementsByTagName('UL')[0].children); + + if (!this.state.currentDrilldownMenuId || nextMenu.id !== this.state.currentDrilldownMenuId) { + this.setState({ currentDrilldownMenuId: nextMenu.id }); + } else { + // if the drilldown transition ends on the same menu, do not focus the first item + return; + } + + const nextTarget = nextMenuChildren.filter( el => !(el.classList.contains('pf-m-disabled') || el.classList.contains('pf-c-divider')) )[0].firstChild; + (nextTarget as HTMLElement).focus(); (nextTarget as HTMLElement).tabIndex = 0; } @@ -284,12 +296,16 @@ class MenuBase extends React.Component { additionalKeyHandler={this.handleExtraKeys} createNavigableElements={this.createNavigableElements} isActiveElement={(element: Element) => - document.activeElement.closest('li') === element || + document.activeElement.closest('li') === element || // if element is a basic MenuItem document.activeElement.parentElement === element || + document.activeElement.closest('.pf-c-menu__search') === element || // if element is a MenuInput (document.activeElement.closest('ol') && document.activeElement.closest('ol').firstChild === element) } getFocusableElement={(navigableElement: Element) => - navigableElement.querySelector('input') || (navigableElement.firstChild as Element) + (navigableElement.tagName === 'DIV' && navigableElement.querySelector('input')) || // for MenuInput + ((navigableElement.firstChild as Element).tagName === 'LABEL' && + navigableElement.querySelector('input')) || // for MenuItem checkboxes + (navigableElement.firstChild as Element) } noHorizontalArrowHandling={ document.activeElement && diff --git a/packages/react-core/src/components/Menu/examples/Menu.md b/packages/react-core/src/components/Menu/examples/Menu.md index 8e73ca06fc0..47a7199bdf4 100644 --- a/packages/react-core/src/components/Menu/examples/Menu.md +++ b/packages/react-core/src/components/Menu/examples/Menu.md @@ -105,6 +105,11 @@ To render an initially drilled in menu, the `menuDrilledIn`, `drilldownPath`, an ```ts file="MenuWithDrilldownBreadcrumbs.tsx" isBeta ``` +### With drilldown and inline filter + +```ts file="MenuFilterDrilldown.tsx" +``` + ### Scrollable ```ts file="MenuScrollable.tsx" diff --git a/packages/react-core/src/components/Menu/examples/MenuFilterDrilldown.tsx b/packages/react-core/src/components/Menu/examples/MenuFilterDrilldown.tsx new file mode 100644 index 00000000000..e22dab9b11c --- /dev/null +++ b/packages/react-core/src/components/Menu/examples/MenuFilterDrilldown.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { + Menu, + MenuContent, + MenuList, + MenuItem, + Divider, + DrilldownMenu, + MenuInput, + SearchInput +} from '@patternfly/react-core'; + +export const MenuWithDrilldown: React.FunctionComponent = () => { + const [menuDrilledIn, setMenuDrilledIn] = React.useState([]); + const [drilldownPath, setDrilldownPath] = React.useState([]); + const [menuHeights, setMenuHeights] = React.useState({}); + const [activeMenu, setActiveMenu] = React.useState('filter_drilldown-rootMenu'); + + const drillIn = (fromMenuId: string, toMenuId: string, pathId: string) => { + setMenuDrilledIn([...menuDrilledIn, fromMenuId]); + setDrilldownPath([...drilldownPath, pathId]); + setActiveMenu(toMenuId); + }; + + const drillOut = (toMenuId: string) => { + const menuDrilledInSansLast = menuDrilledIn.slice(0, menuDrilledIn.length - 1); + const pathSansLast = drilldownPath.slice(0, drilldownPath.length - 1); + setMenuDrilledIn(menuDrilledInSansLast); + setDrilldownPath(pathSansLast); + setActiveMenu(toMenuId); + }; + + const setHeight = (menuId: string, height: number) => { + if ( + menuHeights[menuId] === undefined || + (menuId !== 'filter_drilldown-rootMenu' && menuHeights[menuId] !== height) + ) { + setMenuHeights({ ...menuHeights, [menuId]: height }); + } + }; + + const searchRef = React.createRef(); + const [startInput, setStartInput] = React.useState(''); + + const handleStartTextInputChange = (value: string) => { + setStartInput(value); + searchRef?.current?.focus(); + }; + + const startDrillItems = [ + { + item: 'Application grouping', + rest: { description: 'Description text' } + }, + { item: 'Labels' }, + { item: 'Annotations' }, + { item: 'Count' }, + { item: 'Count 2' }, + { item: 'Count 3' }, + { item: 'Other' } + ]; + + const mapped = startDrillItems + .filter(opt => !startInput || opt.item.toLowerCase().includes(startInput.toString().toLowerCase())) + .map((opt, index) => ( + + {opt.item} + + )); + if (startInput && mapped.length === 0) { + mapped.push( + + No results found + + ); + } + + return ( + + + + + + Start rollout + + + + handleStartTextInputChange(value)} + /> + + + {mapped} + + } + > + Start rollout + + Item B + Item C + Item D + + + + ); +};