From b2687f25b8acc129b6563fd78ffb60199ccef9bf Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Wed, 7 Feb 2024 09:23:29 -0800 Subject: [PATCH] [EuiContextMenu] Support new `renderItem` custom content in `items` arrays (#7510) --- changelogs/upcoming/7510.md | 1 + .../context_menu/context_menu_example.js | 14 +++++++- .../context_menu/context_menu_with_content.js | 22 ++++++++---- .../context_menu/context_menu.stories.tsx | 18 ++++++++++ .../context_menu/context_menu.test.tsx | 36 +++++++++++++++++++ src/components/context_menu/context_menu.tsx | 21 +++++++++-- 6 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 changelogs/upcoming/7510.md diff --git a/changelogs/upcoming/7510.md b/changelogs/upcoming/7510.md new file mode 100644 index 00000000000..aa4670142b8 --- /dev/null +++ b/changelogs/upcoming/7510.md @@ -0,0 +1 @@ +- Updated `EuiContextMenu` with a new `panels.items.renderItem` property, which allows rendering completely custom items next to standard `EuiContextMenuItem` objects diff --git a/src-docs/src/views/context_menu/context_menu_example.js b/src-docs/src/views/context_menu/context_menu_example.js index 0694c88222d..63ce541d310 100644 --- a/src-docs/src/views/context_menu/context_menu_example.js +++ b/src-docs/src/views/context_menu/context_menu_example.js @@ -9,6 +9,7 @@ import { EuiContextMenu, EuiContextMenuItem, EuiContextMenuPanel, + EuiTitle, } from '../../../../src/components'; import { EuiContextMenuPanelDescriptor } from '!!prop-loader!../../../../src/components/context_menu/context_menu'; @@ -173,6 +174,9 @@ export const ContextMenuExample = { ], text: ( <> + +

Custom panels

+

Context menu panels can be passed React elements through the{' '} content prop instead of items. @@ -184,16 +188,24 @@ export const ContextMenuExample = { width: [number of pixels] to the panel tree.

+ +

Custom items

+

You can add separator lines in the items prop if you define an item as{' '} {'{isSeparator: true}'}. This will - pass the rest of its fields as props to a{' '} + pass the rest of the object properties to an{' '} EuiHorizontalRule {' '} component.

+

+ For completely custom rendered items, you can use the{' '} + {'{renderItem}'} property to pass a component or + any function that returns a JSX node. +

), demo: , diff --git a/src-docs/src/views/context_menu/context_menu_with_content.js b/src-docs/src/views/context_menu/context_menu_with_content.js index 0cc869e2f69..fed43aca2d7 100644 --- a/src-docs/src/views/context_menu/context_menu_with_content.js +++ b/src-docs/src/views/context_menu/context_menu_with_content.js @@ -5,6 +5,7 @@ import { EuiContextMenu, EuiIcon, EuiPopover, + EuiPopoverFooter, EuiSpacer, EuiText, } from '../../../../src/components'; @@ -64,10 +65,6 @@ export default () => { icon: , onClick: closePopover, }, - { - isSeparator: true, - key: 'sep', - }, { name: 'See more', icon: 'plusInCircle', @@ -78,6 +75,19 @@ export default () => { content: , }, }, + { + isSeparator: true, + key: 'sep', + }, + { + renderItem: () => ( + + + I'm a custom item! + + + ), + }, ], }); }; @@ -116,7 +126,7 @@ export default () => { ); return ( - + <> { > - + ); }; diff --git a/src/components/context_menu/context_menu.stories.tsx b/src/components/context_menu/context_menu.stories.tsx index 42f09762317..996a1aa7531 100644 --- a/src/components/context_menu/context_menu.stories.tsx +++ b/src/components/context_menu/context_menu.stories.tsx @@ -12,6 +12,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { EuiPopover } from '../popover'; import { EuiButton } from '../button'; import { EuiIcon } from '../icon'; +import { EuiTitle } from '../title'; import { EuiContextMenu, EuiContextMenuProps } from './context_menu'; @@ -95,6 +96,23 @@ const panels: EuiContextMenuProps['panels'] = [ icon: 'user', panel: 2, }, + { + isSeparator: true, + }, + { + renderItem: () => ( + ({ + marginInline: euiTheme.size.s, + marginBlockStart: euiTheme.size.m, + marginBlockEnd: euiTheme.size.xs, + })} + > +

Custom rendered subtitle

+
+ ), + }, { name: 'Permalinks', icon: 'user', diff --git a/src/components/context_menu/context_menu.test.tsx b/src/components/context_menu/context_menu.test.tsx index 7300f3397b6..39fb4138df2 100644 --- a/src/components/context_menu/context_menu.test.tsx +++ b/src/components/context_menu/context_menu.test.tsx @@ -126,6 +126,42 @@ describe('EuiContextMenu', () => { expect(container.firstChild).toMatchSnapshot(); }); + it('allows wildcard content via the `renderItem` prop', () => { + const CustomComponent = () => ( +
Hello world
+ ); + + const { container, getByTestSubject } = render( +

Subtitle

, + }, + { + key: 'custom', + renderItem: CustomComponent, + }, + ...panel3.items, + ], + }, + ]} + initialPanelId={1} + /> + ); + + expect(container.querySelectorAll('.euiContextMenuItem')).toHaveLength(3); + expect(getByTestSubject('subtitle')).toHaveTextContent('Subtitle'); + expect(getByTestSubject('custom')).toHaveTextContent('Hello world'); + }); + describe('props', () => { describe('panels and initialPanelId', () => { it('renders the referenced panel', () => { diff --git a/src/components/context_menu/context_menu.tsx b/src/components/context_menu/context_menu.tsx index b1f99bb35c0..4a23f32a9ea 100644 --- a/src/components/context_menu/context_menu.tsx +++ b/src/components/context_menu/context_menu.tsx @@ -12,6 +12,7 @@ import React, { CSSProperties, ReactElement, ReactNode, + Fragment, } from 'react'; import classNames from 'classnames'; @@ -47,9 +48,21 @@ export interface EuiContextMenuPanelItemSeparator key?: string; } +export type EuiContextMenuPanelItemRenderCustom = { + /** + * Allows rendering any custom content alongside your array of context menu items. + * Accepts either a component or an inline function component that returns any JSX. + */ + renderItem: (() => ReactNode) | Function; + key?: string; +}; + export type EuiContextMenuPanelItemDescriptor = ExclusiveUnion< - EuiContextMenuPanelItemDescriptorEntry, - EuiContextMenuPanelItemSeparator + ExclusiveUnion< + EuiContextMenuPanelItemDescriptorEntry, + EuiContextMenuPanelItemSeparator + >, + EuiContextMenuPanelItemRenderCustom >; export interface EuiContextMenuPanelDescriptor { @@ -313,6 +326,10 @@ export class EuiContextMenuClass extends Component< renderItems(items: EuiContextMenuPanelItemDescriptor[] = []) { return items.map((item, index) => { + if (item.renderItem) { + return {item.renderItem()}; + } + if (isItemSeparator(item)) { const { isSeparator: omit, key = index, ...rest } = item; return ;