package.src.components.Menu.Menu.tsx Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of react-core Show documentation
Show all versions of react-core Show documentation
This library provides a set of common React components for use with the PatternFly reference implementation.
The newest version!
import * as React from 'react';
import styles from '@patternfly/react-styles/css/components/Menu/menu';
import breadcrumbStyles from '@patternfly/react-styles/css/components/Breadcrumb/breadcrumb';
import dropdownStyles from '@patternfly/react-styles/css/components/Dropdown/dropdown';
import { css } from '@patternfly/react-styles';
import { getOUIAProps, OUIAProps, getDefaultOUIAId } from '../../helpers';
import { MenuContext } from './MenuContext';
import { canUseDOM } from '../../helpers/util';
import { KeyboardHandler } from '../../helpers';
export interface MenuProps extends Omit, 'ref' | 'onSelect'>, OUIAProps {
/** Anything that can be rendered inside of the Menu */
children?: React.ReactNode;
/** Additional classes added to the Menu */
className?: string;
/** ID of the menu */
id?: string;
/** Callback for updating when item selection changes. You can also specify onClick on the MenuItem. */
onSelect?: (event?: React.MouseEvent, itemId?: string | number) => void;
/** Single itemId for single select menus, or array of itemIds for multi select. You can also specify isSelected on the MenuItem. */
selected?: any | any[];
/** Callback called when an MenuItems's action button is clicked. You can also specify it within a MenuItemAction. */
onActionClick?: (event?: any, itemId?: any, actionId?: any) => void;
/** @beta Indicates if menu contains a flyout menu */
containsFlyout?: boolean;
/** @beta Indicating that the menu should have nav flyout styling */
isNavFlyout?: boolean;
/** @beta Indicates if menu contains a drilldown menu */
containsDrilldown?: boolean;
/** @beta Indicates if a menu is drilled into */
isMenuDrilledIn?: boolean;
/** @beta Indicates the path of drilled in menu itemIds */
drilldownItemPath?: string[];
/** @beta Array of menus that are drilled in */
drilledInMenus?: string[];
/** @beta Callback for drilling into a submenu */
onDrillIn?: (
event: React.KeyboardEvent | React.MouseEvent,
fromItemId: string,
toItemId: string,
itemId: string
) => void;
/** @beta Callback for drilling out of a submenu */
onDrillOut?: (event: React.KeyboardEvent | React.MouseEvent, toItemId: string, itemId: string) => void;
/** @beta Callback for collecting menu heights */
onGetMenuHeight?: (menuId: string, height: number) => void;
/** @beta ID of parent menu for drilldown menus */
parentMenu?: string;
/** @beta ID of the currently active menu for the drilldown variant */
activeMenu?: string;
/** @beta itemId of the currently active item. You can also specify isActive on the MenuItem. */
activeItemId?: string | number;
/** @hide Forwarded ref */
innerRef?: React.Ref;
/** Internal flag indicating if the Menu is the root of a menu tree */
isRootMenu?: boolean;
/** Indicates if the menu should be without the outer box-shadow */
isPlain?: boolean;
/** Indicates if the menu should be srollable */
isScrollable?: boolean;
/** Value to overwrite the randomly generated data-ouia-component-id.*/
ouiaId?: number | string;
/** Set the value of data-ouia-safe. Only set to true when the component is in a static state, i.e. no animations are occurring. At all other times, this value must be false. */
ouiaSafe?: boolean;
/** @beta Determines the accessible role of the menu. For a non-checkbox menu that can have
* one or more items selected, pass in "listbox". */
role?: string;
}
export interface MenuState {
ouiaStateId: string;
transitionMoveTarget: HTMLElement;
flyoutRef: React.Ref | null;
disableHover: boolean;
currentDrilldownMenuId: string;
}
class MenuBase extends React.Component {
static displayName = 'Menu';
static contextType = MenuContext;
context!: React.ContextType;
private menuRef = React.createRef();
private activeMenu = null as Element;
static defaultProps: MenuProps = {
ouiaSafe: true,
isRootMenu: true,
isPlain: false,
isScrollable: false,
role: 'menu'
};
constructor(props: MenuProps) {
super(props);
if (props.innerRef) {
this.menuRef = props.innerRef as React.RefObject;
}
}
state: MenuState = {
ouiaStateId: getDefaultOUIAId(Menu.displayName),
transitionMoveTarget: null,
flyoutRef: null,
disableHover: false,
currentDrilldownMenuId: this.props.id
};
allowTabFirstItem() {
// Allow tabbing to first menu item
const current = this.menuRef.current;
if (current) {
const first = current.querySelector('ul button:not(:disabled), ul a:not(:disabled)') as
| HTMLButtonElement
| HTMLAnchorElement;
if (first) {
first.tabIndex = 0;
}
}
}
componentDidMount() {
if (this.context) {
this.setState({ disableHover: this.context.disableHover });
}
if (canUseDOM) {
window.addEventListener('transitionend', this.props.isRootMenu ? this.handleDrilldownTransition : null);
}
this.allowTabFirstItem();
}
componentWillUnmount() {
if (canUseDOM) {
window.removeEventListener('transitionend', this.handleDrilldownTransition);
}
}
componentDidUpdate(prevProps: MenuProps) {
if (prevProps.children !== this.props.children) {
this.allowTabFirstItem();
}
}
handleDrilldownTransition = (event: TransitionEvent) => {
const current = this.menuRef.current;
if (
!current ||
(current !== (event.target as HTMLElement).closest(`.${styles.menu}`) &&
!Array.from(current.getElementsByClassName(styles.menu)).includes(
(event.target as HTMLElement).closest(`.${styles.menu}`)
))
) {
return;
}
if (this.state.transitionMoveTarget) {
this.state.transitionMoveTarget.focus();
this.setState({ transitionMoveTarget: null });
} else {
const nextMenu = current.querySelector('#' + this.props.activeMenu) || current || null;
const nextMenuLists = nextMenu.getElementsByTagName('UL');
if (nextMenuLists.length === 0) {
return;
}
const nextMenuChildren = Array.from(nextMenuLists[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(styles.divider))
)[0].firstChild;
(nextTarget as HTMLElement).focus();
(nextTarget as HTMLElement).tabIndex = 0;
}
};
handleExtraKeys = (event: KeyboardEvent) => {
const isDrilldown = this.props.containsDrilldown;
const activeElement = document.activeElement;
if (
(event.target as HTMLElement).closest(`.${styles.menu}`) !== this.activeMenu &&
!(event.target as HTMLElement).classList.contains(breadcrumbStyles.breadcrumbLink)
) {
this.activeMenu = (event.target as HTMLElement).closest(`.${styles.menu}`);
this.setState({ disableHover: true });
}
if ((event.target as HTMLElement).tagName === 'INPUT') {
return;
}
const parentMenu = this.activeMenu;
const key = event.key;
const isFromBreadcrumb =
activeElement.classList.contains(breadcrumbStyles.breadcrumbLink) ||
activeElement.classList.contains(dropdownStyles.dropdownToggle);
if (key === ' ' || key === 'Enter') {
event.preventDefault();
if (isDrilldown && !isFromBreadcrumb) {
const isDrillingOut = activeElement.closest('li').classList.contains('pf-m-current-path');
if (isDrillingOut && parentMenu.parentElement.tagName === 'LI') {
(activeElement as HTMLElement).tabIndex = -1;
(parentMenu.parentElement.firstChild as HTMLElement).tabIndex = 0;
this.setState({ transitionMoveTarget: parentMenu.parentElement.firstChild as HTMLElement });
} else {
if (activeElement.nextElementSibling && activeElement.nextElementSibling.classList.contains(styles.menu)) {
const childItems = Array.from(
activeElement.nextElementSibling.getElementsByTagName('UL')[0].children
).filter((el) => !(el.classList.contains('pf-m-disabled') || el.classList.contains(styles.divider)));
(activeElement as HTMLElement).tabIndex = -1;
(childItems[0].firstChild as HTMLElement).tabIndex = 0;
this.setState({ transitionMoveTarget: childItems[0].firstChild as HTMLElement });
}
}
}
(document.activeElement as HTMLElement).click();
}
};
createNavigableElements = () => {
const isDrilldown = this.props.containsDrilldown;
if (isDrilldown) {
return this.activeMenu
? Array.from(this.activeMenu.getElementsByTagName('UL')[0].children).filter(
(el) => !(el.classList.contains('pf-m-disabled') || el.classList.contains(styles.divider))
)
: [];
} else {
return this.menuRef.current
? Array.from(this.menuRef.current.getElementsByTagName('LI')).filter(
(el) => !(el.classList.contains('pf-m-disabled') || el.classList.contains(styles.divider))
)
: [];
}
};
render() {
const {
id,
children,
className,
onSelect,
selected = null,
onActionClick,
ouiaId,
ouiaSafe,
containsFlyout,
isNavFlyout,
containsDrilldown,
isMenuDrilledIn,
isPlain,
isScrollable,
drilldownItemPath,
drilledInMenus,
onDrillIn,
onDrillOut,
onGetMenuHeight,
parentMenu = null,
activeItemId = null,
/* eslint-disable @typescript-eslint/no-unused-vars */
innerRef,
isRootMenu,
activeMenu,
role,
/* eslint-enable @typescript-eslint/no-unused-vars */
...props
} = this.props;
const _isMenuDrilledIn = isMenuDrilledIn || (drilledInMenus && drilledInMenus.includes(id)) || false;
return (
this.setState({ flyoutRef }),
disableHover: this.state.disableHover,
role
}}
>
{isRootMenu && (
) || null}
additionalKeyHandler={this.handleExtraKeys}
createNavigableElements={this.createNavigableElements}
isActiveElement={(element: Element) =>
document.activeElement.closest('li') === element || // if element is a basic MenuItem
document.activeElement.parentElement === element ||
document.activeElement.closest(`.${styles.menuSearch}`) === element || // if element is a MenuSearch
(document.activeElement.closest('ol') && document.activeElement.closest('ol').firstChild === element)
}
getFocusableElement={(navigableElement: Element) =>
(navigableElement?.tagName === 'DIV' && navigableElement.querySelector('input')) || // for MenuSearchInput
((navigableElement.firstChild as Element)?.tagName === 'LABEL' &&
navigableElement.querySelector('input')) || // for MenuItem checkboxes
((navigableElement.firstChild as Element)?.tagName === 'DIV' &&
navigableElement.querySelector('a, button, input')) || // For aria-disabled element that is rendered inside a div with "display: contents" styling
(navigableElement.firstChild as Element)
}
noHorizontalArrowHandling={
document.activeElement &&
(document.activeElement.classList.contains(breadcrumbStyles.breadcrumbLink) ||
document.activeElement.classList.contains(dropdownStyles.dropdownToggle) ||
document.activeElement.tagName === 'INPUT')
}
noEnterHandling
noSpaceHandling
/>
)}
{children}
);
}
}
export const Menu = React.forwardRef((props: MenuProps, ref: React.Ref) => (
));
Menu.displayName = 'Menu';