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

package.src.components.Nav.NavItem.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/Nav/nav';
import { css } from '@patternfly/react-styles';
import { NavContext, NavSelectClickHandler } from './Nav';
import { PageSidebarContext } from '../Page/PageSidebar';
import { useOUIAProps, OUIAProps } from '../../helpers';
import { Popper } from '../../helpers/Popper/Popper';
import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon';

export interface NavItemProps extends Omit, 'onClick'>, OUIAProps {
  /** Content rendered inside the nav item. */
  children?: React.ReactNode;
  /** Whether to set className on children when React.isValidElement(children) */
  styleChildren?: boolean;
  /** Additional classes added to the nav item */
  className?: string;
  /** Target navigation link. Should not be used if the flyout prop is defined. */
  to?: string;
  /** Flag indicating whether the item is active */
  isActive?: boolean;
  /** Group identifier, will be returned with the onToggle and onSelect callback passed to the Nav component */
  groupId?: string | number | null;
  /** Item identifier, will be returned with the onToggle and onSelect callback passed to the Nav component */
  itemId?: string | number | null;
  /** If true prevents the default anchor link action to occur. Set to true if you want to handle navigation yourself. */
  preventDefault?: boolean;
  /** Callback for item click */
  onClick?: NavSelectClickHandler;
  /** Component used to render NavItems if  React.isValidElement(children) is false */
  component?: React.ElementType | React.ComponentType;
  /** Flyout of a nav item. This should be a Menu component. Should not be used if the to prop is defined. */
  flyout?: React.ReactElement;
  /** Callback when flyout is opened or closed */
  onShowFlyout?: () => void;
  /** z-index of the flyout nav item */
  zIndex?: number;
  /** 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 Adds a wrapper around the nav link text. Improves the layout when the text is a react node. */
  hasNavLinkWrapper?: boolean;
}

