All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.src.components.Menu.Menu.tsx Maven / Gradle / Ivy

Go to download

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';




© 2015 - 2024 Weber Informatics LLC | Privacy Policy