diff --git a/packages/eui/.loki/reference/chrome_desktop_Tabular_Content_EuiDataGrid_Cell_Actions.png b/packages/eui/.loki/reference/chrome_desktop_Tabular_Content_EuiDataGrid_Cell_Actions.png
new file mode 100644
index 00000000000..ca50c61e67d
Binary files /dev/null and b/packages/eui/.loki/reference/chrome_desktop_Tabular_Content_EuiDataGrid_Cell_Actions.png differ
diff --git a/packages/eui/.loki/reference/chrome_desktop_Tabular_Content_EuiDataGrid_Cell_Expansion_Popover.png b/packages/eui/.loki/reference/chrome_desktop_Tabular_Content_EuiDataGrid_Cell_Expansion_Popover.png
new file mode 100644
index 00000000000..2edc9b07ff9
Binary files /dev/null and b/packages/eui/.loki/reference/chrome_desktop_Tabular_Content_EuiDataGrid_Cell_Expansion_Popover.png differ
diff --git a/packages/eui/.loki/reference/chrome_mobile_Tabular_Content_EuiDataGrid_Cell_Actions.png b/packages/eui/.loki/reference/chrome_mobile_Tabular_Content_EuiDataGrid_Cell_Actions.png
new file mode 100644
index 00000000000..1203d043f97
Binary files /dev/null and b/packages/eui/.loki/reference/chrome_mobile_Tabular_Content_EuiDataGrid_Cell_Actions.png differ
diff --git a/packages/eui/.loki/reference/chrome_mobile_Tabular_Content_EuiDataGrid_Cell_Expansion_Popover.png b/packages/eui/.loki/reference/chrome_mobile_Tabular_Content_EuiDataGrid_Cell_Expansion_Popover.png
new file mode 100644
index 00000000000..8e4260f510a
Binary files /dev/null and b/packages/eui/.loki/reference/chrome_mobile_Tabular_Content_EuiDataGrid_Cell_Expansion_Popover.png differ
diff --git a/packages/eui/changelogs/upcoming/8011.md b/packages/eui/changelogs/upcoming/8011.md
new file mode 100644
index 00000000000..c9a58c033c5
--- /dev/null
+++ b/packages/eui/changelogs/upcoming/8011.md
@@ -0,0 +1,12 @@
+- Updated `EuiDataGrid`'s cell actions to always consistently be left-aligned, regardless of text content alignment
+- Increased `EuiDataGrid`'s cell actions hover zone to reduce UX friction when mousing over from the grid cell to its actions
+
+**Bug fixes**
+
+- Fixed an `EuiDataGrid` bug where the `setCellProps` callback passed by `renderCellValue` was not correctly applying custom `data-test-subj`s
+
+**CSS-in-JS conversions**
+
+- Converted `EuiDataGrid`'s cell popover, actions, and focus outline to Emotion; Removed the following Sass variables and mixins:
+ - `$euiZDataGridCellPopover`
+ - `@euiDataGridCellFocus`
diff --git a/packages/eui/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap b/packages/eui/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap
index f8c9392d33e..b8e59d3edb8 100644
--- a/packages/eui/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap
+++ b/packages/eui/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap
@@ -596,7 +596,7 @@ exports[`EuiDataGrid rendering renders additional toolbar controls 1`] = `
>
.euiPopover {
- position: absolute;
- bottom: 100%;
-
- .euiDataGridRowCell--alignLeft & {
- left: 0;
- }
-
- .euiDataGridRowCell--alignRight & {
- right: 0;
- }
-}
-
-.euiDataGridRowCell__actions {
- z-index: $euiZDataGridCellPopover - 2; // Sit below sticky column headers
- margin-bottom: -$euiBorderWidthThin; // Vertical alignment
- display: flex;
- gap: $euiSizeXS / 2;
- padding-inline: $euiSizeXS / 2;
- background-color: var(--euiDataGridCellOutlineColor);
- color: $euiColorEmptyShade;
- border: $euiBorderWidthThin solid var(--euiDataGridCellOutlineColor);
- border-top-left-radius: $euiBorderRadius / 2;
- border-top-right-radius: $euiBorderRadius / 2;
- transform: scaleY(0);
- transform-origin: bottom;
-
- // The first row of cell actions need to be visible above the cell headers,
- // but other cell actions that scroll past the sticky headers should not
- .euiDataGridRowCell[data-gridcell-visible-row-index='0'] > & {
- z-index: $euiZDataGridCellPopover - 1;
- }
-
- .euiDataGridRowCell--alignLeft & {
- border-bottom-right-radius: $euiBorderRadius / 2;
- }
-
- .euiDataGridRowCell--alignRight & {
- border-bottom-left-radius: $euiBorderRadius / 2;
- }
-
- // Visual trickery - fill in the gap between the cell outline border-radius & the actions
- &::after {
- content: '';
- position: absolute;
- top: 100%;
- height: $euiBorderWidthThick;
- width: $euiBorderWidthThick;
- background-color: var(--euiDataGridCellOutlineColor);
-
- .euiDataGridRowCell--alignLeft & {
- left: -$euiBorderWidthThin;
- }
-
- .euiDataGridRowCell--alignRight & {
- right: -$euiBorderWidthThin;
- }
- }
-}
-
-.euiDataGridRowCell__actionButtonIcon {
- height: $euiSize + $euiSizeXS;
- width: $euiSize;
- border-radius: 0;
-
- /* Force all cell action buttons to match EUI colors */
- &,
- svg {
- // stylelint-disable declaration-no-important
- background-color: transparent !important;
- color: currentColor !important;
- fill: currentColor !important;
- // stylelint-enable declaration-no-important
- }
-
- /* Manually increase the size of the expand cell icon - it's a bit small by default */
- &.euiDataGridRowCell__expandCell .euiIcon {
- width: 120%;
- height: 100%;
- }
-}
-
-@keyframes euiDataGridCellActionsSlideIn {
- from { transform: scaleY(0); }
- to { transform: scaleY(1); }
-}
-
-@keyframes euiDataGridCellPopover {
- from { opacity: 0; }
- to { opacity: 1; }
-}
diff --git a/packages/eui/src/components/datagrid/_mixins.scss b/packages/eui/src/components/datagrid/_mixins.scss
index 072c317cb84..aedd5a32cd5 100644
--- a/packages/eui/src/components/datagrid/_mixins.scss
+++ b/packages/eui/src/components/datagrid/_mixins.scss
@@ -4,27 +4,6 @@
}
}
-@mixin euiDataGridCellFocus {
- outline: none; // Remove outline as we're handling it manually
-
- // We don't want to use a border on the focused cell directly because we want to maintain the light gray borders
- // We can't use a box shadow or outline because it would be contained outside the cell and it would be cut by other surrounding cells
- // So the solution is to use use a pseudo-element. It allows us to use a border, and it can be contained inside the cell by positioning it with an absolute position.
- &::after {
- content: '';
- display: block;
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- left: 0;
- border: $euiBorderWidthThick solid var(--euiDataGridCellOutlineColor, $euiFocusRingColor);
- border-radius: $euiBorderRadius / 2;
- z-index: 2; // We want this to be on top of all the content
- pointer-events: none; // Because we put it with a higher z-index we don't want to make it clickable this way we allow selecting the content behind
- }
-}
-
@mixin euiDataGridRowCell {
.euiDataGridRowCell {
@content;
diff --git a/packages/eui/src/components/datagrid/_variables.scss b/packages/eui/src/components/datagrid/_variables.scss
index d7cfaa343a1..7eab3561d0d 100644
--- a/packages/eui/src/components/datagrid/_variables.scss
+++ b/packages/eui/src/components/datagrid/_variables.scss
@@ -1,3 +1 @@
-$euiZDataGridCellPopover: $euiZHeader; // Same z-index as EuiFlyout mask overlays - cell popovers should be under both modal and flyout overlays
-
$euiDataGridColumnResizerWidth: 3px; // Odd number because it straddles a border
diff --git a/packages/eui/src/components/datagrid/body/__snapshots__/data_grid_body_custom.test.tsx.snap b/packages/eui/src/components/datagrid/body/__snapshots__/data_grid_body_custom.test.tsx.snap
index 269852fce17..86412d4b8e7 100644
--- a/packages/eui/src/components/datagrid/body/__snapshots__/data_grid_body_custom.test.tsx.snap
+++ b/packages/eui/src/components/datagrid/body/__snapshots__/data_grid_body_custom.test.tsx.snap
@@ -11,7 +11,7 @@ exports[`EuiDataGridBodyCustomRender treats \`renderCustomGridBody\` as a render
>
+///
+///
+
+import React from 'react';
+import { EuiDataGrid } from '../../data_grid';
+
+const EXPECTED_HOVER_COLOR = 'rgb(105, 112, 125)';
+const EXPECTED_FOCUS_COLOR = 'rgb(0, 119, 204)';
+const ANIMATION = {
+ DELAY: 350,
+ DURATION: 150,
+ BUFFER: 25, // extra wait buffer to reduce flakiness
+};
+
+describe('Cell outline styles', () => {
+ const baseProps = {
+ 'aria-label': 'Test',
+ width: 300,
+ rowCount: 1,
+ renderCellValue: () => (
+ <>
+
+
+ >
+ ),
+ columns: [
+ { id: 'expandable', isExpandable: true },
+ {
+ id: 'notExpandable',
+ isExpandable: false,
+ display: (
+
+ ),
+ },
+ ],
+ columnVisibility: {
+ setVisibleColumns: () => {},
+ visibleColumns: ['expandable', 'notExpandable'],
+ },
+ };
+
+ // Test utils
+ const getExpandableRowCell = () =>
+ cy.get('.euiDataGridRowCell[data-gridcell-column-id="expandable"]');
+ const getCellExpansionPopover = () => cy.get('.euiDataGridRowCell__popover');
+ const getActions = () => cy.get('.euiDataGridRowCell__actions');
+ const getActionsHeight = () =>
+ getActions().then(($el) => {
+ const { height } = $el[0].getBoundingClientRect();
+ return height;
+ });
+ const getOutlineColor = (el: HTMLElement) => {
+ // get Window reference from element
+ const win = el.ownerDocument.defaultView!;
+ // use getComputedStyle to read the pseudo selector
+ const pseudoElement = win.getComputedStyle(el, 'after');
+
+ return pseudoElement.getPropertyValue('border-color');
+ };
+
+ it('does not show cell actions if not focused or hovered', () => {
+ cy.realMount();
+ getActions().should('not.exist');
+ });
+
+ describe('keyboard UI/UX', () => {
+ const tabToDataGrid = () => {
+ cy.repeatRealPress('Tab', 4);
+ };
+ const moveToRowCell = () => {
+ cy.realPress('ArrowDown');
+ };
+
+ it('shows the cell outline and actions as blue on focus', () => {
+ cy.realMount();
+ tabToDataGrid();
+ moveToRowCell();
+
+ getExpandableRowCell().then(($el) => {
+ expect(getOutlineColor($el[0])).to.eq(EXPECTED_FOCUS_COLOR);
+ });
+ getActions().should('have.css', 'background-color', EXPECTED_FOCUS_COLOR);
+ });
+
+ it('runs the actions height animation without a delay on focus', () => {
+ cy.realMount();
+ tabToDataGrid();
+ moveToRowCell();
+
+ cy.wait(ANIMATION.DURATION + ANIMATION.BUFFER);
+ getActionsHeight().then((height) => expect(height).to.eq(22));
+ });
+
+ it('does not re-run the actions height animation on popover keyboard close', () => {
+ cy.realMount();
+ tabToDataGrid();
+ moveToRowCell();
+
+ cy.realPress('Enter');
+ getCellExpansionPopover().should('be.visible');
+ cy.wait(ANIMATION.DURATION + ANIMATION.BUFFER);
+ getActionsHeight().then((height) => expect(height).to.eq(22));
+
+ cy.realPress('Escape');
+ getCellExpansionPopover().should('not.exist');
+ getActionsHeight().then((height) => expect(height).to.eq(22));
+ });
+
+ describe('focus trap', () => {
+ it('should show gray hover styles on header cells when the focus trap is entered', () => {
+ const getHeaderCell = () =>
+ cy.get(
+ '.euiDataGridHeaderCell[data-gridcell-column-id="notExpandable"]'
+ );
+
+ cy.realMount();
+ tabToDataGrid();
+ cy.realPress('ArrowRight');
+ getHeaderCell()
+ .should('be.focused')
+ .then(($el) => {
+ expect(getOutlineColor($el[0])).to.eq(EXPECTED_FOCUS_COLOR);
+ });
+
+ cy.realPress('Enter');
+ getHeaderCell().then(($el) => {
+ expect(getOutlineColor($el[0])).to.eq(EXPECTED_HOVER_COLOR);
+ });
+ cy.get('[data-test-subj="interactiveHeader"]').should('be.focused');
+
+ cy.realPress('Escape');
+ getHeaderCell()
+ .should('be.focused')
+ .then(($el) => {
+ expect(getOutlineColor($el[0])).to.eq(EXPECTED_FOCUS_COLOR);
+ });
+ });
+
+ it('should show gray hover styles on row cells when the focus trap is entered', () => {
+ const getRowCell = () =>
+ cy.get(
+ '.euiDataGridRowCell[data-gridcell-column-id="notExpandable"]'
+ );
+
+ cy.realMount();
+ tabToDataGrid();
+ cy.realPress('ArrowRight');
+ moveToRowCell();
+ getRowCell()
+ .should('be.focused')
+ .then(($el) => {
+ expect(getOutlineColor($el[0])).to.eq(EXPECTED_FOCUS_COLOR);
+ });
+
+ cy.realPress('Enter');
+ getRowCell().then(($el) => {
+ expect(getOutlineColor($el[0])).to.eq(EXPECTED_HOVER_COLOR);
+ });
+ cy.get('[data-test-subj="interactiveChildA"]').should('be.focused');
+
+ cy.realPress('Escape');
+ getRowCell()
+ .should('be.focused')
+ .then(($el) => {
+ expect(getOutlineColor($el[0])).to.eq(EXPECTED_FOCUS_COLOR);
+ });
+ });
+ });
+
+ describe('open popovers', () => {
+ it('should always show the focus color state when the cell header actions popover is open', () => {
+ cy.realMount();
+ tabToDataGrid();
+
+ cy.realPress('Enter');
+ cy.get(
+ '[data-test-subj="dataGridHeaderCellActionGroup-expandable"]'
+ ).should('be.visible');
+
+ cy.get(
+ '.euiDataGridHeaderCell[data-gridcell-column-id="expandable"]'
+ ).then(($el) => {
+ expect(getOutlineColor($el[0])).to.eq(EXPECTED_FOCUS_COLOR);
+ });
+ });
+
+ it('should always show the focus color state when the cell expansion popover is open', () => {
+ cy.realMount();
+ tabToDataGrid();
+ moveToRowCell();
+
+ cy.realPress('Enter');
+ getCellExpansionPopover().should('be.visible');
+
+ getExpandableRowCell().then(($el) => {
+ expect(getOutlineColor($el[0])).to.eq(EXPECTED_FOCUS_COLOR);
+ });
+ });
+ });
+ });
+
+ describe('mouse UI/UX', () => {
+ it('shows the cell outline and actions as gray on hover', () => {
+ cy.realMount();
+
+ getExpandableRowCell().realHover();
+
+ getExpandableRowCell().then(($el) => {
+ expect(getOutlineColor($el[0])).to.eq(EXPECTED_HOVER_COLOR);
+ });
+ getActions().should('have.css', 'background-color', EXPECTED_HOVER_COLOR);
+ });
+
+ it('waits to run the actions height animation on hover', () => {
+ cy.realMount();
+
+ getExpandableRowCell().realHover();
+ getActionsHeight().then((height) => expect(height).to.eq(0));
+
+ cy.wait(ANIMATION.DELAY + ANIMATION.DURATION + ANIMATION.BUFFER);
+ getActionsHeight().then((height) => expect(height).to.eq(22));
+ });
+
+ it('immediately runs the actions height animation if clicked after hover', () => {
+ cy.realMount();
+
+ getExpandableRowCell().realHover();
+ getActionsHeight().then((height) => expect(height).to.eq(0));
+
+ getExpandableRowCell().realClick();
+ cy.wait(ANIMATION.DURATION + ANIMATION.BUFFER);
+ getActionsHeight().then((height) => expect(height).to.eq(22));
+ });
+
+ it('does not flash between hover and focus colors when cell expansion is toggled via click', () => {
+ const clickExpandAction = () =>
+ cy
+ .get('[data-test-subj="euiDataGridCellExpandButton"]')
+ .realMouseMove(0, 0, { position: 'center' })
+ .realClick();
+
+ cy.realMount();
+
+ getExpandableRowCell().realHover();
+ cy.wait(ANIMATION.DELAY + ANIMATION.DURATION + ANIMATION.BUFFER);
+ clickExpandAction();
+ getCellExpansionPopover().should('be.visible');
+ getActions().should('have.css', 'background-color', EXPECTED_FOCUS_COLOR);
+
+ clickExpandAction();
+ getCellExpansionPopover().should('not.exist');
+ getActions().should('have.css', 'background-color', EXPECTED_FOCUS_COLOR);
+ });
+
+ it('has an invisible hover zone to the right of the cell actions', () => {
+ cy.realMount();
+
+ getExpandableRowCell().realHover();
+ getActions().should('be.visible');
+
+ getActions()
+ .realMouseMove(16, 0, { position: 'right' })
+ .should('be.visible')
+ .realMouseMove(80, 0, { position: 'right' }) // ~50% of cell width
+ .should('not.exist');
+ });
+ });
+});
diff --git a/packages/eui/src/components/datagrid/body/cell/data_grid_cell.styles.ts b/packages/eui/src/components/datagrid/body/cell/data_grid_cell.styles.ts
new file mode 100644
index 00000000000..5150ad88215
--- /dev/null
+++ b/packages/eui/src/components/datagrid/body/cell/data_grid_cell.styles.ts
@@ -0,0 +1,118 @@
+/*
+ * 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 { mathWithUnits } from '../../../../global_styling';
+
+export const euiDataGridCellOutlineStyles = ({ euiTheme }: UseEuiTheme) => {
+ const focusColor = euiTheme.colors.primary;
+ const hoverColor = euiTheme.colors.darkShade;
+ const outlineWidth = euiTheme.border.width.thick;
+ const borderRadius = mathWithUnits(
+ euiTheme.border.radius.medium,
+ (x) => x / 2
+ );
+
+ // Note: We use a pseudo element for the 'outline' over any other CSS approaches
+ // (outline, border, box-shadow) because it gives us the most control and reduces
+ // overlap with other cells or inner elements
+ return {
+ borderRadius,
+ focusColor,
+ focusStyles: `
+ /* Remove outline as we're handling it manually. Needed to override global styles */
+ &:focus-visible {
+ outline: none;
+ }
+
+ &::after {
+ content: '';
+ /* We want this to be visually on top of cell content but not interactive */
+ z-index: 2;
+ pointer-events: none;
+ position: absolute;
+ inset: 0;
+ border: ${outlineWidth} solid ${focusColor};
+ border-radius: ${borderRadius};
+ }
+ `,
+ hoverColor,
+ hoverStyles: `
+ &::after {
+ border-color: ${hoverColor};
+ }
+ `,
+ };
+};
+
+export const euiDataGridCellOutlineSelectors = (parentSelector = '&') => {
+ // Focus selectors
+ const focus = ':focus'; // cell has been clicked or keyboard navigated to
+ const isOpen = '.euiDataGridRowCell--open'; // always show when the cell expansion popover is open
+ const isClosing = '[data-keyboard-closing]'; // prevents the animation from replaying when keyboard focus is moved from the popover back to the cell
+ const isEntered = ':has([data-focus-lock-disabled="false"])'; // cell focus trap has been entered - ideally show the outline still, but grayed out
+
+ // Hover selectors
+ const hover = ':hover'; // hover styles should not supercede focus styles
+ const focusWithin = ':focus-within'; // used by :hover:not() to prevent flash of gray when mouse users are opening/closing the expansion popover via cell action click
+
+ // Cell header specific selectors
+ const headerActionsOpen = '.euiDataGridHeaderCell--isActionsPopoverOpen';
+
+ // Utils
+ const selectors = (...args: string[]) => [...args].join(', ');
+ const is = (selectors: string) => `${parentSelector}:is(${selectors})`;
+ const hoverNot = (selectors: string) =>
+ `${parentSelector}:hover:not(${selectors})`;
+ const _ = (selectors: string) => `${parentSelector}${selectors}`;
+
+ return {
+ outline: {
+ show: is(selectors(hover, focus, isOpen, isEntered)),
+ hover: hoverNot(selectors(focus, focusWithin, isOpen)),
+ focusTrapped: _(isEntered),
+ },
+
+ actions: {
+ hoverZone: hoverNot(selectors(focus, isOpen)),
+ hoverColor: hoverNot(selectors(focus, focusWithin, isOpen)),
+ showAnimation: is(selectors(hover, focus, isOpen, isClosing)),
+ hoverAnimation: hoverNot(selectors(focus, isOpen, isClosing)),
+ },
+
+ header: {
+ focus: is(selectors(focus, focusWithin, headerActionsOpen)), // :focus-within here is primarily intended for when the column actions button has been clicked twice
+ focusTrapped: _(isEntered),
+ },
+ };
+};
+
+export const euiDataGridRowCellStyles = (euiThemeContext: UseEuiTheme) => {
+ const cellOutline = euiDataGridCellOutlineStyles(euiThemeContext);
+ const { outline: outlineSelectors } = euiDataGridCellOutlineSelectors();
+
+ return {
+ euiDataGridRowCell: css`
+ position: relative; /* Needed for .euiDataGridRowCell__actions */
+
+ ${outlineSelectors.show} {
+ ${cellOutline.focusStyles}
+ }
+
+ ${outlineSelectors.hover} {
+ ${cellOutline.hoverStyles}
+ }
+
+ ${outlineSelectors.focusTrapped} {
+ ${cellOutline.hoverStyles}
+ }
+ `,
+ };
+};
diff --git a/packages/eui/src/components/datagrid/body/cell/data_grid_cell.test.tsx b/packages/eui/src/components/datagrid/body/cell/data_grid_cell.test.tsx
index aae8013f6c2..4f0d3e6793a 100644
--- a/packages/eui/src/components/datagrid/body/cell/data_grid_cell.test.tsx
+++ b/packages/eui/src/components/datagrid/body/cell/data_grid_cell.test.tsx
@@ -54,7 +54,7 @@ describe('EuiDataGridCell', () => {
});
it("renders the cell's `aria-rowindex` correctly when paginated on a different page", () => {
- const component = mount(
+ const { getByTestSubject } = render(
{
}}
/>
);
- expect(
- component.find('[data-test-subj="dataGridRowCell"]').prop('aria-rowindex')
- ).toEqual(61);
+
+ expect(getByTestSubject('dataGridRowCell')).toHaveAttribute(
+ 'aria-rowindex',
+ '61'
+ );
});
it('renders cell actions', () => {
diff --git a/packages/eui/src/components/datagrid/body/cell/data_grid_cell.tsx b/packages/eui/src/components/datagrid/body/cell/data_grid_cell.tsx
index 2f019022b8f..8e8c29ee203 100644
--- a/packages/eui/src/components/datagrid/body/cell/data_grid_cell.tsx
+++ b/packages/eui/src/components/datagrid/body/cell/data_grid_cell.tsx
@@ -22,7 +22,7 @@ import React, {
import { createPortal } from 'react-dom';
import { IS_JEST_ENVIRONMENT } from '../../../../utils';
-import { keys } from '../../../../services';
+import { keys, RenderWithEuiStylesMemoizer } from '../../../../services';
import { EuiScreenReaderOnly } from '../../../accessibility';
import { EuiI18n } from '../../../i18n';
import { EuiTextBlockTruncate } from '../../../text_truncate';
@@ -45,6 +45,7 @@ import {
} from './data_grid_cell_actions';
import { DefaultCellPopover } from './data_grid_cell_popover';
import { HandleInteractiveChildren } from './focus_utils';
+import { euiDataGridRowCellStyles } from './data_grid_cell.styles';
const EuiDataGridCellContent: FunctionComponent<
EuiDataGridCellValueProps & {
@@ -175,7 +176,6 @@ export class EuiDataGridCell extends Component<
cellProps: {},
isFocused: false,
isHovered: false,
- cellTextAlign: 'Left',
};
unsubscribeCell?: Function;
style = null;
@@ -415,30 +415,6 @@ export class EuiDataGridCell extends Component<
} else if (this.contentObserver) {
this.contentObserver.disconnect();
}
- this.setCellTextAlign();
- };
-
- setCellTextAlign = () => {
- if (this.cellContentsRef) {
- const { columnType } = this.props;
- if (!columnType) {
- // If no schema was set, this is likely a left aligned column
- this.setState({ cellTextAlign: 'Left' });
- } else if (columnType === 'numeric' || columnType === 'currency') {
- // Default EUI schemas that we know set right text align
- this.setState({ cellTextAlign: 'Right' });
- } else {
- // If the consumer is using a custom schema, it may have custom text alignment
- const textAlign = window
- .getComputedStyle(this.cellContentsRef)
- .getPropertyValue('text-align');
-
- this.setState({
- cellTextAlign:
- textAlign === 'right' || textAlign === 'end' ? 'Right' : 'Left',
- });
- }
- }
};
isExpandable = () => {
@@ -474,7 +450,9 @@ export class EuiDataGridCell extends Component<
// Set popover anchor
const cellAnchorEl = this.popoverAnchorRef.current!;
setPopoverAnchor(cellAnchorEl);
- setPopoverAnchorPosition(`down${this.state.cellTextAlign}`);
+ // TODO: Potentially switch to `topLeft` based on occlusion with sticky header
+ // @see https://github.com/elastic/eui/issues/7828
+ setPopoverAnchorPosition('downLeft');
// Set popover contents with cell content
const {
@@ -582,7 +560,6 @@ export class EuiDataGridCell extends Component<
const cellClasses = classNames(
'euiDataGridRowCell',
- `euiDataGridRowCell--align${this.state.cellTextAlign}`,
{
[`euiDataGridRowCell--${columnType}`]: columnType,
'euiDataGridRowCell--open': popoverIsOpen,
@@ -633,50 +610,58 @@ export class EuiDataGridCell extends Component<
return (
-
-
-
-
-
- `);
-
- const action = component.find('MockAction') as any;
- const button = action.renderProp('Component');
- expect(button({ iconType: 'function' })).toMatchInlineSnapshot(`
-
+ expect(getByTestSubject('mockCellAction')).toMatchInlineSnapshot(`
+
`);
});
it('renders primary actions in their own footer, and all remaining secondary actions in a column footer', () => {
- const component = shallow(
+ const { container } = render(
{
/>
);
- expect(component).toMatchInlineSnapshot(`
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `);
+ expect(container.querySelectorAll('.euiPopoverFooter')).toHaveLength(2);
});
it('uses visibleCellActions to configure the number of primary vs. secondary actions', () => {
- const component = shallow(
+ const { container } = render(
{
/>
);
- expect(
- component.find('EuiPopoverFooter').first().find('MockAction')
- ).toHaveLength(3);
- expect(
- component.find('EuiPopoverFooter').last().find('MockAction')
- ).toHaveLength(1);
+ const footers = container.querySelectorAll('.euiPopoverFooter');
+ expect(footers[0].querySelectorAll('button')).toHaveLength(3);
+ expect(footers[1].querySelectorAll('button')).toHaveLength(1);
});
it('does not render anything if the column has no cell actions', () => {
- const component = shallow(
+ const { container } = render(
{
/>
);
- expect(component).toMatchInlineSnapshot('');
+ expect(container).toBeEmptyDOMElement();
});
});
diff --git a/packages/eui/src/components/datagrid/body/cell/data_grid_cell_actions.tsx b/packages/eui/src/components/datagrid/body/cell/data_grid_cell_actions.tsx
index 3f278e1f9a0..6317d672c88 100644
--- a/packages/eui/src/components/datagrid/body/cell/data_grid_cell_actions.tsx
+++ b/packages/eui/src/components/datagrid/body/cell/data_grid_cell_actions.tsx
@@ -7,12 +7,8 @@
*/
import React, { JSXElementConstructor, Ref, useMemo, useCallback } from 'react';
-import {
- EuiDataGridColumn,
- EuiDataGridColumnCellAction,
- EuiDataGridColumnCellActionProps,
-} from '../../data_grid_types';
+import { useEuiMemoizedStyles } from '../../../../services';
import { EuiI18n } from '../../../i18n';
import { EuiButtonIcon, EuiButtonIconProps } from '../../../button/button_icon';
import {
@@ -22,6 +18,13 @@ import {
import { EuiFlexGroup, EuiFlexItem } from '../../../flex';
import { EuiPopoverFooter } from '../../../popover';
+import {
+ EuiDataGridColumn,
+ EuiDataGridColumnCellAction,
+ EuiDataGridColumnCellActionProps,
+} from '../../data_grid_types';
+import { euiDataGridCellActionsStyles } from './data_grid_cell_actions.styles';
+
export const EuiDataGridCellActions = ({
onExpandClick,
popoverAnchorRef,
@@ -35,6 +38,8 @@ export const EuiDataGridCellActions = ({
rowIndex: number;
colIndex: number;
}) => {
+ const styles = useEuiMemoizedStyles(euiDataGridCellActionsStyles);
+
// Note: The cell expand button/expansion popover is *always* rendered if
// column.cellActions is present (regardless of column.isExpandable).
// This is because cell actions are not otherwise accessible to keyboard
@@ -48,6 +53,7 @@ export const EuiDataGridCellActions = ({
>
{(expandButtonTitle: string) => (
),
- [onExpandClick]
+ [onExpandClick, styles]
);
const additionalButtons = useMemo(() => {
@@ -71,6 +77,7 @@ export const EuiDataGridCellActions = ({
-
+
+
{[...additionalButtons, expandButton]}
{/* The cell expansion popover needs a separate div/ref - otherwise the
extra popover wrappers mess up the absolute positioning and cause
animation stuttering on the cell actions */}
- >
+
);
};
diff --git a/packages/eui/src/components/datagrid/body/cell/data_grid_cell_popover.spec.tsx b/packages/eui/src/components/datagrid/body/cell/data_grid_cell_popover.spec.tsx
index 8a1ad813c00..da2ae883a92 100644
--- a/packages/eui/src/components/datagrid/body/cell/data_grid_cell_popover.spec.tsx
+++ b/packages/eui/src/components/datagrid/body/cell/data_grid_cell_popover.spec.tsx
@@ -17,11 +17,11 @@ const baseProps: EuiDataGridProps = {
'aria-label': 'Grid cell popover test',
height: 300,
width: 400,
- columns: [{ id: 'A' }, { id: 'B' }, { id: 'C', schema: 'numeric' }],
+ columns: [{ id: 'A' }, { id: 'B' }],
rowCount: 2,
renderCellValue: ({ rowIndex, columnId }) => `${columnId}, ${rowIndex}`,
columnVisibility: {
- visibleColumns: ['A', 'B', 'C'],
+ visibleColumns: ['A', 'B'],
setVisibleColumns: () => {},
},
};
@@ -51,7 +51,7 @@ describe('EuiDataGridCellPopover', () => {
cy.realMount();
cy.get(
'[data-gridcell-row-index="0"][data-gridcell-column-index="0"]'
- ).realClick();
+ ).realHover();
cy.get('[data-test-subj="euiDataGridCellExpandButton"]').click();
cy.focused().should(
@@ -71,7 +71,7 @@ describe('EuiDataGridCellPopover', () => {
cy.realMount();
cy.get(
'[data-gridcell-row-index="1"][data-gridcell-column-index="1"]'
- ).realClick();
+ ).realHover();
cy.get('[data-test-subj="euiDataGridCellExpandButton"]').click();
cy.focused().should(
@@ -146,14 +146,10 @@ describe('EuiDataGridCellPopover', () => {
cy.realPress('Enter');
cy.get('[data-test-subj="euiDataGridExpansionPopover"]').should('exist');
- cy.get('[data-test-subj="cellActionA"]').first().realClick();
+ cy.get('[data-test-subj="cellActionA"]').first().click();
cy.get('[data-test-subj="euiDataGridExpansionPopover"]').should('exist');
- // Close and re-open the cell popover by clicking
- cy.get('[data-test-subj="euiDataGridCellExpandButton"]').click();
- cy.get('[data-test-subj="euiDataGridCellExpandButton"]').click();
-
- cy.get('[data-test-subj="cellActionB"]').first().realClick();
+ cy.get('[data-test-subj="cellActionB"]').first().click();
cy.get('[data-test-subj="euiDataGridExpansionPopover"]').should('exist');
// Clicking the cell actions outside the popover should not have disabled the focus trap
@@ -190,7 +186,7 @@ describe('EuiDataGridCellPopover', () => {
);
cy.get(
'[data-gridcell-row-index="0"][data-gridcell-column-index="0"]'
- ).realClick();
+ ).realHover();
cy.get('[data-test-subj="euiDataGridCellExpandButton"]').click();
cy.get('.euiDataGridRowCell__popover.hello.world').should('exist');
@@ -206,8 +202,6 @@ describe('EuiDataGridCellPopover', () => {
return 'short text';
case 'B':
return 'Very long text that should get cut off because it is so long, lorem ipsum dolor sit amet words words words';
- case 'C':
- return 'right aligned text';
}
},
};
@@ -225,7 +219,7 @@ describe('EuiDataGridCellPopover', () => {
openCellPopover('A');
cy.get('[data-test-subj="euiDataGridExpansionPopover"]')
.should('have.css', 'left', '1px')
- .should('have.css', 'top', '80px')
+ .should('have.css', 'top', '72px')
.should('have.css', 'width', '112px');
});
@@ -235,29 +229,17 @@ describe('EuiDataGridCellPopover', () => {
openCellPopover('B');
cy.get('[data-test-subj="euiDataGridExpansionPopover"]')
.should('have.css', 'left', '109px')
- .should('have.css', 'top', '80px')
+ .should('have.css', 'top', '72px')
.should('have.css', 'width', '375px');
});
- it('right aligned popover', () => {
- cy.realMount();
-
- openCellPopover('C');
-
- // Matchers used due to subpixel rendering shenanigans
- cy.get('[data-test-subj="euiDataGridExpansionPopover"]')
- .should('have.css', 'top', '80px')
- .should('have.css', 'left')
- .and('match', /^255[.\d]+px$/);
- cy.get('[data-test-subj="euiDataGridExpansionPopover"]')
- .should('have.css', 'width')
- .and('match', /^143[.\d]+px$/);
- });
-
describe('max popover dimensions', () => {
it('never exceeds 75% of the viewport width or 50% of the viewport height', () => {
cy.viewport(300, 200);
- cy.realMount();
+ // Popover renders correctly when the Cypress viewport is scrolled to the left
+ // but not if it's scrolled to the right, hence this column workaround.
+ // I can't reproduce this bug in Storybook or production datagrids 🤷
+ cy.realMount();
openCellPopover('B');
cy.get('[data-test-subj="euiDataGridExpansionPopover"]')
diff --git a/packages/eui/src/components/datagrid/body/cell/data_grid_cell_popover.styles.ts b/packages/eui/src/components/datagrid/body/cell/data_grid_cell_popover.styles.ts
new file mode 100644
index 00000000000..3e8c319adcb
--- /dev/null
+++ b/packages/eui/src/components/datagrid/body/cell/data_grid_cell_popover.styles.ts
@@ -0,0 +1,36 @@
+/*
+ * 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 { euiScrollBarStyles, euiCanAnimate } from '../../../../global_styling';
+
+export const euiDataGridCellPopoverStyles = (euiThemeContext: UseEuiTheme) => {
+ const { euiTheme } = euiThemeContext;
+
+ return {
+ euiDataGridRowCell__popover: css`
+ ${euiScrollBarStyles(euiThemeContext)}
+ overflow: auto;
+
+ /* For some reason, the normal popover opacity transition doesn't work for datagrid popovers
+ * so we'll force it via an animation. If we don't, cells constrained by the inline max-width
+ * style that we set will see a flash of unwanted content before repositioning */
+ ${euiCanAnimate} {
+ animation-duration: ${euiTheme.animation.normal};
+ animation-name: ${fadeIn};
+ }
+ `,
+ };
+};
+
+const fadeIn = keyframes`
+ from { opacity: 0; }
+ to { opacity: 1; }
+`;
diff --git a/packages/eui/src/components/datagrid/body/cell/data_grid_cell_popover.test.tsx b/packages/eui/src/components/datagrid/body/cell/data_grid_cell_popover.test.tsx
index 47b45dcdc5c..b8ab7d90267 100644
--- a/packages/eui/src/components/datagrid/body/cell/data_grid_cell_popover.test.tsx
+++ b/packages/eui/src/components/datagrid/body/cell/data_grid_cell_popover.test.tsx
@@ -8,10 +8,9 @@
import React from 'react';
import { createEvent, fireEvent } from '@testing-library/react';
-import { shallow } from 'enzyme';
-
import { render, renderHook, renderHookAct } from '../../../../test/rtl';
import { keys } from '../../../../services';
+
import { DataGridCellPopoverContextShape } from '../../data_grid_types';
import { useCellPopover, DefaultCellPopover } from './data_grid_cell_popover';
@@ -97,42 +96,38 @@ describe('useCellPopover', () => {
expect(result.current.cellPopover).toBeFalsy(); // Should be empty on init
populateCellPopover(result.current.cellPopoverContext);
- expect(result.current.cellPopover).toMatchInlineSnapshot(`
- }
- closePopover={[Function]}
- display="block"
- focusTrapProps={
- {
- "clickOutsideDisables": false,
- "onClickOutside": [Function],
- }
- }
- hasArrow={false}
- isOpen={true}
- onKeyDown={[Function]}
- panelClassName="euiDataGridRowCell__popover"
- panelPaddingSize="s"
- panelProps={
- {
- "data-test-subj": "euiDataGridExpansionPopover",
- }
- }
- panelStyle={
- {
- "maxBlockSize": "50vh",
- "maxInlineSize": "min(75vw, max(0px, 400px))",
- }
- }
- repositionToCrossAxis={false}
+ expect(result.current.cellPopover).toBeTruthy();
+
+ const { baseElement } = render(<>{result.current.cellPopover}>);
+ expect(baseElement.querySelector('.euiDataGridRowCell__popover'))
+ .toMatchInlineSnapshot(`
+
-
- Hello world
+ You are in a dialog. Press Escape, or tap/click outside the dialog to close.
+
+