diff --git a/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx b/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx index f5d82b24b48..6251789c2cd 100644 --- a/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx +++ b/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx @@ -8,6 +8,7 @@ import React, { FunctionComponent, PropsWithChildren, useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; import { disableStorybookControls, hideAllStorybookControls, @@ -17,7 +18,7 @@ import { EuiHeader, EuiHeaderSection, EuiHeaderSectionItem } from '../header'; import { EuiPageTemplate } from '../page_template'; import { EuiBottomBar } from '../bottom_bar'; import { EuiFlyout } from '../flyout'; -import { EuiButton } from '../button'; +import { EuiButton, EuiButtonEmpty } from '../button'; import { EuiTitle } from '../title'; import { @@ -100,10 +101,34 @@ export const Playground: Story = { items={[ { title: 'Get started', href: '#' }, ...renderGroup('Explore', [ - { title: 'Discover', href: '#' }, + { + title: 'Discover', + onClick: () => action('Discover')('clicked!'), + }, { title: 'Dashboards', href: '#' }, { title: 'Visualize library', href: '#' }, ]), + { + title: 'Machine learning', + items: [ + { title: 'Anomaly detection', href: '#' }, + { title: 'Data frame analytics', href: '#' }, + { + title: 'Sub group', + items: [ + { title: 'Sub item 1', href: '#' }, + { title: 'Sub item 2', href: '#' }, + ], + }, + ], + }, + { + renderItem: ({ closePortals }) => ( + + Custom rendered item + + ), + }, ...renderGroup('Content', [ { title: 'Indices', href: '#' }, { title: 'Transforms', href: '#' }, diff --git a/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_beta.test.tsx b/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_beta.test.tsx index 70ac29c632f..c767d49a96f 100644 --- a/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_beta.test.tsx +++ b/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_beta.test.tsx @@ -12,6 +12,8 @@ import { render } from '../../test/rtl'; import { shouldRenderCustomStyles } from '../../test/internal'; import { requiredProps } from '../../test'; +import { EuiCollapsibleNavLink } from './collapsible_nav_item/collapsible_nav_link'; +import { EuiCollapsibleNavSubItem } from './collapsible_nav_item'; import { EuiCollapsibleNavBeta } from './collapsible_nav_beta'; describe('EuiCollapsibleNavBeta', () => { @@ -186,6 +188,59 @@ describe('EuiCollapsibleNavBeta', () => { mobile.queryByTestSubject('overlayFlyout') ).not.toBeInTheDocument(); }); + + it('closes the overlay flyout automatically when links are clicked', () => { + mockWindowResize(600); + const { queryByTestSubject, getByTestSubject } = render( + + + Link + + + ); + fireEvent.click(getByTestSubject('euiCollapsibleNavButton')); + expect(queryByTestSubject('nav')).toBeInTheDocument(); + + fireEvent.click(getByTestSubject('link')); + expect(queryByTestSubject('nav')).not.toBeInTheDocument(); + }); + + it('allows preventing the overfly flyout close', () => { + mockWindowResize(600); + const { getByTestSubject } = render( + + e.preventDefault()} + > + Button + + + ); + fireEvent.click(getByTestSubject('euiCollapsibleNavButton')); + fireEvent.click(getByTestSubject('button')); + expect(getByTestSubject('nav')).toBeInTheDocument(); + }); + + it('allows custom rendered subitems to close the flyout', () => { + mockWindowResize(600); + const { queryByTestSubject, getByTestSubject } = render( + + ( + + )} + /> + + ); + fireEvent.click(getByTestSubject('euiCollapsibleNavButton')); + expect(queryByTestSubject('nav')).toBeInTheDocument(); + + fireEvent.click(getByTestSubject('custom')); + expect(queryByTestSubject('nav')).not.toBeInTheDocument(); + }); }); // TODO: Visual snapshot for left vs right `side` prop, once we add visual snapshot testing diff --git a/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_beta.tsx b/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_beta.tsx index 1b7a2d837f9..600a765fe31 100644 --- a/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_beta.tsx +++ b/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_beta.tsx @@ -139,6 +139,7 @@ const _EuiCollapsibleNavBeta: FunctionComponent = ({ const toggleOverlayFlyout = useCallback(() => { setIsOverlayOpen((isOpen) => !isOpen); }, []); + const closeOverlayFlyout = useCallback(() => setIsOverlayOpen(false), []); const flyoutType = isOverlay ? 'overlay' : 'push'; const isPush = !isOverlay; @@ -219,7 +220,7 @@ const _EuiCollapsibleNavBeta: FunctionComponent = ({ type={flyoutType} paddingSize="none" pushMinBreakpoint="xs" - onClose={onClose} + onClose={isPush ? onClose : closeOverlayFlyout} hideCloseButton={true} > {children} @@ -230,7 +231,13 @@ const _EuiCollapsibleNavBeta: FunctionComponent = ({ return ( event.preventDefault(), + }, { title: 'Popover link B', href: '#' }, { title: 'Popover link C', href: '#' }, + { + renderItem: ({ closePortals }) => ( + + ), + }, ], }, }; diff --git a/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/collapsed/collapsed_nav_popover.test.tsx b/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/collapsed/collapsed_nav_popover.test.tsx index 007aa846d54..249ca67a0ec 100644 --- a/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/collapsed/collapsed_nav_popover.test.tsx +++ b/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/collapsed/collapsed_nav_popover.test.tsx @@ -46,4 +46,72 @@ describe('EuiCollapsedNavPopover', () => { fireEvent.keyDown(baseElement, { key: 'Escape' }); await waitForEuiPopoverClose(); }); + + it('closes the popover when clicking on a link', async () => { + const { getByTestSubject } = render( + + ); + fireEvent.click(getByTestSubject('euiCollapsedNavButton')); + await waitForEuiPopoverOpen(); + + fireEvent.click(getByTestSubject('A')); + await waitForEuiPopoverOpen(); // popover should not close for non-links + + fireEvent.click(getByTestSubject('B')); + await waitForEuiPopoverClose(); // popover should close + }); + + it('does not close the popover if the link prevents default', async () => { + const onClick = jest.fn((event) => event.preventDefault()); + + const { getByTestSubject } = render( + + ); + fireEvent.click(getByTestSubject('euiCollapsedNavButton')); + await waitForEuiPopoverOpen(); + + fireEvent.click(getByTestSubject('A')); + expect(onClick).toHaveBeenCalledTimes(1); + await waitForEuiPopoverOpen(); // popover should not have closed + }); + + it('allows custom rendered subitems to close the popover', async () => { + const { getByTestSubject } = render( + ( + + ), + }, + ]} + /> + ); + fireEvent.click(getByTestSubject('euiCollapsedNavButton')); + await waitForEuiPopoverOpen(); + + fireEvent.click(getByTestSubject('custom')); + await waitForEuiPopoverClose(); + }); }); diff --git a/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/collapsed/collapsed_nav_popover.tsx b/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/collapsed/collapsed_nav_popover.tsx index 7a4ef740c60..7ce254b4877 100644 --- a/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/collapsed/collapsed_nav_popover.tsx +++ b/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/collapsed/collapsed_nav_popover.tsx @@ -6,21 +6,26 @@ * Side Public License, v 1. */ -import React, { FunctionComponent, useState, useCallback } from 'react'; +import React, { + FunctionComponent, + MouseEvent, + useState, + useCallback, + useContext, +} from 'react'; import { useEuiMemoizedStyles } from '../../../../services'; - import { type EuiPopoverProps, EuiPopover, EuiPopoverTitle, } from '../../../popover'; +import { EuiCollapsibleNavContext } from '../../context'; import { EuiCollapsibleNavSubItem, EuiCollapsibleNavItemProps, } from '../collapsible_nav_item'; - import { EuiCollapsedNavButton } from './collapsed_nav_button'; import { euiCollapsedNavPopoverStyles } from './collapsed_nav_popover.styles'; @@ -48,6 +53,20 @@ export const EuiCollapsedNavPopover: FunctionComponent< ); const closePopover = useCallback(() => setIsPopoverOpen(false), []); + const closePopoverClick = useCallback( + (event: MouseEvent) => { + closePopover(); + // Visually hide the tooltip for mouse users only + const isMouseEvent = event.screenX !== 0 && event.screenY !== 0; + if (isMouseEvent) setIsTooltipHidden(true); + }, + [closePopover] + ); + const [isTooltipHidden, setIsTooltipHidden] = useState(false); + const reshowTooltip = useCallback(() => setIsTooltipHidden(false), []); + + const navContext = useContext(EuiCollapsibleNavContext); + return ( } {...rest} @@ -81,9 +101,13 @@ export const EuiCollapsedNavPopover: FunctionComponent<
- {items!.map((item, index) => ( - - ))} + + {items!.map((item, index) => ( + + ))} +
); diff --git a/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.tsx b/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.tsx index b1b9bc71c7f..311e94a697c 100644 --- a/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.tsx +++ b/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.tsx @@ -9,6 +9,7 @@ import React, { FunctionComponent, HTMLAttributes, + MouseEventHandler, ReactNode, useContext, useMemo, @@ -103,7 +104,16 @@ export type EuiCollapsibleNavItemProps = _SharedEuiCollapsibleNavItemProps & { >; export type EuiCollapsibleNavCustomSubItem = { - renderItem: () => ReactNode; + renderItem: (options: { + /** + * When the side nav is collapsed on larger screens, the menu appears in an EuiPopover. + * When the sidenav is collapsed on smaller screens, the menu appears in an EuiFlyout. + * + * Use this handler to close either the portalled flyout or popover, depending on which is present. + * If the handler is not defined, it means there is no portal onscreen to close. + */ + closePortals?: MouseEventHandler; + }) => ReactNode; }; export type EuiCollapsibleNavSubItemProps = ExclusiveUnion< @@ -229,9 +239,10 @@ export const EuiCollapsibleNavSubItem: FunctionComponent< EuiCollapsibleNavSubItemProps > = ({ renderItem, className, ...props }) => { const classes = classNames('euiCollapsibleNavSubItem', className); + const { closePortals } = useContext(EuiCollapsibleNavContext); if (renderItem) { - return <>{renderItem()}; + return <>{renderItem({ closePortals })}; } return ( diff --git a/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_link.tsx b/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_link.tsx index eba54251b86..99bee4f1e3e 100644 --- a/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_link.tsx +++ b/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_link.tsx @@ -6,12 +6,18 @@ * Side Public License, v 1. */ -import React, { FunctionComponent, ReactNode } from 'react'; +import React, { + FunctionComponent, + ReactNode, + useCallback, + useContext, +} from 'react'; import classNames from 'classnames'; import { useEuiMemoizedStyles } from '../../../services'; import { EuiLink, EuiLinkProps } from '../../link'; +import { EuiCollapsibleNavContext } from '../context'; import type { _SharedEuiCollapsibleNavItemProps, _EuiCollapsibleNavItemDisplayProps, @@ -71,11 +77,25 @@ export const EuiCollapsibleNavLink: FunctionComponent< isInteractive && linkProps?.css, ]; + const { closePortals } = useContext(EuiCollapsibleNavContext); + const onClick = useCallback( + (event: React.MouseEvent) => { + rest.onClick?.(event); + linkProps?.onClick?.(event as any); + if (!event.defaultPrevented) { + closePortals?.(event); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [rest.onClick, linkProps?.onClick, closePortals] + ); + return isInteractive ? ( diff --git a/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/index.ts b/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/index.ts index 3cfd0ef8d2a..71f7efe0600 100644 --- a/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/index.ts +++ b/packages/eui/src/components/collapsible_nav_beta/collapsible_nav_item/index.ts @@ -11,4 +11,7 @@ export type { EuiCollapsibleNavSubItemProps, } from './collapsible_nav_item'; -export { EuiCollapsibleNavItem } from './collapsible_nav_item'; +export { + EuiCollapsibleNavItem, + EuiCollapsibleNavSubItem, +} from './collapsible_nav_item'; diff --git a/packages/eui/src/components/collapsible_nav_beta/context.ts b/packages/eui/src/components/collapsible_nav_beta/context.ts index 2cde9af0955..7ae67a730a9 100644 --- a/packages/eui/src/components/collapsible_nav_beta/context.ts +++ b/packages/eui/src/components/collapsible_nav_beta/context.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { createContext } from 'react'; +import { createContext, MouseEventHandler } from 'react'; import { _EuiFlyoutSide } from '../flyout/flyout'; @@ -15,6 +15,7 @@ type _EuiCollapsibleNavContext = { isPush: boolean; isOverlayOpen: boolean; side: _EuiFlyoutSide; + closePortals?: MouseEventHandler; }; export const EuiCollapsibleNavContext =