package.src.components.Nav.NavItem.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/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}