Skip to content

Commit

Permalink
[EuiContextMenu] Support new renderItem custom content in items a…
Browse files Browse the repository at this point in the history
…rrays (#7510)
  • Loading branch information
cee-chen authored Feb 7, 2024
1 parent 239dc72 commit b2687f2
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 9 deletions.
1 change: 1 addition & 0 deletions changelogs/upcoming/7510.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Updated `EuiContextMenu` with a new `panels.items.renderItem` property, which allows rendering completely custom items next to standard `EuiContextMenuItem` objects
14 changes: 13 additions & 1 deletion src-docs/src/views/context_menu/context_menu_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
EuiContextMenu,
EuiContextMenuItem,
EuiContextMenuPanel,
EuiTitle,
} from '../../../../src/components';
import { EuiContextMenuPanelDescriptor } from '!!prop-loader!../../../../src/components/context_menu/context_menu';

Expand Down Expand Up @@ -173,6 +174,9 @@ export const ContextMenuExample = {
],
text: (
<>
<EuiTitle>
<h3>Custom panels</h3>
</EuiTitle>
<p>
Context menu panels can be passed React elements through the{' '}
<EuiCode>content</EuiCode> prop instead of <EuiCode>items</EuiCode>.
Expand All @@ -184,16 +188,24 @@ export const ContextMenuExample = {
<EuiCode language="ts">width: [number of pixels]</EuiCode> to the
panel tree.
</p>
<EuiTitle>
<h3>Custom items</h3>
</EuiTitle>
<p>
You can add separator lines in the <EuiCode>items</EuiCode> prop if
you define an item as{' '}
<EuiCode language="ts">{'{isSeparator: true}'}</EuiCode>. This will
pass the rest of its fields as props to a{' '}
pass the rest of the object properties to an{' '}
<Link to="/layout/horizontal-rule">
<strong>EuiHorizontalRule</strong>
</Link>{' '}
component.
</p>
<p>
For completely custom rendered items, you can use the{' '}
<EuiCode>{'{renderItem}'}</EuiCode> property to pass a component or
any function that returns a JSX node.
</p>
</>
),
demo: <ContextMenuWithContent />,
Expand Down
22 changes: 16 additions & 6 deletions src-docs/src/views/context_menu/context_menu_with_content.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
EuiContextMenu,
EuiIcon,
EuiPopover,
EuiPopoverFooter,
EuiSpacer,
EuiText,
} from '../../../../src/components';
Expand Down Expand Up @@ -64,10 +65,6 @@ export default () => {
icon: <EuiIcon type="search" size="m" />,
onClick: closePopover,
},
{
isSeparator: true,
key: 'sep',
},
{
name: 'See more',
icon: 'plusInCircle',
Expand All @@ -78,6 +75,19 @@ export default () => {
content: <Content />,
},
},
{
isSeparator: true,
key: 'sep',
},
{
renderItem: () => (
<EuiPopoverFooter paddingSize="s">
<EuiButton size="s" style={{ marginInlineStart: 'auto' }}>
I'm a custom item!
</EuiButton>
</EuiPopoverFooter>
),
},
],
});
};
Expand Down Expand Up @@ -116,7 +126,7 @@ export default () => {
);

return (
<React.Fragment>
<>
<EuiPopover
id={normalContextMenuPopoverId}
button={button}
Expand All @@ -140,6 +150,6 @@ export default () => {
>
<EuiContextMenu initialPanelId={0} panels={dynamicPanels} />
</EuiPopover>
</React.Fragment>
</>
);
};
18 changes: 18 additions & 0 deletions src/components/context_menu/context_menu.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -95,6 +96,23 @@ const panels: EuiContextMenuProps['panels'] = [
icon: 'user',
panel: 2,
},
{
isSeparator: true,
},
{
renderItem: () => (
<EuiTitle
size="xxs"
css={({ euiTheme }) => ({
marginInline: euiTheme.size.s,
marginBlockStart: euiTheme.size.m,
marginBlockEnd: euiTheme.size.xs,
})}
>
<h3>Custom rendered subtitle</h3>
</EuiTitle>
),
},
{
name: 'Permalinks',
icon: 'user',
Expand Down
36 changes: 36 additions & 0 deletions src/components/context_menu/context_menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,42 @@ describe('EuiContextMenu', () => {
expect(container.firstChild).toMatchSnapshot();
});

it('allows wildcard content via the `renderItem` prop', () => {
const CustomComponent = () => (
<div data-test-subj="custom">Hello world</div>
);

const { container, getByTestSubject } = render(
<EuiContextMenu
panels={[
{
id: 1,
title: 'Testing renderItem',
items: [
{
name: 'Renders an EuiContextMenuItem',
panel: 2,
},
{
renderItem: () => <h3 data-test-subj="subtitle">Subtitle</h3>,
},
{
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', () => {
Expand Down
21 changes: 19 additions & 2 deletions src/components/context_menu/context_menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import React, {
CSSProperties,
ReactElement,
ReactNode,
Fragment,
} from 'react';
import classNames from 'classnames';

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -313,6 +326,10 @@ export class EuiContextMenuClass extends Component<

renderItems(items: EuiContextMenuPanelItemDescriptor[] = []) {
return items.map((item, index) => {
if (item.renderItem) {
return <Fragment key={item.key ?? index}>{item.renderItem()}</Fragment>;
}

if (isItemSeparator(item)) {
const { isSeparator: omit, key = index, ...rest } = item;
return <EuiHorizontalRule key={key} margin="none" {...rest} />;
Expand Down

0 comments on commit b2687f2

Please sign in to comment.