diff --git a/src-docs/src/views/bottom_bar/bottom_bar.js b/src-docs/src/views/bottom_bar/bottom_bar.js index c0317d4e97e..4598e151e81 100644 --- a/src-docs/src/views/bottom_bar/bottom_bar.js +++ b/src-docs/src/views/bottom_bar/bottom_bar.js @@ -6,10 +6,15 @@ import { EuiFlexItem, EuiButton, EuiButtonEmpty, + EuiPopover, + EuiContextMenuPanel, + EuiContextMenuItem, } from '../../../../src/components'; export default () => { const [showBar, setShowBar] = useState(false); + const [showBarPopover, setShowBarPopover] = useState(false); + const closePopover = () => setShowBarPopover(false); const button = ( setShowBar((show) => !show)}> @@ -25,9 +30,33 @@ export default () => { - - Help - + setShowBarPopover(!showBarPopover)} + > + Help + + } + panelPaddingSize="none" + repositionOnScroll + > + + Link A + , + + Link B + , + ]} + /> + diff --git a/src-docs/src/views/context_menu/content_panel.tsx b/src-docs/src/views/context_menu/content_panel.tsx index cf0bd66f49e..c338be7ba69 100644 --- a/src-docs/src/views/context_menu/content_panel.tsx +++ b/src-docs/src/views/context_menu/content_panel.tsx @@ -65,10 +65,20 @@ export default () => { anchorPosition="downLeft" > - + Add a field to this data view - + Manage this data view diff --git a/src-docs/src/views/context_menu/context_menu.js b/src-docs/src/views/context_menu/context_menu.js index 38964c9bb5c..b6460eb09e8 100644 --- a/src-docs/src/views/context_menu/context_menu.js +++ b/src-docs/src/views/context_menu/context_menu.js @@ -43,9 +43,7 @@ export default () => { { name: 'Handle an onClick', icon: 'search', - onClick: () => { - closePopover(); - }, + onClick: closePopover, }, { name: 'Go to a link', @@ -64,17 +62,17 @@ export default () => { toolTipTitle: 'Optional tooltip', toolTipContent: 'Optional content for a tooltip', toolTipPosition: 'right', - onClick: () => { - closePopover(); - }, + onClick: closePopover, }, { name: 'Use an app icon', icon: 'visualizeApp', + onClick: closePopover, }, { name: 'Pass an icon as a component to customize it', icon: , + onClick: closePopover, }, { name: 'Disabled option', @@ -82,9 +80,7 @@ export default () => { toolTipContent: 'For reasons, this item is disabled', toolTipPosition: 'right', disabled: true, - onClick: () => { - closePopover(); - }, + onClick: closePopover, }, ], }, @@ -96,9 +92,7 @@ export default () => { { name: 'PDF reports', icon: 'user', - onClick: () => { - closePopover(); - }, + onClick: closePopover, }, { name: 'Embed code', @@ -108,9 +102,7 @@ export default () => { { name: 'Permalinks', icon: 'user', - onClick: () => { - closePopover(); - }, + onClick: closePopover, }, ], }, 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 db18ad83718..0cc869e2f69 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 @@ -62,9 +62,7 @@ export default () => { { name: 'Show fullscreen', icon: , - onClick: () => { - closePopover(); - }, + onClick: closePopover, }, { isSeparator: true, diff --git a/src/components/basic_table/__snapshots__/collapsed_item_actions.test.tsx.snap b/src/components/basic_table/__snapshots__/collapsed_item_actions.test.tsx.snap index bfa5ecba192..6fbdfc5735a 100644 --- a/src/components/basic_table/__snapshots__/collapsed_item_actions.test.tsx.snap +++ b/src/components/basic_table/__snapshots__/collapsed_item_actions.test.tsx.snap @@ -50,7 +50,7 @@ exports[`CollapsedItemActions render with href and _target provided 1`] = ` popoverRef={[Function]} repositionToCrossAxis={true} > -
Testing separator @@ -27,137 +28,29 @@ exports[`EuiContextMenu can pass-through horizontal rule props 1`] = `
`; -exports[`EuiContextMenu is rendered 1`] = ` +exports[`EuiContextMenu props panels and initialPanelId navigates back to the previous panel when clicking the title button 1`] = `
-`; - -exports[`EuiContextMenu panel item can be a separator line 1`] = ` -
-
-
- - Testing separator - -
-
- -
- -
-
-
-`; - -exports[`EuiContextMenu panel item can contain JSX 1`] = ` -
-
- - 3 - -
-
- -
-
-
-`; - -exports[`EuiContextMenu props panels and initialPanelId allows you to click the title button to go back to the previous panel 1`] = ` -
-
@@ -169,32 +62,29 @@ exports[`EuiContextMenu props panels and initialPanelId allows you to click the
`; -exports[`EuiContextMenu props panels and initialPanelId allows you to click the title button to go back to the previous panel 2`] = ` +exports[`EuiContextMenu props panels and initialPanelId navigates back to the previous panel when clicking the title button 2`] = `
@@ -204,82 +94,67 @@ exports[`EuiContextMenu props panels and initialPanelId allows you to click the
@@ -288,30 +163,27 @@ exports[`EuiContextMenu props panels and initialPanelId allows you to click the exports[`EuiContextMenu props panels and initialPanelId renders the referenced panel 1`] = `
@@ -323,32 +195,29 @@ exports[`EuiContextMenu props panels and initialPanelId renders the referenced p
`; -exports[`EuiContextMenu props size m is rendered 1`] = ` +exports[`EuiContextMenu props size m 1`] = `
@@ -360,37 +229,126 @@ exports[`EuiContextMenu props size m is rendered 1`] = `
`; -exports[`EuiContextMenu props size s is rendered 1`] = ` +exports[`EuiContextMenu props size s 1`] = `
+
+
+ 2 +
+
+
+
+`; + +exports[`EuiContextMenu renders 1`] = ` +
+`; + +exports[`EuiContextMenu renders isSeparator items 1`] = ` +
+
+
+ + Testing separator + +
+
+
+ class="euiContextMenuItem__text emotion-euiContextMenuItem__text" + > + Foo + +
+
+
- 2 + Bar +
+
+
+
+`; + +exports[`EuiContextMenu renders panels with JSX 1`] = ` +
+
+
+ + 3 - +
-
- 2 +
+ + + foo + +
diff --git a/src/components/context_menu/__snapshots__/context_menu_item.test.tsx.snap b/src/components/context_menu/__snapshots__/context_menu_item.test.tsx.snap index edcbc65f47c..4901078ca83 100644 --- a/src/components/context_menu/__snapshots__/context_menu_item.test.tsx.snap +++ b/src/components/context_menu/__snapshots__/context_menu_item.test.tsx.snap @@ -1,175 +1,62 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`EuiContextMenuItem is rendered 1`] = ` +exports[`EuiContextMenuItem props disabled 1`] = ` -`; - -exports[`EuiContextMenuItem props disabled is rendered 1`] = ` - `; -exports[`EuiContextMenuItem props hasPanel is rendered 1`] = ` - -`; - -exports[`EuiContextMenuItem props href renders a link 1`] = ` - + class="euiIcon fa-user emotion-euiContextMenu__icon" + /> - - - + class="euiContextMenuItem__text emotion-euiContextMenuItem__text" + /> +
`; -exports[`EuiContextMenuItem props icon is rendered 1`] = ` - + class="euiContextMenuItem__text emotion-euiContextMenuItem__text" + /> +
`; -exports[`EuiContextMenuItem props onClick renders a button 1`] = ` - + class="euiContextMenuItem__text emotion-euiContextMenuItem__text-s" + /> +
`; -exports[`EuiContextMenuItem props rel is rendered 1`] = ` +exports[`EuiContextMenuItem renders 1`] = ` - - - - -`; - -exports[`EuiContextMenuItem props size m is rendered 1`] = ` - -`; - -exports[`EuiContextMenuItem props size s is rendered 1`] = ` - -`; - -exports[`EuiContextMenuItem props target is rendered 1`] = ` - - + Hello `; diff --git a/src/components/context_menu/__snapshots__/context_menu_panel.test.tsx.snap b/src/components/context_menu/__snapshots__/context_menu_panel.test.tsx.snap index 9df1a570faa..d790a6b6e4a 100644 --- a/src/components/context_menu/__snapshots__/context_menu_panel.test.tsx.snap +++ b/src/components/context_menu/__snapshots__/context_menu_panel.test.tsx.snap @@ -1,56 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`EuiContextMenuPanel is rendered 1`] = ` +exports[`EuiContextMenuPanel props size m 1`] = `
-
- Hello -
-
-`; - -exports[`EuiContextMenuPanel props onClose renders a button as a title 1`] = ` -
- -
-
-`; - -exports[`EuiContextMenuPanel props size m is rendered 1`] = ` -
Title @@ -59,16 +19,17 @@ exports[`EuiContextMenuPanel props size m is rendered 1`] = `
`; -exports[`EuiContextMenuPanel props size s is rendered 1`] = ` +exports[`EuiContextMenuPanel props size s 1`] = `
Title @@ -77,16 +38,17 @@ exports[`EuiContextMenuPanel props size s is rendered 1`] = `
`; -exports[`EuiContextMenuPanel props title is rendered 1`] = ` +exports[`EuiContextMenuPanel props title 1`] = `
Title @@ -95,38 +57,15 @@ exports[`EuiContextMenuPanel props title is rendered 1`] = `
`; -exports[`EuiContextMenuPanel props transitionDirection next with transitionType in is rendered 1`] = ` +exports[`EuiContextMenuPanel renders 1`] = `
-
-
-`; - -exports[`EuiContextMenuPanel props transitionDirection next with transitionType out is rendered 1`] = ` -
-
-
-`; - -exports[`EuiContextMenuPanel props transitionDirection previous with transitionType in is rendered 1`] = ` -
-
-
-`; - -exports[`EuiContextMenuPanel props transitionDirection previous with transitionType out is rendered 1`] = ` -
-
+
+ Hello +
`; diff --git a/src/components/context_menu/_context_menu.scss b/src/components/context_menu/_context_menu.scss deleted file mode 100644 index 3e2f4454ff1..00000000000 --- a/src/components/context_menu/_context_menu.scss +++ /dev/null @@ -1,27 +0,0 @@ -$euiContextMenuWidth: $euiSize * 16; - -.euiContextMenu { - width: $euiContextMenuWidth; - max-width: 100%; - position: relative; - overflow: hidden; - transition: height $euiAnimSpeedFast $euiAnimSlightResistance; - border-radius: $euiBorderRadius; - - .euiContextMenu__content { - padding: $euiSizeS; - } -} - -/** - * 1. When there are multiple ContextMenuPanels, the ContextMenu will absolutely - * position them. ContextMenuPanel will break the layout of a Popover if it's - * absolutely positioned by default. - */ -.euiContextMenu__panel { - position: absolute; /* 1 */ -} - -.euiContextMenu__icon { - margin-right: $euiSizeS; -} diff --git a/src/components/context_menu/_context_menu_item.scss b/src/components/context_menu/_context_menu_item.scss deleted file mode 100644 index 018dffb5db2..00000000000 --- a/src/components/context_menu/_context_menu_item.scss +++ /dev/null @@ -1,65 +0,0 @@ -.euiContextMenuItem { - display: block; - padding: $euiSizeM; - width: 100%; - text-align: left; - color: $euiTextColor; - outline-offset: -$euiFocusRingSize; - - &:hover, - &:focus { - text-decoration: underline; - } - - &:focus { - background-color: $euiFocusBackgroundColor; - } - - &.euiContextMenuItem-isDisabled { - color: $euiButtonColorDisabledText; - cursor: default; - - &:hover, - &:focus { - text-decoration: none; - } - } - - &--small { - padding: ($euiSizeS * .75) $euiSizeS; - - .euiContextMenuItem__text { - @include euiFontSizeS; - } - } -} - -.euiContextMenuItem__inner { - display: flex; -} - -.euiContextMenuItem__text { - flex-grow: 1; - overflow: hidden; // allows for text truncation -} - -.euiContextMenuItem__arrow { - align-self: flex-end; -} - -.euiContextMenu__itemLayout { - display: flex; - align-items: center; - - &.euiContextMenu__itemLayout--bottom { - align-items: flex-end; - } - - &.euiContextMenu__itemLayout--top { - align-items: flex-start; - } - - .euiContextMenu__icon { - flex-shrink: 0; - } -} diff --git a/src/components/context_menu/_context_menu_panel.scss b/src/components/context_menu/_context_menu_panel.scss deleted file mode 100644 index 7f21ff1e355..00000000000 --- a/src/components/context_menu/_context_menu_panel.scss +++ /dev/null @@ -1,101 +0,0 @@ -.euiContextMenuPanel { - width: 100%; - visibility: visible; - outline-offset: -$euiFocusRingSize; - - &:focus { - outline: none; // Hide focus ring because of `tabindex=-1` on Safari - } - - &.euiContextMenuPanel-txInLeft { - pointer-events: none; - animation: euiContextMenuPanelTxInLeft $euiAnimSpeedNormal $euiAnimSlightResistance; - animation-fill-mode: forwards; - } - - &.euiContextMenuPanel-txOutLeft { - pointer-events: none; - animation: euiContextMenuPanelTxOutLeft $euiAnimSpeedNormal $euiAnimSlightResistance; - animation-fill-mode: forwards; - } - - &.euiContextMenuPanel-txInRight { - pointer-events: none; - animation: euiContextMenuPanelTxInRight $euiAnimSpeedNormal $euiAnimSlightResistance; - animation-fill-mode: forwards; - } - - &.euiContextMenuPanel-txOutRight { - pointer-events: none; - animation: euiContextMenuPanelTxOutRight $euiAnimSpeedNormal $euiAnimSlightResistance; - animation-fill-mode: forwards; - } -} - -.euiContextMenuPanel--next { - transform: translateX($euiContextMenuWidth); - visibility: hidden; -} - -.euiContextMenuPanel--previous { - transform: translateX(-$euiContextMenuWidth); - visibility: hidden; -} - -.euiContextMenuPanelTitle { - @include euiTitle('xxs'); - padding: $euiSizeM; - width: 100%; - text-align: left; - outline-offset: -$euiFocusRingSize; - border-bottom: $euiBorderThin; - - &:enabled:hover, - &:enabled:focus { - text-decoration: underline; - } - - &--small { - padding: ($euiSizeS * .75) $euiSizeS; - } -} - -@keyframes euiContextMenuPanelTxInLeft { - 0% { - transform: translateX($euiContextMenuWidth); - } - - 100% { - transform: translateX(0); - } -} - -@keyframes euiContextMenuPanelTxOutLeft { - 0% { - transform: translateX(0); - } - - 100% { - transform: translateX(-$euiContextMenuWidth); - } -} - -@keyframes euiContextMenuPanelTxInRight { - 0% { - transform: translateX(-$euiContextMenuWidth); - } - - 100% { - transform: translateX(0); - } -} - -@keyframes euiContextMenuPanelTxOutRight { - 0% { - transform: translateX(0); - } - - 100% { - transform: translateX($euiContextMenuWidth); - } -} diff --git a/src/components/context_menu/_index.scss b/src/components/context_menu/_index.scss deleted file mode 100644 index aaa11e331f2..00000000000 --- a/src/components/context_menu/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import 'context_menu'; -@import 'context_menu_panel'; -@import 'context_menu_item'; diff --git a/src/components/context_menu/context_menu.styles.ts b/src/components/context_menu/context_menu.styles.ts new file mode 100644 index 00000000000..daad2f44f79 --- /dev/null +++ b/src/components/context_menu/context_menu.styles.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { css } from '@emotion/react'; + +import { UseEuiTheme } from '../../services'; +import { logicalCSS, mathWithUnits, euiCanAnimate } from '../../global_styling'; + +export const euiContextMenuVariables = ({ euiTheme }: UseEuiTheme) => { + return { + panelWidth: mathWithUnits(euiTheme.size.base, (x) => x * 16), + }; +}; + +export const euiContextMenuStyles = (euiThemeContext: UseEuiTheme) => { + const { euiTheme } = euiThemeContext; + const { panelWidth } = euiContextMenuVariables(euiThemeContext); + + return { + euiContextMenu: css` + ${logicalCSS('width', panelWidth)} + ${logicalCSS('max-width', '100%')} + position: relative; + overflow: hidden; + border-radius: ${euiTheme.border.radius.medium}; + + ${euiCanAnimate} { + transition: height ${euiTheme.animation.fast} + ${euiTheme.animation.resistance}; + } + `, + }; +}; diff --git a/src/components/context_menu/context_menu.test.tsx b/src/components/context_menu/context_menu.test.tsx index 23cd068304d..b3627cd7fac 100644 --- a/src/components/context_menu/context_menu.test.tsx +++ b/src/components/context_menu/context_menu.test.tsx @@ -7,10 +7,10 @@ */ import React from 'react'; -import { mount } from 'enzyme'; -import { act } from '@testing-library/react'; -import { requiredProps, takeMountedSnapshot } from '../../test'; +import { act, fireEvent } from '@testing-library/react'; import { render } from '../../test/rtl'; +import { shouldRenderCustomStyles } from '../../test/internal'; +import { requiredProps } from '../../test'; import { EuiContextMenu, SIZES } from './context_menu'; @@ -68,13 +68,15 @@ export const tick = (ms = 0) => act(() => new Promise((resolve) => setTimeout(resolve, ms))); describe('EuiContextMenu', () => { - test('is rendered', () => { + shouldRenderCustomStyles(); + + it('renders', () => { const { container } = render(); expect(container.firstChild).toMatchSnapshot(); }); - it('panel item can contain JSX', () => { + it('renders panels with JSX', () => { const { container } = render( ); @@ -82,7 +84,7 @@ describe('EuiContextMenu', () => { expect(container.firstChild).toMatchSnapshot(); }); - it('panel item can be a separator line', () => { + it('renders isSeparator items', () => { const { container } = render( { expect(container.firstChild).toMatchSnapshot(); }); - it('allows you to click the title button to go back to the previous panel', async () => { + it('navigates back to the previous panel when clicking the title button', async () => { const onPanelChange = jest.fn(); - const component = mount( + const { container, getByTestSubject } = render( { await tick(20); - expect(takeMountedSnapshot(component)).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); // Navigate to a different panel. - component - .find('[data-test-subj="contextMenuPanelTitleButton"]') - .simulate('click'); + fireEvent.click(getByTestSubject('contextMenuPanelTitleButton')); await tick(20); - expect(takeMountedSnapshot(component)).toMatchSnapshot(); + expect(container.firstChild).toMatchSnapshot(); expect(onPanelChange).toHaveBeenCalledWith({ panelId: 1, direction: 'previous', @@ -168,7 +168,7 @@ describe('EuiContextMenu', () => { describe('size', () => { SIZES.forEach((size) => { - it(`${size} is rendered`, () => { + test(size, () => { const { container } = render( ); diff --git a/src/components/context_menu/context_menu.tsx b/src/components/context_menu/context_menu.tsx index 141dec0cb7f..68faaa8a1f8 100644 --- a/src/components/context_menu/context_menu.tsx +++ b/src/components/context_menu/context_menu.tsx @@ -15,7 +15,10 @@ import React, { } from 'react'; import classNames from 'classnames'; -import { CommonProps, ExclusiveUnion, keysOf } from '../common'; +import { withEuiTheme, WithEuiThemeProps } from '../../services'; +import { CommonProps, ExclusiveUnion } from '../common'; +import { EuiHorizontalRule, EuiHorizontalRuleProps } from '../horizontal_rule'; + import { EuiContextMenuPanel, EuiContextMenuPanelTransitionDirection, @@ -25,7 +28,7 @@ import { EuiContextMenuItem, EuiContextMenuItemProps, } from './context_menu_item'; -import { EuiHorizontalRule, EuiHorizontalRuleProps } from '../horizontal_rule'; +import { euiContextMenuStyles } from './context_menu.styles'; export type EuiContextMenuPanelId = string | number; @@ -59,15 +62,10 @@ export interface EuiContextMenuPanelDescriptor { /** * Alters the size of the items and the title */ - size?: keyof typeof sizeToClassNameMap; + size?: (typeof SIZES)[number]; } -const sizeToClassNameMap = { - s: 'euiContextMenu--small', - m: null, -}; - -export const SIZES = keysOf(sizeToClassNameMap); +export const SIZES = ['s', 'm'] as const; export type EuiContextMenuProps = CommonProps & Omit, 'style'> & { @@ -84,7 +82,7 @@ export type EuiContextMenuProps = CommonProps & /** * Alters the size of the items and the title */ - size?: keyof typeof sizeToClassNameMap; + size?: (typeof SIZES)[number]; }; const isItemSeparator = ( @@ -161,7 +159,10 @@ interface State { isUsingKeyboardToNavigate: boolean; } -export class EuiContextMenu extends Component { +export class EuiContextMenuClass extends Component< + WithEuiThemeProps & EuiContextMenuProps, + State +> { static defaultProps: Partial = { panels: [], size: 'm', @@ -185,7 +186,7 @@ export class EuiContextMenu extends Component { return null; } - constructor(props: EuiContextMenuProps) { + constructor(props: WithEuiThemeProps & EuiContextMenuProps) { super(props); this.state = { @@ -377,11 +378,16 @@ export class EuiContextMenu extends Component { onClose = () => window.requestAnimationFrame(this.showPreviousPanel); } + const cssStyles = { + position: 'absolute' as const, + label: 'euiContextMenu__panel', + }; + return ( { } render() { - const { panels, onPanelChange, className, initialPanelId, size, ...rest } = - this.props; + const { + theme, + panels, + onPanelChange, + className, + initialPanelId, + size, + ...rest + } = this.props; const incomingPanel = this.renderPanel(this.state.incomingPanelId!, 'in'); let outgoingPanel; @@ -432,14 +445,14 @@ export class EuiContextMenu extends Component { ? this.state.idToPanelMap[this.state.incomingPanelId!].width : undefined; - const classes = classNames( - 'euiContextMenu', - size && sizeToClassNameMap[size], - className - ); + const classes = classNames('euiContextMenu', className); + + const styles = euiContextMenuStyles(theme); + const cssStyles = [styles.euiContextMenu]; return (
{ ); } } + +export const EuiContextMenu = + withEuiTheme(EuiContextMenuClass); diff --git a/src/components/context_menu/context_menu_item.styles.ts b/src/components/context_menu/context_menu_item.styles.ts new file mode 100644 index 00000000000..7f83f0b6bbc --- /dev/null +++ b/src/components/context_menu/context_menu_item.styles.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { css } from '@emotion/react'; + +import { UseEuiTheme } from '../../services'; +import { + logicalCSS, + logicalTextAlignCSS, + euiFontSize, +} from '../../global_styling'; + +export const euiContextMenuItemStyles = (euiThemeContext: UseEuiTheme) => { + const { euiTheme } = euiThemeContext; + + return { + euiContextMenuItem: css` + display: flex; + gap: ${euiTheme.size.s}; + ${logicalCSS('width', '100%')} + ${logicalTextAlignCSS('left')} + color: ${euiTheme.colors.text}; + outline-offset: -${euiTheme.focus.width}; + + &:enabled:hover, + &:enabled:focus { + text-decoration: underline; + } + + &:enabled:focus { + background-color: ${euiTheme.focus.backgroundColor}; + } + `, + disabled: css` + color: ${euiTheme.colors.disabledText}; + cursor: default; + `, + layoutAlign: { + center: css` + align-items: center; + `, + top: css` + align-items: flex-start; + `, + bottom: css` + align-items: flex-end; + `, + }, + sizes: { + m: css` + padding: ${euiTheme.size.m}; + `, + s: css` + padding: ${euiTheme.size.s}; + `, + }, + // Children + euiContextMenu__icon: css` + flex-shrink: 0; + `, + text: { + euiContextMenuItem__text: css` + flex-grow: 1; + overflow: hidden; /* allows for text truncation */ + `, + s: css` + ${euiFontSize(euiThemeContext, 's')} + `, + }, + euiContextMenuItem__arrow: css` + align-self: flex-end; + `, + }; +}; diff --git a/src/components/context_menu/context_menu_item.test.tsx b/src/components/context_menu/context_menu_item.test.tsx index bf77c47d294..7ec564cc5e2 100644 --- a/src/components/context_menu/context_menu_item.test.tsx +++ b/src/components/context_menu/context_menu_item.test.tsx @@ -7,43 +7,44 @@ */ import React from 'react'; -import { shallow, mount } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; +import { fireEvent } from '@testing-library/react'; import { render } from '../../test/rtl'; +import { shouldRenderCustomStyles } from '../../test/internal'; +import { requiredProps } from '../../test/required_props'; import { EuiContextMenuItem, SIZES } from './context_menu_item'; describe('EuiContextMenuItem', () => { - test('is rendered', () => { + shouldRenderCustomStyles(); + + it('renders', () => { const { container } = render( - Hello + + Hello + ); expect(container.firstChild).toMatchSnapshot(); }); describe('props', () => { - describe('icon', () => { - test('is rendered', () => { - const { container } = render( - } /> - ); + test('icon', () => { + const { container } = render( + } /> + ); - expect(container.firstChild).toMatchSnapshot(); - }); + expect(container.firstChild).toMatchSnapshot(); }); - describe('disabled', () => { - test('is rendered', () => { - const { container } = render(); + test('disabled', () => { + const { container } = render(); - expect(container.firstChild).toMatchSnapshot(); - }); + expect(container.firstChild).toMatchSnapshot(); }); describe('size', () => { SIZES.forEach((size) => { - it(`${size} is rendered`, () => { + test(size, () => { const { container } = render(); expect(container.firstChild).toMatchSnapshot(); @@ -52,83 +53,72 @@ describe('EuiContextMenuItem', () => { }); describe('onClick', () => { - test('renders a button', () => { + it('renders a button', () => { const { container } = render( {}} /> ); - expect(container.firstChild).toMatchSnapshot(); + expect(container.firstChild?.nodeName).toEqual('BUTTON'); }); - test("isn't called upon instantiation", () => { + it('is called when the item is clicked', () => { const onClickHandler = jest.fn(); - shallow(); - - expect(onClickHandler).not.toHaveBeenCalled(); - }); - - test('is called when the item is clicked', () => { - const onClickHandler = jest.fn(); - - const component = shallow( + const { container } = render( ); + expect(onClickHandler).not.toHaveBeenCalled(); - component.simulate('click'); - + fireEvent.click(container.firstChild!); expect(onClickHandler).toHaveBeenCalledTimes(1); }); - test('is not called when the item is clicked but set to disabled', () => { + it('is not called when the item is clicked but set to disabled', () => { const onClickHandler = jest.fn(); - const component = mount( + const { container } = render( ); - component.simulate('click'); + fireEvent.click(container.firstChild!); expect(onClickHandler).not.toHaveBeenCalled(); }); }); - describe('href', () => { - test('renders a link', () => { - const { container } = render( - - ); + test('href', () => { + const { container } = render( + + ); - expect(container.firstChild).toMatchSnapshot(); - }); + expect(container.firstChild?.nodeName).toEqual('A'); }); - describe('rel', () => { - test('is rendered', () => { - const { container } = render( - - ); + test('rel', () => { + const { container } = render( + + ); - expect(container.firstChild).toMatchSnapshot(); - }); + expect(container.querySelector('a')).toHaveAttribute( + 'rel', + 'help noreferrer' + ); }); - describe('target', () => { - test('is rendered', () => { - const { container } = render( - - ); + test('target', () => { + const { container } = render( + + ); - expect(container.firstChild).toMatchSnapshot(); - }); + expect(container.querySelector('a')).toHaveAttribute('target', '_blank'); }); - describe('hasPanel', () => { - test('is rendered', () => { - const { container } = render(); + test('hasPanel renders a right arrow', () => { + const { container } = render(); - expect(container.firstChild).toMatchSnapshot(); - }); + expect( + container.querySelector('.euiContextMenu__arrow') + ).toBeInTheDocument(); }); }); }); diff --git a/src/components/context_menu/context_menu_item.tsx b/src/components/context_menu/context_menu_item.tsx index ee84229072f..50c6158e213 100644 --- a/src/components/context_menu/context_menu_item.tsx +++ b/src/components/context_menu/context_menu_item.tsx @@ -9,31 +9,31 @@ import React, { AnchorHTMLAttributes, ButtonHTMLAttributes, - cloneElement, - Component, + HTMLAttributes, + FunctionComponent, ReactElement, ReactNode, Ref, } from 'react'; import classNames from 'classnames'; +import { + useEuiTheme, + getSecureRelForTarget, + cloneElementWithCss, +} from '../../services'; +import { validateHref } from '../../services/security/href_validator'; import { CommonProps, keysOf } from '../common'; import { EuiIcon } from '../icon'; import { EuiToolTip, ToolTipPositions } from '../tool_tip'; -import { getSecureRelForTarget } from '../../services'; -import { validateHref } from '../../services/security/href_validator'; +import { euiContextMenuItemStyles } from './context_menu_item.styles'; export type EuiContextMenuItemIcon = ReactElement | string | HTMLElement; export type EuiContextMenuItemLayoutAlignment = 'center' | 'top' | 'bottom'; -const sizeToClassNameMap = { - s: 'euiContextMenuItem--small', - m: null, -}; - -export const SIZES = keysOf(sizeToClassNameMap); +export const SIZES = ['s', 'm'] as const; export interface EuiContextMenuItemProps extends CommonProps { icon?: EuiContextMenuItemIcon; @@ -63,7 +63,7 @@ export interface EuiContextMenuItemProps extends CommonProps { /** * Reduce the size to `s` when in need of a more compressed menu */ - size?: keyof typeof sizeToClassNameMap; + size?: (typeof SIZES)[number]; } type Props = CommonProps & @@ -83,126 +83,134 @@ const layoutAlignToClassNames: { export const LAYOUT_ALIGN = keysOf(layoutAlignToClassNames); -export class EuiContextMenuItem extends Component { - render() { - const { - children, - className, - hasPanel, - icon, - buttonRef, - disabled: _disabled, - layoutAlign = 'center', - toolTipTitle, - toolTipContent, - toolTipPosition = 'right', - href, - target, - rel, - size, - ...rest - } = this.props; - let iconInstance; - - const isHrefValid = !href || validateHref(href); - const disabled = _disabled || !isHrefValid; - - if (icon) { - switch (typeof icon) { - case 'string': - iconInstance = ( - - ); - break; - - default: - // Assume it's already an instance of an icon. - iconInstance = cloneElement(icon as ReactElement, { - className: 'euiContextMenu__icon', - }); - } - } - - let arrow; - - if (hasPanel) { - arrow = ( - - ); - } - - const classes = classNames( - 'euiContextMenuItem', - size && sizeToClassNameMap[size], - className, - { - 'euiContextMenuItem-isDisabled': disabled, - } +export const EuiContextMenuItem: FunctionComponent = ({ + children, + className, + hasPanel, + icon, + buttonRef, + disabled: _disabled, + layoutAlign = 'center', + toolTipTitle, + toolTipContent, + toolTipPosition = 'right', + href, + target, + rel, + size = 'm', + ...rest +}) => { + const isHrefValid = !href || validateHref(href); + const disabled = _disabled || !isHrefValid; + + const classes = classNames('euiContextMenuItem', className); + + const euiTheme = useEuiTheme(); + const styles = euiContextMenuItemStyles(euiTheme); + const cssStyles = [ + styles.euiContextMenuItem, + styles.sizes[size], + styles.layoutAlign[layoutAlign], + disabled && styles.disabled, + ]; + + const iconInstance = + icon && + (typeof icon === 'string' ? ( + + ) : ( + // Assume it's already an instance of an icon. + cloneElementWithCss(icon as ReactElement, { + css: styles.euiContextMenu__icon, + }) + )); + + const arrow = hasPanel && ( + + ); + + const textStyles = [ + styles.text.euiContextMenuItem__text, + size === 's' && styles.text.s, + ]; + const buttonContent = ( + <> + {iconInstance} + + {children} + + {arrow} + + ); + + let button; + // elements don't respect the `disabled` attribute. So if we're disabled, we'll just pretend + // this is a button and piggyback off its disabled styles. + if (href && !disabled) { + const secureRel = getSecureRelForTarget({ href, target, rel }); + + button = ( + } + {...(rest as AnchorHTMLAttributes)} + > + {buttonContent} + ); - - const layoutClasses = classNames( - 'euiContextMenu__itemLayout', - layoutAlignToClassNames[layoutAlign] + } else if (href || rest.onClick) { + button = ( + ); - - const buttonInner = ( - - {iconInstance} - {children} - {arrow} - + } else { + button = ( +
} + {...(rest as HTMLAttributes)} + > + {buttonContent} +
); + } - let button; - // elements don't respect the `disabled` attribute. So if we're disabled, we'll just pretend - // this is a button and piggyback off its disabled styles. - if (href && !disabled) { - const secureRel = getSecureRelForTarget({ href, target, rel }); - - button = ( - } - {...(rest as AnchorHTMLAttributes)} - > - {buttonInner} - - ); - } else { - button = ( - - ); - } - - if (toolTipContent) { - return ( - - {button} - - ); - } else { - return button; - } + if (toolTipContent) { + return ( + + {button} + + ); + } else { + return button; } -} +}; diff --git a/src/components/context_menu/context_menu_panel.a11y.tsx b/src/components/context_menu/context_menu_panel.a11y.tsx index f48f5411200..59f81b43dba 100644 --- a/src/components/context_menu/context_menu_panel.a11y.tsx +++ b/src/components/context_menu/context_menu_panel.a11y.tsx @@ -16,10 +16,10 @@ import { EuiContextMenuItem } from './context_menu_item'; import { EuiContextMenuPanel } from './context_menu_panel'; const items = [ - + Option A , - + {}}> Option B , diff --git a/src/components/context_menu/context_menu_panel.spec.tsx b/src/components/context_menu/context_menu_panel.spec.tsx index f51a5e18dc2..9e41fb7d6db 100644 --- a/src/components/context_menu/context_menu_panel.spec.tsx +++ b/src/components/context_menu/context_menu_panel.spec.tsx @@ -18,22 +18,28 @@ import { EuiContextMenuItem } from './context_menu_item'; import { EuiContextMenuPanel } from './context_menu_panel'; const items = [ - + Option A , - + Option B , - + Option C , ]; const children = ( <> - - - + + + ); @@ -41,14 +47,14 @@ describe('EuiContextMenuPanel', () => { describe('Focus behavior', () => { it('focuses the panel by default', () => { cy.mount({children}); - cy.focused().should('have.attr', 'class', 'euiContextMenuPanel'); + cy.focused().should('have.class', 'euiContextMenuPanel'); }); describe('with `children`', () => { it('ignores arrow key navigation, which only toggles for `items`', () => { cy.mount({children}); cy.realPress('{downarrow}'); - cy.focused().should('have.attr', 'class', 'euiContextMenuPanel'); + cy.focused().should('have.class', 'euiContextMenuPanel'); }); }); @@ -64,7 +70,7 @@ describe('EuiContextMenuPanel', () => { cy.mount( ); - cy.focused().should('have.attr', 'class', 'euiContextMenuPanel'); + cy.focused().should('have.class', 'euiContextMenuPanel'); }); it('focuses and registers any tabbable child as navigable menu items', () => { @@ -122,9 +128,9 @@ describe('EuiContextMenuPanel', () => { id: 'A', title: 'Panel A', items: [ - { name: 'Lorem' }, + { name: 'Lorem', href: '#' }, { name: 'Go to Panel B', panel: 'B', 'data-test-subj': 'panelA' }, - { name: 'Ipsum' }, + { name: 'Ipsum', href: '#' }, ], }, { @@ -132,8 +138,8 @@ describe('EuiContextMenuPanel', () => { title: 'Panel B', items: [ { name: 'Go to Panel C', panel: 'C', 'data-test-subj': 'panelB' }, - { name: 'Lorem' }, - { name: 'Ipsum' }, + { name: 'Lorem', href: '#' }, + { name: 'Ipsum', href: '#' }, ], initialFocusedItemIndex: 0, }, @@ -153,7 +159,7 @@ describe('EuiContextMenuPanel', () => { cy.realPress('{rightarrow}'); cy.focused().should('have.attr', 'data-test-subj', 'panelB'); // has initialFocusedItemIndex cy.realPress('{rightarrow}'); - cy.focused().should('have.attr', 'class', 'euiContextMenuPanelTitle'); + cy.focused().should('have.class', 'euiContextMenuPanel__title'); }); it('focuses the correct toggling item when using the left arrow key to navigate to the previous panel', () => { @@ -198,8 +204,8 @@ describe('EuiContextMenuPanel', () => { it('reclaims focus from the parent popover panel', () => { mountAndOpenPopover(); - cy.focused().should('not.have.attr', 'class', 'euiPopover__panel'); - cy.focused().should('have.attr', 'class', 'euiContextMenuPanel'); + cy.focused().should('not.have.class', 'euiPopover__panel'); + cy.focused().should('have.class', 'euiContextMenuPanel'); }); it('does not hijack focus from the EuiPopover if `initialFocus` is set', () => { @@ -208,7 +214,7 @@ describe('EuiContextMenuPanel', () => { ); - cy.focused().should('not.have.attr', 'class', 'euiContextMenuPanel'); + cy.focused().should('not.class', 'euiContextMenuPanel'); cy.focused().should('have.attr', 'id', 'testInitialFocus'); }); @@ -234,35 +240,35 @@ describe('EuiContextMenuPanel', () => { }); it('focuses the panel by default', () => { - cy.focused().should('have.attr', 'class', 'euiContextMenuPanel'); + cy.focused().should('have.class', 'euiContextMenuPanel'); }); it('down arrow key focuses the first menu item', () => { - cy.focused().should('have.attr', 'class', 'euiContextMenuPanel'); + cy.focused().should('have.class', 'euiContextMenuPanel'); cy.realPress('{downarrow}'); cy.focused().should('have.attr', 'data-test-subj', 'itemA'); }); it('subsequently, down arrow key focuses the next menu item', () => { - cy.focused().should('have.attr', 'class', 'euiContextMenuPanel'); + cy.focused().should('have.class', 'euiContextMenuPanel'); cy.repeatRealPress('{downarrow}'); cy.focused().should('have.attr', 'data-test-subj', 'itemB'); }); it('up arrow key wraps to last menu item', () => { - cy.focused().should('have.attr', 'class', 'euiContextMenuPanel'); + cy.focused().should('have.class', 'euiContextMenuPanel'); cy.realPress('{uparrow}'); cy.focused().should('have.attr', 'data-test-subj', 'itemC'); }); it('down arrow key wraps to first menu item', () => { - cy.focused().should('have.attr', 'class', 'euiContextMenuPanel'); + cy.focused().should('have.class', 'euiContextMenuPanel'); cy.repeatRealPress('{downarrow}', 4); cy.focused().should('have.attr', 'data-test-subj', 'itemA'); }); it('subsequently, up arrow key focuses the previous menu item', () => { - cy.focused().should('have.attr', 'class', 'euiContextMenuPanel'); + cy.focused().should('have.class', 'euiContextMenuPanel'); cy.repeatRealPress('{uparrow}'); cy.focused().should('have.attr', 'data-test-subj', 'itemB'); }); @@ -329,6 +335,7 @@ describe('EuiContextMenuPanel', () => { items: [ { name: 'End', + href: '#', 'data-test-subj': 'itemC', }, ], @@ -376,19 +383,19 @@ describe('EuiContextMenuPanel', () => { }); it('tab key focuses the first menu item', () => { - cy.focused().should('have.attr', 'class', 'euiContextMenuPanel'); + cy.focused().should('have.class', 'euiContextMenuPanel'); cy.realPress('Tab'); cy.focused().should('have.attr', 'data-test-subj', 'itemA'); }); it('subsequently, tab key focuses the next menu item', () => { - cy.focused().should('have.attr', 'class', 'euiContextMenuPanel'); + cy.focused().should('have.class', 'euiContextMenuPanel'); cy.repeatRealPress('Tab'); cy.focused().should('have.attr', 'data-test-subj', 'itemB'); }); it('shift+tab key focuses the previous menu item', () => { - cy.focused().should('have.attr', 'class', 'euiContextMenuPanel'); + cy.focused().should('have.class', 'euiContextMenuPanel'); cy.repeatRealPress('Tab'); cy.focused().should('have.attr', 'data-test-subj', 'itemB'); cy.realPress(['Shift', 'Tab']); diff --git a/src/components/context_menu/context_menu_panel.styles.ts b/src/components/context_menu/context_menu_panel.styles.ts new file mode 100644 index 00000000000..58611f32f9f --- /dev/null +++ b/src/components/context_menu/context_menu_panel.styles.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { css, keyframes } from '@emotion/react'; + +import { UseEuiTheme } from '../../services'; +import { logicalCSS, euiCantAnimate } from '../../global_styling'; +import { euiTitle } from '../title/title.styles'; + +import { euiContextMenuVariables } from './context_menu.styles'; + +export const euiContextMenuPanelStyles = (euiThemeContext: UseEuiTheme) => { + const { euiTheme } = euiThemeContext; + const { panelWidth } = euiContextMenuVariables(euiThemeContext); + + const animations = { + transitioning: css` + pointer-events: none; + animation-fill-mode: forwards; + animation-duration: ${euiTheme.animation.normal}; + animation-timing-function: ${euiTheme.animation.resistance}; + + ${euiCantAnimate} { + animation-duration: 0s; /* Run the animation instantly, which triggers onAnimationEnd */ + } + `, + inLeft: keyframes` + 0% { transform: translateX(${panelWidth}); } + 100% { transform: translateX(0); } + `, + outLeft: keyframes` + 0% { transform: translateX(0); } + 100% { transform: translateX(-${panelWidth}); } + `, + inRight: keyframes` + 0% { transform: translateX(-${panelWidth}); } + 100% { transform: translateX(0); } + `, + outRight: keyframes` + 0% { transform: translateX(0); } + 100% { transform: translateX(${panelWidth}); } + `, + }; + + return { + euiContextMenuPanel: css` + ${logicalCSS('width', '100%')} + visibility: visible; + outline-offset: -${euiTheme.focus.width}; + + &:focus { + outline: none; /* Hide focus ring because of tabindex=-1 on Safari */ + } + `, + // Panel animations + next: { + in: css` + ${animations.transitioning} + animation-name: ${animations.inLeft}; + `, + out: css` + ${animations.transitioning} + animation-name: ${animations.outLeft}; + `, + }, + previous: { + in: css` + ${animations.transitioning} + animation-name: ${animations.inRight}; + `, + out: css` + ${animations.transitioning} + animation-name: ${animations.outRight}; + `, + }, + // Children + euiContextMenuPanel__title: css` + ${euiTitle(euiThemeContext, 'xxs')} + ${logicalCSS('border-bottom', euiTheme.border.thin)} + + &:enabled:focus { + /* Override the default focus background on EUiContextMenuItems */ + background-color: unset; + } + `, + }; +}; diff --git a/src/components/context_menu/context_menu_panel.test.tsx b/src/components/context_menu/context_menu_panel.test.tsx index 50db21f50e1..22e7152ee6f 100644 --- a/src/components/context_menu/context_menu_panel.test.tsx +++ b/src/components/context_menu/context_menu_panel.test.tsx @@ -7,9 +7,10 @@ */ import React from 'react'; -import { mount } from 'enzyme'; -import { requiredProps } from '../../test'; +import { fireEvent } from '@testing-library/react'; import { render } from '../../test/rtl'; +import { shouldRenderCustomStyles } from '../../test/internal'; +import { requiredProps } from '../../test'; import { EuiContextMenuPanel, SIZES } from './context_menu_panel'; @@ -30,7 +31,9 @@ const items = [ ]; describe('EuiContextMenuPanel', () => { - test('is rendered', () => { + shouldRenderCustomStyles(); + + it('renders', () => { const { container } = render( Hello ); @@ -39,17 +42,15 @@ describe('EuiContextMenuPanel', () => { }); describe('props', () => { - describe('title', () => { - test('is rendered', () => { - const { container } = render(); + test('title', () => { + const { container } = render(); - expect(container.firstChild).toMatchSnapshot(); - }); + expect(container.firstChild).toMatchSnapshot(); }); describe('size', () => { SIZES.forEach((size) => { - it(`${size} is rendered`, () => { + test(size, () => { const { container } = render( ); @@ -60,30 +61,37 @@ describe('EuiContextMenuPanel', () => { }); describe('onClose', () => { - test('renders a button as a title', () => { - const { container } = render( + it('renders a button and a left arrow', () => { + const { container, getByTestSubject } = render( {}} /> ); - expect(container.firstChild).toMatchSnapshot(); + expect( + getByTestSubject('contextMenuPanelTitleButton').nodeName + ).toEqual('BUTTON'); + expect( + container.querySelector('[data-euiicon-type="arrowLeft"]') + ).toBeInTheDocument(); }); - test("isn't called upon instantiation", () => { - const onCloseHandler = jest.fn(); - - mount(); + it('renders a plain div if onClose is not passed', () => { + const { getByTestSubject } = render( + + ); - expect(onCloseHandler).not.toHaveBeenCalled(); + expect(getByTestSubject('contextMenuPanelTitle').nodeName).toEqual( + 'DIV' + ); }); - test('is called when the title is clicked', () => { + it('is called when the title is clicked', () => { const onCloseHandler = jest.fn(); - const component = mount( + const { getByTestSubject } = render( ); - component.find('button').simulate('click'); + fireEvent.click(getByTestSubject('contextMenuPanelTitleButton')); expect(onCloseHandler).toHaveBeenCalledTimes(1); }); @@ -93,100 +101,38 @@ describe('EuiContextMenuPanel', () => { it('is called with a height value', () => { const onHeightChange = jest.fn(); - mount(); + render(); expect(onHeightChange).toHaveBeenCalledWith(0); }); }); - describe('transitionDirection', () => { - describe('next', () => { - describe('with transitionType', () => { - describe('in', () => { - test('is rendered', () => { - const { container } = render( - - ); - - expect(container.firstChild).toMatchSnapshot(); - }); - }); - - describe('out', () => { - test('is rendered', () => { - const { container } = render( - - ); - - expect(container.firstChild).toMatchSnapshot(); - }); - }); - }); - }); - - describe('previous', () => { - describe('with transitionType', () => { - describe('in', () => { - test('is rendered', () => { - const { container } = render( - - ); - - expect(container.firstChild).toMatchSnapshot(); - }); - }); - - describe('out', () => { - test('is rendered', () => { - const { container } = render( - - ); - - expect(container.firstChild).toMatchSnapshot(); - }); - }); - }); - }); - }); - describe('onUseKeyboardToNavigate', () => { it('is called when up arrow is pressed', () => { const onUseKeyboardToNavigateHandler = jest.fn(); - const component = mount( + const { container } = render( ); - component.simulate('keydown', { key: keys.ARROW_UP }); + fireEvent.keyDown(container.firstChild!, { key: keys.ARROW_UP }); expect(onUseKeyboardToNavigateHandler).toHaveBeenCalledTimes(1); }); it('is called when down arrow is pressed', () => { const onUseKeyboardToNavigateHandler = jest.fn(); - const component = mount( + const { container } = render( ); - component.simulate('keydown', { key: keys.ARROW_UP }); + fireEvent.keyDown(container.firstChild!, { key: keys.ARROW_UP }); expect(onUseKeyboardToNavigateHandler).toHaveBeenCalledTimes(1); }); @@ -194,7 +140,7 @@ describe('EuiContextMenuPanel', () => { it('calls handler if onClose and showPreviousPanel exists', () => { const onUseKeyboardToNavigateHandler = jest.fn(); - const component = mount( + const { container } = render( {}} @@ -203,21 +149,21 @@ describe('EuiContextMenuPanel', () => { /> ); - component.simulate('keydown', { key: keys.ARROW_LEFT }); + fireEvent.keyDown(container.firstChild!, { key: keys.ARROW_LEFT }); expect(onUseKeyboardToNavigateHandler).toHaveBeenCalledTimes(1); }); it("doesn't call handler if showPreviousPanel doesn't exist", () => { const onUseKeyboardToNavigateHandler = jest.fn(); - const component = mount( + const { container } = render( ); - component.simulate('keydown', { key: keys.ARROW_LEFT }); + fireEvent.keyDown(container.firstChild!, { key: keys.ARROW_LEFT }); expect(onUseKeyboardToNavigateHandler).not.toHaveBeenCalled(); }); }); @@ -226,7 +172,7 @@ describe('EuiContextMenuPanel', () => { it('calls handler if showNextPanel exists', () => { const onUseKeyboardToNavigateHandler = jest.fn(); - const component = mount( + const { container } = render( {}} @@ -234,21 +180,21 @@ describe('EuiContextMenuPanel', () => { /> ); - component.simulate('keydown', { key: keys.ARROW_RIGHT }); + fireEvent.keyDown(container.firstChild!, { key: keys.ARROW_RIGHT }); expect(onUseKeyboardToNavigateHandler).toHaveBeenCalledTimes(1); }); it("doesn't call handler if showNextPanel doesn't exist", () => { const onUseKeyboardToNavigateHandler = jest.fn(); - const component = mount( + const { container } = render( ); - component.simulate('keydown', { key: keys.ARROW_RIGHT }); + fireEvent.keyDown(container.firstChild!, { key: keys.ARROW_RIGHT }); expect(onUseKeyboardToNavigateHandler).not.toHaveBeenCalled(); }); }); diff --git a/src/components/context_menu/context_menu_panel.tsx b/src/components/context_menu/context_menu_panel.tsx index 1e6602b2c2c..bf928459f59 100644 --- a/src/components/context_menu/context_menu_panel.tsx +++ b/src/components/context_menu/context_menu_panel.tsx @@ -10,20 +10,22 @@ import React, { cloneElement, Component, HTMLAttributes, + PropsWithChildren, ReactElement, ReactNode, } from 'react'; import classNames from 'classnames'; import { tabbable, FocusableElement } from 'tabbable'; -import { CommonProps, NoArgCallback, keysOf } from '../common'; -import { EuiIcon } from '../icon'; +import { withEuiTheme, WithEuiThemeProps, keys } from '../../services'; +import { CommonProps, NoArgCallback } from '../common'; import { EuiResizeObserver } from '../observer/resize_observer'; -import { keys } from '../../services'; + import { EuiContextMenuItem, EuiContextMenuItemProps, } from './context_menu_item'; +import { euiContextMenuPanelStyles } from './context_menu_panel.styles'; export type EuiContextMenuPanelHeightChangeHandler = (height: number) => void; export type EuiContextMenuPanelTransitionType = 'in' | 'out'; @@ -32,48 +34,32 @@ export type EuiContextMenuPanelShowPanelCallback = ( currentPanelIndex?: number ) => void; -const titleSizeToClassNameMap = { - s: 'euiContextMenuPanelTitle--small', - m: null, -}; - -export const SIZES = keysOf(titleSizeToClassNameMap); - -export interface EuiContextMenuPanelProps { - initialFocusedItemIndex?: number; - items?: ReactElement[]; - onClose?: NoArgCallback; - onHeightChange?: EuiContextMenuPanelHeightChangeHandler; - onTransitionComplete?: NoArgCallback; - onUseKeyboardToNavigate?: NoArgCallback; - showNextPanel?: EuiContextMenuPanelShowPanelCallback; - showPreviousPanel?: NoArgCallback; - title?: ReactNode; - transitionDirection?: EuiContextMenuPanelTransitionDirection; - transitionType?: EuiContextMenuPanelTransitionType; - /** - * Alters the size of the items and the title - */ - size?: (typeof SIZES)[number]; -} +export const SIZES = ['s', 'm'] as const; -type Props = CommonProps & +export type EuiContextMenuPanelProps = PropsWithChildren & + CommonProps & Omit< HTMLAttributes, 'onKeyDown' | 'tabIndex' | 'onAnimationEnd' | 'title' - > & - EuiContextMenuPanelProps; - -const transitionDirectionAndTypeToClassNameMap = { - next: { - in: 'euiContextMenuPanel-txInLeft', - out: 'euiContextMenuPanel-txOutLeft', - }, - previous: { - in: 'euiContextMenuPanel-txInRight', - out: 'euiContextMenuPanel-txOutRight', - }, -}; + > & { + initialFocusedItemIndex?: number; + items?: ReactElement[]; + onClose?: NoArgCallback; + onHeightChange?: EuiContextMenuPanelHeightChangeHandler; + onTransitionComplete?: NoArgCallback; + onUseKeyboardToNavigate?: NoArgCallback; + showNextPanel?: EuiContextMenuPanelShowPanelCallback; + showPreviousPanel?: NoArgCallback; + title?: ReactNode; + transitionDirection?: EuiContextMenuPanelTransitionDirection; + transitionType?: EuiContextMenuPanelTransitionType; + /** + * Alters the size of the items and the title + */ + size?: (typeof SIZES)[number]; + }; + +type Props = EuiContextMenuPanelProps; interface State { prevProps: { @@ -87,7 +73,10 @@ interface State { tookInitialFocus: boolean; } -export class EuiContextMenuPanel extends Component { +export class EuiContextMenuPanelClass extends Component< + WithEuiThemeProps & Props, + State +> { static defaultProps: Partial = { items: [], }; @@ -97,7 +86,7 @@ export class EuiContextMenuPanel extends Component { private panel?: HTMLElement | null = null; private initialPopoverParent?: HTMLElement | null = null; - constructor(props: Props) { + constructor(props: WithEuiThemeProps & Props) { super(props); this.state = { @@ -405,6 +394,7 @@ export class EuiContextMenuPanel extends Component { render() { const { + theme, children, className, onClose, @@ -421,55 +411,32 @@ export class EuiContextMenuPanel extends Component { size, ...rest } = this.props; - let panelTitle; - - if (title) { - const titleClasses = classNames( - 'euiContextMenuPanelTitle', - size && titleSizeToClassNameMap[size] - ); - if (Boolean(onClose)) { - panelTitle = ( - - ); - } else { - panelTitle = ( -
- {title} -
- ); - } - } + const classes = classNames('euiContextMenuPanel', className); - const classes = classNames( - 'euiContextMenuPanel', - className, + const styles = euiContextMenuPanelStyles(theme); + const cssStyles = [ + styles.euiContextMenuPanel, transitionDirection && transitionType && - transitionDirectionAndTypeToClassNameMap[transitionDirection] - ? transitionDirectionAndTypeToClassNameMap[transitionDirection][ - transitionType - ] - : undefined + styles[transitionDirection][transitionType], + ]; + + const panelTitle = title && ( + { + if (onClose) this.backButton = node; + }} + data-test-subj={ + onClose ? 'contextMenuPanelTitleButton' : 'contextMenuPanelTitle' + } + icon={onClose && 'arrowLeft'} + > + {title} + ); const content = @@ -488,6 +455,7 @@ export class EuiContextMenuPanel extends Component { return (
{ ); } } + +export const EuiContextMenuPanel = withEuiTheme( + EuiContextMenuPanelClass +); diff --git a/src/components/form/super_select/__snapshots__/super_select.test.tsx.snap b/src/components/form/super_select/__snapshots__/super_select.test.tsx.snap index 6ae3b34d89b..c6ab8ddaf78 100644 --- a/src/components/form/super_select/__snapshots__/super_select.test.tsx.snap +++ b/src/components/form/super_select/__snapshots__/super_select.test.tsx.snap @@ -165,46 +165,38 @@ exports[`EuiSuperSelect props custom display is propagated to dropdown 1`] = ` >
@@ -403,48 +395,40 @@ exports[`EuiSuperSelect props more props are propogated to each option 1`] = ` >
@@ -544,46 +528,38 @@ exports[`EuiSuperSelect props options are rendered when select is open 1`] = ` >
@@ -686,46 +662,38 @@ exports[`EuiSuperSelect props renders popoverProps on the underlying EuiPopover >
diff --git a/src/components/index.scss b/src/components/index.scss index 4443e3a4c91..4060be38d41 100644 --- a/src/components/index.scss +++ b/src/components/index.scss @@ -2,7 +2,6 @@ @import 'color_picker/index'; @import 'combo_box/index'; -@import 'context_menu/index'; @import 'control_bar/index'; @import 'date_picker/index'; @import 'datagrid/index'; diff --git a/src/components/notification/__snapshots__/notification_event.test.tsx.snap b/src/components/notification/__snapshots__/notification_event.test.tsx.snap index 090ea4760d8..31c0724e804 100644 --- a/src/components/notification/__snapshots__/notification_event.test.tsx.snap +++ b/src/components/notification/__snapshots__/notification_event.test.tsx.snap @@ -126,52 +126,37 @@ exports[`EuiNotificationEvent props badgeColor is rendered 1`] = ` exports[`EuiNotificationEvent props contextMenuItems are rendered 1`] = `
- - - +
`; diff --git a/src/components/notification/__snapshots__/notification_event_meta.test.tsx.snap b/src/components/notification/__snapshots__/notification_event_meta.test.tsx.snap index fc70d03b283..3375b7a8bfe 100644 --- a/src/components/notification/__snapshots__/notification_event_meta.test.tsx.snap +++ b/src/components/notification/__snapshots__/notification_event_meta.test.tsx.snap @@ -74,52 +74,37 @@ exports[`EuiNotificationEventMeta props badgeColor is rendered 1`] = ` exports[`EuiNotificationEventMeta props contextMenuItems are rendered 1`] = `
- - - +
`; diff --git a/src/components/table/mobile/__snapshots__/table_sort_mobile_item.test.tsx.snap b/src/components/table/mobile/__snapshots__/table_sort_mobile_item.test.tsx.snap index ae8241ebf81..5fa625a57a4 100644 --- a/src/components/table/mobile/__snapshots__/table_sort_mobile_item.test.tsx.snap +++ b/src/components/table/mobile/__snapshots__/table_sort_mobile_item.test.tsx.snap @@ -3,21 +3,17 @@ exports[`EuiTableSortMobileItem is rendered 1`] = ` `; diff --git a/src/components/table/mobile/table_sort_mobile_item.test.tsx b/src/components/table/mobile/table_sort_mobile_item.test.tsx index 6cb4d9225d9..79c6602c356 100644 --- a/src/components/table/mobile/table_sort_mobile_item.test.tsx +++ b/src/components/table/mobile/table_sort_mobile_item.test.tsx @@ -14,7 +14,9 @@ import { EuiTableSortMobileItem } from './table_sort_mobile_item'; describe('EuiTableSortMobileItem', () => { test('is rendered', () => { - const { container } = render(); + const { container } = render( + {}} /> + ); expect(container.firstChild).toMatchSnapshot(); }); diff --git a/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap b/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap index da90f4cc68f..f04c07d60e8 100644 --- a/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap +++ b/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap @@ -229,70 +229,58 @@ exports[`EuiTablePagination renders 1`] = `

diff --git a/upcoming_changelogs/7312.md b/upcoming_changelogs/7312.md new file mode 100644 index 00000000000..f5915a92d3e --- /dev/null +++ b/upcoming_changelogs/7312.md @@ -0,0 +1,11 @@ +**Bug fixes** + +- `EuiContextMenu` now renders text colors correctly when used within an `EuiBottomBar` + +**Accessibility** + +- `EuiContextMenu` now correctly respects reduced motion preferences + +**CSS-in-JS conversions** + +- Converted `EuiContextMenu`, `EuiContextMenuPanel`, and `EuiContextMenuItem` to Emotion; Removed `$euiContextMenuWidth`