diff --git a/packages/eui/src/components/datagrid/body/cell/data_grid_cell.styles.spec.tsx b/packages/eui/src/components/datagrid/body/cell/data_grid_cell.styles.spec.tsx
new file mode 100644
index 000000000000..a77dbae6db39
--- /dev/null
+++ b/packages/eui/src/components/datagrid/body/cell/data_grid_cell.styles.spec.tsx
@@ -0,0 +1,277 @@
+/*
+ * 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 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(70, 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
index eabc61a23ca2..5150ad882157 100644
--- 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
@@ -28,7 +28,7 @@ export const euiDataGridCellOutlineStyles = ({ euiTheme }: UseEuiTheme) => {
focusColor,
focusStyles: `
/* Remove outline as we're handling it manually. Needed to override global styles */
- &:focus:focus-visible {
+ &:focus-visible {
outline: none;
}
@@ -49,27 +49,68 @@ export const euiDataGridCellOutlineStyles = ({ euiTheme }: UseEuiTheme) => {
border-color: ${hoverColor};
}
`,
- rowCellFocusSelectors: [
- ':focus', // cell has been clicked or keyboard navigated to
- '.euiDataGridRowCell--open', // always show when the cell expansion popover is open
- '[data-keyboard-closing]', // prevents the animation from replaying when keyboard focus is moved from the popover back to the cell
- ].join(', '),
+ };
+};
+
+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 */
- &:hover,
- ${cellOutline.rowCellFocusSelectors} {
+ ${outlineSelectors.show} {
${cellOutline.focusStyles}
}
- &:hover:not(${cellOutline.rowCellFocusSelectors}) {
+ ${outlineSelectors.hover} {
+ ${cellOutline.hoverStyles}
+ }
+
+ ${outlineSelectors.focusTrapped} {
${cellOutline.hoverStyles}
}
`,
diff --git a/packages/eui/src/components/datagrid/body/cell/data_grid_cell_actions.styles.ts b/packages/eui/src/components/datagrid/body/cell/data_grid_cell_actions.styles.ts
index 0a7a633cbe69..c551ee37d1ab 100644
--- a/packages/eui/src/components/datagrid/body/cell/data_grid_cell_actions.styles.ts
+++ b/packages/eui/src/components/datagrid/body/cell/data_grid_cell_actions.styles.ts
@@ -17,14 +17,21 @@ import {
} from '../../../../global_styling';
import { euiDataGridVariables } from '../../data_grid.styles';
-import { euiDataGridCellOutlineStyles } from './data_grid_cell.styles';
+import {
+ euiDataGridCellOutlineStyles,
+ euiDataGridCellOutlineSelectors,
+} from './data_grid_cell.styles';
export const euiDataGridCellActionsStyles = (euiThemeContext: UseEuiTheme) => {
const { euiTheme } = euiThemeContext;
const { levels } = euiDataGridVariables(euiThemeContext);
- const cellOutline = euiDataGridCellOutlineStyles(euiThemeContext);
const borderWidth = euiTheme.border.width.thin;
+ const cellOutline = euiDataGridCellOutlineStyles(euiThemeContext);
+ const { actions: cellSelectors } = euiDataGridCellOutlineSelectors(
+ '.euiDataGridRowCell'
+ );
+
return {
euiDataGridRowCell__actionsWrapper: css`
position: absolute;
@@ -40,20 +47,16 @@ export const euiDataGridCellActionsStyles = (euiThemeContext: UseEuiTheme) => {
z-index: ${levels.stickyHeader + 1};
}
- /* If a cell is not hovered nor focused nor open via popover, don't show the actions */
- .euiDataGridRowCell:not(:hover, ${cellOutline.rowCellFocusSelectors}) & {
- display: none;
- }
-
- /* Increase non-visible hitbox of cell on hover, to reduce UX friction
- * for users mousing from the cell diagonally over to the actions */
- .euiDataGridRowCell:hover:not(${cellOutline.rowCellFocusSelectors}) & {
+ /* Increase non-visible hover zone, to reduce UX friction for
+ * users mousing from the cell diagonally over to the actions */
+ ${cellSelectors.hoverZone} & {
${logicalCSS('min-width', '50%')}
${logicalCSS('padding-right', euiTheme.size.base)}
}
`,
euiDataGridRowCell__actions: css`
+ position: relative;
display: flex;
gap: ${euiTheme.size.xxs};
${logicalCSS('width', 'fit-content')}
@@ -73,14 +76,20 @@ export const euiDataGridCellActionsStyles = (euiThemeContext: UseEuiTheme) => {
${logicalCSS('top', '100%')}
${logicalCSS('left', `-${borderWidth}`)}
${logicalSizeCSS(mathWithUnits(borderWidth, (x) => x * 2))}
- background-color: ${cellOutline.focusColor};
+ background-color: inherit;
+ }
+
+ /* When hovered and not focused, cell actions should match the gray focus outline */
+ ${cellSelectors.hoverColor} & {
+ background-color: ${cellOutline.hoverColor};
+ border-color: ${cellOutline.hoverColor};
}
${euiCanAnimate} {
transform: scaleY(0);
transform-origin: bottom;
- .euiDataGridRowCell:is(:hover, ${cellOutline.rowCellFocusSelectors}) & {
+ ${cellSelectors.showAnimation} & {
animation-duration: ${euiTheme.animation.fast};
animation-name: ${slideUp};
animation-iteration-count: 1;
@@ -88,14 +97,8 @@ export const euiDataGridCellActionsStyles = (euiThemeContext: UseEuiTheme) => {
}
/* Delay the actions showing on hover only, show instantly otherwise */
- .euiDataGridRowCell:hover:not(${cellOutline.rowCellFocusSelectors}) & {
- animation-delay: ${euiTheme.animation.slow}; /* 2 */
- background-color: ${cellOutline.hoverColor};
- border-color: ${cellOutline.hoverColor};
-
- &::after {
- background-color: ${cellOutline.hoverColor};
- }
+ ${cellSelectors.hoverAnimation} & {
+ animation-delay: ${euiTheme.animation.slow};
}
}
`,
diff --git a/packages/eui/src/components/datagrid/body/header/data_grid_header_cell_wrapper.styles.ts b/packages/eui/src/components/datagrid/body/header/data_grid_header_cell_wrapper.styles.ts
index 7655a8db29cd..5ea2bfc551a0 100644
--- a/packages/eui/src/components/datagrid/body/header/data_grid_header_cell_wrapper.styles.ts
+++ b/packages/eui/src/components/datagrid/body/header/data_grid_header_cell_wrapper.styles.ts
@@ -10,7 +10,10 @@ import { css } from '@emotion/react';
import { UseEuiTheme } from '../../../../services';
-import { euiDataGridCellOutlineStyles } from '../cell/data_grid_cell.styles';
+import {
+ euiDataGridCellOutlineStyles,
+ euiDataGridCellOutlineSelectors,
+} from '../cell/data_grid_cell.styles';
/**
* Styles that apply to both control and non-control columns
@@ -18,15 +21,19 @@ import { euiDataGridCellOutlineStyles } from '../cell/data_grid_cell.styles';
export const euiDataGridHeaderCellWrapperStyles = (
euiThemeContext: UseEuiTheme
) => {
- const { focusStyles } = euiDataGridCellOutlineStyles(euiThemeContext);
+ const { focusStyles, hoverStyles } =
+ euiDataGridCellOutlineStyles(euiThemeContext);
+ const { header: outlineSelectors } = euiDataGridCellOutlineSelectors();
return {
euiDataGridHeaderCell: css`
- &:focus,
- &:has(.euiDataGridHeaderCell__button:focus),
- &.euiDataGridHeaderCell--isActionsPopoverOpen {
+ ${outlineSelectors.focus} {
${focusStyles}
}
+
+ ${outlineSelectors.focusTrapped} {
+ ${hoverStyles}
+ }
`,
};
};