diff --git a/CHANGELOG.md b/CHANGELOG.md index 8064c13efc1..10fdbbb4be8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Added `boolean` type to the `notification` prop of `EuiHeaderSectionItemButton` to show a simple dot ([#4008](https://github.com/elastic/eui/pull/4008)) - Added `popoverButton` and `popoverButtonBreakpoints` props to `EuiSelectableTemplateSitewide` for responsive capabilities ([#4008](https://github.com/elastic/eui/pull/4008)) - Added `isWithinMaxBreakpoint` service ([#4008](https://github.com/elastic/eui/pull/4008)) +- Added horizontal line separator to `EuiContextMenu` ([#4018](https://github.com/elastic/eui/pull/4018)) **Bug fixes** 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 744dc0a1ca6..3426bde4315 100644 --- a/src-docs/src/views/context_menu/context_menu_example.js +++ b/src-docs/src/views/context_menu/context_menu_example.js @@ -8,8 +8,8 @@ import { GuideSectionTypes } from '../../components'; import { EuiCode, EuiContextMenu, - EuiContextMenuPanel, EuiContextMenuItem, + EuiContextMenuPanel, } from '../../../../src/components'; import ContextMenu from './context_menu'; @@ -122,6 +122,16 @@ export const ContextMenuExample = { width: [number of pixels] to the panel tree.

+

+ 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{' '} + + EuiHorizontalRule + {' '} + component. +

), 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 1239037c8a7..1212fb59ec8 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 @@ -59,6 +59,10 @@ export default () => { window.alert('Show fullscreen'); }, }, + { + isSeparator: true, + key: 'sep', + }, { name: 'See more', icon: 'plusInCircle', diff --git a/src/components/context_menu/__snapshots__/context_menu.test.tsx.snap b/src/components/context_menu/__snapshots__/context_menu.test.tsx.snap index 85a3236d7bb..170e66b1299 100644 --- a/src/components/context_menu/__snapshots__/context_menu.test.tsx.snap +++ b/src/components/context_menu/__snapshots__/context_menu.test.tsx.snap @@ -1,5 +1,33 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`EuiContextMenu can pass-through horizontal rule props 1`] = ` +
+
+
+ + Testing separator + +
+
+
+
+
+
+
+
+`; + exports[`EuiContextMenu is rendered 1`] = `
`; +exports[`EuiContextMenu panel item can be a separator line 1`] = ` +
+
+
+ + Testing separator + +
+
+
+ +
+ +
+
+
+
+`; + exports[`EuiContextMenu panel item can contain JSX 1`] = `
+
+
+ + Testing separator + +
+
+
+
+
+
+
+
+`; + exports[`EuiContextMenu is rendered 1`] = `
`; +exports[`EuiContextMenu panel item can be a separator line 1`] = ` +
+
+
+ + Testing separator + +
+
+
+ +
+ +
+
+
+
+`; + exports[`EuiContextMenu panel item can contain JSX 1`] = `
{ expect(component).toMatchSnapshot(); }); + it('panel item can be a separator line', () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); + + it('can pass-through horizontal rule props', () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); + 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 c51fe3fc8d6..fa87b807ce2 100644 --- a/src/components/context_menu/context_menu.tsx +++ b/src/components/context_menu/context_menu.tsx @@ -25,7 +25,7 @@ import React, { } from 'react'; import classNames from 'classnames'; -import { CommonProps } from '../common'; +import { CommonProps, ExclusiveUnion } from '../common'; import { EuiContextMenuPanel, EuiContextMenuPanelTransitionDirection, @@ -35,10 +35,11 @@ import { EuiContextMenuItem, EuiContextMenuItemProps, } from './context_menu_item'; +import { EuiHorizontalRule, EuiHorizontalRuleProps } from '../horizontal_rule'; export type EuiContextMenuPanelId = string | number; -export type EuiContextMenuPanelItemDescriptor = Omit< +export type EuiContextMenuPanelItemDescriptorEntry = Omit< EuiContextMenuItemProps, 'hasPanel' > & { @@ -47,6 +48,17 @@ export type EuiContextMenuPanelItemDescriptor = Omit< panel?: EuiContextMenuPanelId; }; +export interface EuiContextMenuPanelItemSeparator + extends EuiHorizontalRuleProps { + isSeparator: true; + key?: string; +} + +export type EuiContextMenuPanelItemDescriptor = ExclusiveUnion< + EuiContextMenuPanelItemDescriptorEntry, + EuiContextMenuPanelItemSeparator +>; + export interface EuiContextMenuPanelDescriptor { id: EuiContextMenuPanelId; title?: string; @@ -61,6 +73,11 @@ export type EuiContextMenuProps = CommonProps & initialPanelId?: EuiContextMenuPanelId; }; +const isItemSeparator = ( + item: EuiContextMenuPanelItemDescriptor +): item is EuiContextMenuPanelItemSeparator => + (item as EuiContextMenuPanelItemSeparator).isSeparator === true; + function mapIdsToPanels(panels: EuiContextMenuPanelDescriptor[]) { const map: { [id: string]: EuiContextMenuPanelDescriptor } = {}; @@ -77,6 +94,7 @@ function mapIdsToPreviousPanels(panels: EuiContextMenuPanelDescriptor[]) { panels.forEach(panel => { if (Array.isArray(panel.items)) { panel.items.forEach(item => { + if (isItemSeparator(item)) return; const isCloseable = item.panel !== undefined; if (isCloseable) { idToPreviousPanelIdMap[item.panel!] = panel.id; @@ -98,6 +116,7 @@ function mapPanelItemsToPanels(panels: EuiContextMenuPanelDescriptor[]) { if (panel.items) { panel.items.forEach((item, index) => { + if (isItemSeparator(item)) return; if (item.panel) { idAndItemIndexToPanelIdMap[panel.id][index] = item.panel; } @@ -227,7 +246,8 @@ export class EuiContextMenu extends Component { // Set focus on the item which shows the panel we're leaving. const previousPanel = this.state.idToPanelMap[previousPanelId]; const focusedItemIndex = previousPanel.items!.findIndex( - item => item.panel === this.state.incomingPanelId + item => + !isItemSeparator(item) && item.panel === this.state.incomingPanelId ); if (focusedItemIndex !== -1) { @@ -277,6 +297,11 @@ export class EuiContextMenu extends Component { renderItems(items: EuiContextMenuPanelItemDescriptor[] = []) { return items.map((item, index) => { + if (isItemSeparator(item)) { + const { isSeparator: omit, key = index, ...rest } = item; + return ; + } + const { panel, name, diff --git a/src/components/context_menu/context_menu_item.tsx b/src/components/context_menu/context_menu_item.tsx index ef45779ce76..a3e2d457fe7 100644 --- a/src/components/context_menu/context_menu_item.tsx +++ b/src/components/context_menu/context_menu_item.tsx @@ -100,7 +100,6 @@ export class EuiContextMenuItem extends Component { rel, ...rest } = this.props; - let iconInstance; if (icon) { diff --git a/src/components/context_menu/context_menu_panel.tsx b/src/components/context_menu/context_menu_panel.tsx index c709d62a248..6961aec546b 100644 --- a/src/components/context_menu/context_menu_panel.tsx +++ b/src/components/context_menu/context_menu_panel.tsx @@ -32,6 +32,7 @@ import { EuiIcon } from '../icon'; import { EuiPopoverTitle } from '../popover'; import { EuiResizeObserver } from '../observer/resize_observer'; import { cascadingMenuKeys } from '../../services'; +import { EuiContextMenuItem } from './context_menu_item'; export type EuiContextMenuPanelHeightChangeHandler = (height: number) => void; export type EuiContextMenuPanelTransitionType = 'in' | 'out'; @@ -471,9 +472,11 @@ export class EuiContextMenuPanel extends Component { const content = items && items.length ? items.map((MenuItem, index) => - cloneElement(MenuItem, { - buttonRef: this.menuItemRef.bind(this, index), - }) + MenuItem.type === EuiContextMenuItem + ? cloneElement(MenuItem, { + buttonRef: this.menuItemRef.bind(this, index), + }) + : MenuItem ) : children; diff --git a/src/components/horizontal_rule/horizontal_rule.tsx b/src/components/horizontal_rule/horizontal_rule.tsx index c9bf997db9b..f3b571acb04 100644 --- a/src/components/horizontal_rule/horizontal_rule.tsx +++ b/src/components/horizontal_rule/horizontal_rule.tsx @@ -25,7 +25,9 @@ import { CommonProps } from '../common'; export type EuiHorizontalRuleSize = keyof typeof sizeToClassNameMap; export type EuiHorizontalRuleMargin = keyof typeof marginToClassNameMap; -export interface EuiHorizontalRuleProps { +export interface EuiHorizontalRuleProps + extends CommonProps, + HTMLAttributes { /** * Defines the width of the HR. */ @@ -53,9 +55,7 @@ const marginToClassNameMap = { export const MARGINS = Object.keys(marginToClassNameMap); -export const EuiHorizontalRule: FunctionComponent & - EuiHorizontalRuleProps> = ({ +export const EuiHorizontalRule: FunctionComponent = ({ className, size = 'full', margin = 'l', diff --git a/src/components/horizontal_rule/index.ts b/src/components/horizontal_rule/index.ts index a4169736e69..5707020475b 100644 --- a/src/components/horizontal_rule/index.ts +++ b/src/components/horizontal_rule/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { EuiHorizontalRule } from './horizontal_rule'; +export { EuiHorizontalRule, EuiHorizontalRuleProps } from './horizontal_rule';