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
24 changes: 20 additions & 4 deletions packages/react-core/src/components/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export interface MenuState {
transitionMoveTarget: HTMLElement;
flyoutRef: React.Ref<HTMLLIElement> | null;
disableHover: boolean;
currentDrilldownMenuId: string;
}

class MenuBase extends React.Component<MenuProps, MenuState> {
Expand Down Expand Up @@ -98,7 +99,8 @@ class MenuBase extends React.Component<MenuProps, MenuState> {
searchInputValue: '',
transitionMoveTarget: null,
flyoutRef: null,
disableHover: false
disableHover: false,
currentDrilldownMenuId: this.props.id
};

allowTabFirstItem() {
Expand Down Expand Up @@ -155,9 +157,19 @@ class MenuBase extends React.Component<MenuProps, MenuState> {
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;
}
Expand Down Expand Up @@ -284,12 +296,16 @@ class MenuBase extends React.Component<MenuProps, MenuState> {
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 &&
Expand Down
5 changes: 5 additions & 0 deletions packages/react-core/src/components/Menu/examples/Menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string[]>([]);
const [drilldownPath, setDrilldownPath] = React.useState<string[]>([]);
const [menuHeights, setMenuHeights] = React.useState<any>({});
const [activeMenu, setActiveMenu] = React.useState<string>('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<HTMLInputElement>();
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) => (
<MenuItem key={opt.item} itemId={index} {...opt.rest}>
{opt.item}
</MenuItem>
));
if (startInput && mapped.length === 0) {
mapped.push(
<MenuItem isDisabled key="no result">
No results found
</MenuItem>
);
}

return (
<Menu
id="filter_drilldown-rootMenu"
containsDrilldown
drilldownItemPath={drilldownPath}
drilledInMenus={menuDrilledIn}
activeMenu={activeMenu}
onDrillIn={drillIn}
onDrillOut={drillOut}
onGetMenuHeight={setHeight}
>
<MenuContent menuHeight={`${menuHeights[activeMenu]}px`}>
<MenuList>
<MenuItem
itemId="filter_group:start_rollout"
direction="down"
drilldownMenu={
<DrilldownMenu id="filter_drilldown-drilldownMenuStart">
<MenuItem itemId="filter_group:start_rollout_breadcrumb" direction="up">
Start rollout
</MenuItem>
<Divider component="li" />
<MenuInput>
<SearchInput
ref={searchRef}
value={startInput}
aria-label="Filter menu items"
type="search"
onChange={value => handleStartTextInputChange(value)}
/>
</MenuInput>
<Divider component="li" />
{mapped}
</DrilldownMenu>
}
>
Start rollout
</MenuItem>
<MenuItem itemId="item-a">Item B</MenuItem>
<MenuItem itemId="item-b">Item C</MenuItem>
<MenuItem itemId="item-c">Item D</MenuItem>
</MenuList>
</MenuContent>
</Menu>
);
};