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 @@ -25,6 +26,7 @@ import {
EuiCollapsibleNavBetaProps,
EuiCollapsibleNavItemProps,
} from './';
import { EuiCollapsibleNavSubItem } from './collapsible_nav_item';

const meta: Meta<EuiCollapsibleNavBetaProps> = {
title: 'Navigation/EuiCollapsibleNav (beta)/EuiCollapsibleNavBeta',
Expand Down Expand Up @@ -100,10 +102,37 @@ 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: ({ closePopover }) => (
<EuiCollapsibleNavSubItem
title="Render item node"
onClick={() => {
closePopover?.();
}}
/>
),
},
...renderGroup('Content', [
{ title: 'Indices', href: '#' },
{ title: 'Transforms', href: '#' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,27 @@ describe('EuiCollapsedNavPopover', () => {
fireEvent.keyDown(baseElement, { key: 'Escape' });
await waitForEuiPopoverClose();
});

it('closes the popover when clicking on a link', async () => {
const onClick = jest.fn();
const { getByTestSubject } = render(
<EuiCollapsedNavPopover
{...requiredProps}
title="Item"
titleElement="h3"
items={[
{ title: 'Sub-item A', onClick, 'data-test-subj': 'A' },
{ title: 'Sub-item B', href: '#', 'data-test-subj': 'B' },
]}
/>
);
fireEvent.click(getByTestSubject('euiCollapsedNavButton'));
await waitForEuiPopoverOpen();

expect(onClick).not.toHaveBeenCalled();
fireEvent.click(getByTestSubject('A'));

await waitForEuiPopoverClose(); // popover should close
expect(onClick).toHaveBeenCalledTimes(1); // custom onClick should be called
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import {
EuiCollapsibleNavSubItem,
EuiCollapsibleNavItemProps,
EuiCollapsibleNavSubItemProps,
} from '../collapsible_nav_item';

import { EuiCollapsedNavButton } from './collapsed_nav_button';
Expand Down Expand Up @@ -48,6 +49,43 @@ export const EuiCollapsedNavPopover: FunctionComponent<
);
const closePopover = useCallback(() => setIsPopoverOpen(false), []);

const closePopoverAndClearFocus = useCallback(() => {
closePopover();

setTimeout(() => {
if (document.activeElement instanceof HTMLElement) {
// We don't want the tooltip to appear after closing the popover
document.activeElement.blur();
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
}
}, 10);
}, [closePopover]);

const withOnClick = (
item: EuiCollapsibleNavSubItemProps
): EuiCollapsibleNavSubItemProps => {
if (item.renderItem) {
return item;
}

const updatedItem: EuiCollapsibleNavSubItemProps = {
...item,
};

updatedItem.items = item.items ? item.items?.map(withOnClick) : undefined;

if (!updatedItem.items) {
// Only override the onClick if there are no sub-items (leaf node)
updatedItem.onClick = (e: React.MouseEvent<HTMLElement>) => {
if (item.onClick) {
item.onClick(e);
}
closePopoverAndClearFocus();
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
};
}

return updatedItem;
};

return (
<EuiPopover
closePopover={closePopover}
Expand Down Expand Up @@ -82,7 +120,11 @@ export const EuiCollapsedNavPopover: FunctionComponent<
</EuiPopoverTitle>
<div css={styles.euiCollapsedNavPopover__items}>
{items!.map((item, index) => (
<EuiCollapsibleNavSubItem key={index} {...item} />
<EuiCollapsibleNavSubItem
key={index}
closePopover={closePopoverAndClearFocus}
{...withOnClick(item)}
/>
))}
</div>
</EuiPopover>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,13 @@ export type EuiCollapsibleNavItemProps = _SharedEuiCollapsibleNavItemProps & {
>;

export type EuiCollapsibleNavCustomSubItem = {
renderItem: () => ReactNode;
renderItem: (options: {
/**
* When the side nav is collapsed, the menu appears in a EuiPopover. Use this handler to close the popover.
* If the handler is not defined it means that the side nav is not collapsed.
*/
closePopover?: () => void;
}) => ReactNode;
};

export type EuiCollapsibleNavSubItemProps = ExclusiveUnion<
Expand Down Expand Up @@ -226,12 +232,12 @@ export const EuiCollapsibleNavItemTitle: FunctionComponent<
* or they can simply be more links or accordions
*/
export const EuiCollapsibleNavSubItem: FunctionComponent<
EuiCollapsibleNavSubItemProps
> = ({ renderItem, className, ...props }) => {
EuiCollapsibleNavSubItemProps & { closePopover?: () => void }
> = ({ renderItem, className, closePopover, ...props }) => {
const classes = classNames('euiCollapsibleNavSubItem', className);

if (renderItem) {
return <>{renderItem()}</>;
return <>{renderItem({ closePopover })}</>;
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ export type {
EuiCollapsibleNavSubItemProps,
} from './collapsible_nav_item';

export { EuiCollapsibleNavItem } from './collapsible_nav_item';
export {
EuiCollapsibleNavItem,
EuiCollapsibleNavSubItem,
} from './collapsible_nav_item';
Loading