Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EuiCollapsibleNavBeta] Close popover when clicking on links #8139

Merged
merged 10 commits into from
Nov 27, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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 }) => (
<EuiButtonEmpty onClick={closePortals} size="s">
Custom rendered item
</EuiButtonEmpty>
),
},
...renderGroup('Content', [
{ title: 'Indices', href: '#' },
{ title: 'Transforms', href: '#' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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(
<EuiCollapsibleNavBeta data-test-subj="nav">
<EuiCollapsibleNavLink data-test-subj="link" href="#">
Link
</EuiCollapsibleNavLink>
</EuiCollapsibleNavBeta>
);
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(
<EuiCollapsibleNavBeta data-test-subj="nav">
<EuiCollapsibleNavLink
data-test-subj="button"
onClick={(e: React.MouseEvent) => e.preventDefault()}
>
Button
</EuiCollapsibleNavLink>
</EuiCollapsibleNavBeta>
);
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(
<EuiCollapsibleNavBeta data-test-subj="nav">
<EuiCollapsibleNavSubItem
renderItem={({ closePortals }) => (
<button onClick={closePortals} data-test-subj="custom">
Custom
</button>
)}
/>
</EuiCollapsibleNavBeta>
);
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ const _EuiCollapsibleNavBeta: FunctionComponent<EuiCollapsibleNavBetaProps> = ({
const toggleOverlayFlyout = useCallback(() => {
setIsOverlayOpen((isOpen) => !isOpen);
}, []);
const closeOverlayFlyout = useCallback(() => setIsOverlayOpen(false), []);

const flyoutType = isOverlay ? 'overlay' : 'push';
const isPush = !isOverlay;
Expand Down Expand Up @@ -219,7 +220,7 @@ const _EuiCollapsibleNavBeta: FunctionComponent<EuiCollapsibleNavBetaProps> = ({
type={flyoutType}
paddingSize="none"
pushMinBreakpoint="xs"
onClose={onClose}
onClose={isPush ? onClose : closeOverlayFlyout}
hideCloseButton={true}
>
{children}
Expand All @@ -230,7 +231,13 @@ const _EuiCollapsibleNavBeta: FunctionComponent<EuiCollapsibleNavBetaProps> = ({

return (
<EuiCollapsibleNavContext.Provider
value={{ isPush, isCollapsed, isOverlayOpen, side }}
value={{
isPush,
isCollapsed,
isOverlayOpen,
side,
closePortals: closeOverlayFlyout,
}}
>
<EuiCollapsibleNavButton
ref={buttonRef}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export const EuiCollapsedNavButton: FunctionComponent<
position={side}
display="block"
anchorProps={rest}
repositionOnScroll={true}
>
<EuiButtonIcon
data-test-subj="euiCollapsedNavButton"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,24 @@ export const Accordion: Story = {
title: 'Collapsed nav item',
icon: 'home',
items: [
{ title: 'Popover link A', href: '#', linkProps: { target: '_blank' } },
{
title: 'Popover link A',
href: '#',
linkProps: { target: '_blank' },
onClick: (event) => event.preventDefault(),
},
{ title: 'Popover link B', href: '#' },
{ title: 'Popover link C', href: '#' },
{
renderItem: ({ closePortals }) => (
<button
css={({ euiTheme }) => ({ padding: euiTheme.size.s })}
onClick={(event) => closePortals?.(event)}
>
Custom button
</button>
),
},
],
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<EuiCollapsedNavPopover
{...requiredProps}
title="Item"
titleElement="h3"
items={[
{ title: 'Not a link', 'data-test-subj': 'A' },
{ title: 'Nav link', href: '#', 'data-test-subj': 'B' },
]}
/>
);
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(
<EuiCollapsedNavPopover
{...requiredProps}
title="Item"
titleElement="h3"
items={[{ title: 'Link', onClick, 'data-test-subj': 'A' }]}
/>
);
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(
<EuiCollapsedNavPopover
{...requiredProps}
title="Item"
titleElement="h3"
items={[
{
renderItem: ({ closePortals }) => (
<button
onClick={(e) => closePortals?.(e)}
data-test-subj="custom"
>
Custom button
</button>
),
},
]}
/>
);
fireEvent.click(getByTestSubject('euiCollapsedNavButton'));
await waitForEuiPopoverOpen();

fireEvent.click(getByTestSubject('custom'));
await waitForEuiPopoverClose();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 (
<EuiPopover
closePopover={closePopover}
Expand All @@ -63,7 +82,8 @@ export const EuiCollapsedNavPopover: FunctionComponent<
iconProps={iconProps}
isSelected={isSelected}
onClick={togglePopover}
hideToolTip={isPopoverOpen}
hideToolTip={isPopoverOpen || isTooltipHidden}
linkProps={{ onMouseOver: reshowTooltip }}
/>
}
{...rest}
Expand All @@ -81,9 +101,13 @@ export const EuiCollapsedNavPopover: FunctionComponent<
</TitleElement>
</EuiPopoverTitle>
<div css={styles.euiCollapsedNavPopover__items}>
{items!.map((item, index) => (
<EuiCollapsibleNavSubItem key={index} {...item} />
))}
<EuiCollapsibleNavContext.Provider
value={{ ...navContext, closePortals: closePopoverClick }}
>
{items!.map((item, index) => (
<EuiCollapsibleNavSubItem key={index} {...item} />
))}
</EuiCollapsibleNavContext.Provider>
</div>
</EuiPopover>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import React, {
FunctionComponent,
HTMLAttributes,
MouseEventHandler,
ReactNode,
useContext,
useMemo,
Expand Down Expand Up @@ -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<
Expand Down Expand Up @@ -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 (
Expand Down
Loading