export const NavItem: React.FunctionComponent = ({
  children,
  styleChildren = true,
  className,
  to,
  isActive = false,
  groupId = null as string,
  itemId = null as string,
  preventDefault = false,
  onClick,
  component = 'a',
  flyout,
  onShowFlyout,
  ouiaId,
  ouiaSafe,
  zIndex = 9999,
  hasNavLinkWrapper,
  ...props
}: NavItemProps) => {
  const { flyoutRef, setFlyoutRef, navRef } = React.useContext(NavContext);
  const { isSidebarOpen } = React.useContext(PageSidebarContext);
  const [flyoutTarget, setFlyoutTarget] = React.useState(null);
  const [isHovered, setIsHovered] = React.useState(false);
  const ref = React.useRef();
  const flyoutVisible = ref === flyoutRef;
  const popperRef = React.useRef();
  const hasFlyout = flyout !== undefined;
  const Component = hasFlyout ? 'button' : (component as any);

  // A NavItem should not be both a link and a flyout
  if (to && hasFlyout) {
    // eslint-disable-next-line no-console
    console.error('NavItem cannot have both "to" and "flyout" props.');
  }

  const showFlyout = (show: boolean, override?: boolean) => {
    if ((!flyoutVisible || override) && show) {
      setFlyoutRef(ref);
    } else if ((flyoutVisible || override) && !show) {
      setFlyoutRef(null);
    }

    onShowFlyout && show && onShowFlyout();
  };

  const onMouseOver = (event: React.MouseEvent) => {
    const evtContainedInFlyout = (event.target as HTMLElement).closest(`.${styles.navItem}.pf-m-flyout`);
    if (hasFlyout && !flyoutVisible) {
      showFlyout(true);
    } else if (flyoutRef !== null && !evtContainedInFlyout) {
      setFlyoutRef(null);
    }
  };

  const onFlyoutClick = (event: MouseEvent) => {
    const target = event.target;
    const closestItem = (target as HTMLElement).closest('.pf-m-flyout');
    if (!closestItem) {
      if (hasFlyout) {
        showFlyout(false, true);
      } else if (flyoutRef !== null) {
        setFlyoutRef(null);
      }
    }
  };

  const handleFlyout = (event: KeyboardEvent) => {
    const key = event.key;
    const target = event.target as HTMLElement;

    if ((key === ' ' || key === 'Enter' || key === 'ArrowRight') && hasFlyout && ref?.current?.contains(target)) {
      event.stopPropagation();
      event.preventDefault();
      if (!flyoutVisible) {
        showFlyout(true);
        setFlyoutTarget(target as HTMLElement);
      }
    }

    // We only want the NavItem to handle closing a flyout menu if only the first level flyout is open.
    // Otherwise, MenuItem should handle closing its flyouts
    if (
      (key === 'Escape' || key === 'ArrowLeft') &&
      popperRef?.current?.querySelectorAll(`.${styles.menu}`).length === 1
    ) {
      if (flyoutVisible) {
        event.stopPropagation();
        event.preventDefault();
        showFlyout(false);
      }
    }
  };

  React.useEffect(() => {
    if (hasFlyout) {
      window.addEventListener('click', onFlyoutClick);
    }
    return () => {
      if (hasFlyout) {
        window.removeEventListener('click', onFlyoutClick);
      }
    };
  }, []);

  React.useEffect(() => {
    if (flyoutTarget) {
      if (flyoutVisible) {
        const flyoutItems = Array.from(
          (popperRef.current as HTMLElement).getElementsByTagName('UL')[0].children
        ).filter((el) => !(el.classList.contains('pf-m-disabled') || el.classList.contains(styles.divider)));
        (flyoutItems[0].firstChild as HTMLElement).focus();
      } else {
        flyoutTarget.focus();
      }
    }
  }, [flyoutVisible, flyoutTarget]);

  const flyoutButton = (
    
      
        
      
    
  );

  const ariaFlyoutProps = {
    'aria-haspopup': 'menu',
    'aria-expanded': flyoutVisible
  };

  const tabIndex = isSidebarOpen ? null : -1;

  const renderDefaultLink = (context: any): React.ReactNode => {
    const preventLinkDefault = preventDefault || !to;
    return (
       context.onSelect(e, groupId, itemId, to, preventLinkDefault, onClick)}
        className={css(
          styles.navLink,
          isActive && styles.modifiers.current,
          isHovered && styles.modifiers.hover,
          className
        )}
        aria-current={isActive ? 'page' : null}
        tabIndex={tabIndex}
        {...(hasFlyout && { ...ariaFlyoutProps })}
        {...props}
      >
        {hasNavLinkWrapper ? {children} : children}
        {flyout && flyoutButton}
      
    );
  };

  const renderClonedChild = (context: any, child: React.ReactElement): React.ReactNode =>
    React.cloneElement(child, {
      onClick: (e: MouseEvent) => context.onSelect(e, groupId, itemId, to, preventDefault, onClick),
      'aria-current': isActive ? 'page' : null,
      ...(styleChildren && {
        className: css(styles.navLink, isActive && styles.modifiers.current, child.props && child.props.className)
      }),
      tabIndex: child.props.tabIndex || tabIndex,
      children: hasFlyout ? (
        
          {child.props.children}
          {flyoutButton}
        
      ) : (
        child.props.children
      )
    });

  const ouiaProps = useOUIAProps(NavItem.displayName, ouiaId, ouiaSafe);

  const handleMouseEnter = () => {
    setIsHovered(true);
  };

  const handleMouseLeave = () => {
    setIsHovered(false);
  };

  const flyoutPopper = (
    
          {flyout}
        
} popperRef={popperRef} placement="right-start" isVisible={flyoutVisible} onDocumentKeyDown={handleFlyout} zIndex={zIndex} appendTo={navRef?.current} /> ); const navItem = ( <>
  • {(context) => React.isValidElement(children) ? renderClonedChild(context, children as React.ReactElement) : renderDefaultLink(context) }
  • {flyout && flyoutPopper} ); return navItem; }; NavItem.displayName = 'NavItem';




    © 2015 - 2024 Weber Informatics LLC | Privacy Policy