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 ( -
- - - -
+ + {(stylesMemoizer) => { + const styles = stylesMemoizer(euiDataGridRowCellStyles); + const cssStyles = [styles.euiDataGridRowCell, cellProps?.css]; + return ( +
+ + + +
+ ); + }} +
); } 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 new file mode 100644 index 00000000000..c551ee37d1a --- /dev/null +++ b/packages/eui/src/components/datagrid/body/cell/data_grid_cell_actions.styles.ts @@ -0,0 +1,136 @@ +/* + * 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 { + euiCanAnimate, + logicalCSS, + logicalSizeCSS, + mathWithUnits, +} from '../../../../global_styling'; + +import { euiDataGridVariables } from '../../data_grid.styles'; +import { + euiDataGridCellOutlineStyles, + euiDataGridCellOutlineSelectors, +} from './data_grid_cell.styles'; + +export const euiDataGridCellActionsStyles = (euiThemeContext: UseEuiTheme) => { + const { euiTheme } = euiThemeContext; + const { levels } = euiDataGridVariables(euiThemeContext); + const borderWidth = euiTheme.border.width.thin; + + const cellOutline = euiDataGridCellOutlineStyles(euiThemeContext); + const { actions: cellSelectors } = euiDataGridCellOutlineSelectors( + '.euiDataGridRowCell' + ); + + return { + euiDataGridRowCell__actionsWrapper: css` + position: absolute; + ${logicalCSS('left', 0)} + ${logicalCSS('bottom', '100%')} + + /* Sit below sticky column headers */ + z-index: ${levels.stickyHeader - 1}; + + /* 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: ${levels.stickyHeader + 1}; + } + + /* 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')} + padding-inline: ${euiTheme.size.xxs}; + ${logicalCSS('margin-bottom', `-${borderWidth}`)} + + background-color: ${cellOutline.focusColor}; + color: ${euiTheme.colors.emptyShade}; + border: ${borderWidth} solid ${cellOutline.focusColor}; + border-radius: ${cellOutline.borderRadius}; + ${logicalCSS('border-bottom-left-radius', 0)} + + /* Visual trickery - fill in the gap between the cell outline border-radius & the actions */ + &::after { + content: ''; + position: absolute; + ${logicalCSS('top', '100%')} + ${logicalCSS('left', `-${borderWidth}`)} + ${logicalSizeCSS(mathWithUnits(borderWidth, (x) => x * 2))} + 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; + + ${cellSelectors.showAnimation} & { + animation-duration: ${euiTheme.animation.fast}; + animation-name: ${slideUp}; + animation-iteration-count: 1; + animation-fill-mode: forwards; + } + + /* Delay the actions showing on hover only, show instantly otherwise */ + ${cellSelectors.hoverAnimation} & { + animation-delay: ${euiTheme.animation.slow}; + } + } + `, + + euiDataGridRowCell__actionButtonIcon: css` + ${logicalCSS('width', euiTheme.size.base)} + ${logicalCSS( + 'height', + mathWithUnits([euiTheme.size.base, euiTheme.size.xs], (x, y) => x + y) + )} + border-radius: 0; + + /* Force all cell action buttons to match EUI colors */ + /* stylelint-disable declaration-no-important */ + &, + svg { + 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 { + ${logicalCSS('width', '120%')} + ${logicalCSS('height', '100%')} + } + `, + }; +}; + +const slideUp = keyframes` + from { transform: scaleY(0); } + to { transform: scaleY(1); } +`; diff --git a/packages/eui/src/components/datagrid/body/cell/data_grid_cell_actions.test.tsx b/packages/eui/src/components/datagrid/body/cell/data_grid_cell_actions.test.tsx index f17711d7d3e..d41797821f1 100644 --- a/packages/eui/src/components/datagrid/body/cell/data_grid_cell_actions.test.tsx +++ b/packages/eui/src/components/datagrid/body/cell/data_grid_cell_actions.test.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { render } from '../../../../test/rtl'; import { EuiDataGridColumnCellAction } from '../../data_grid_types'; import { @@ -29,104 +29,71 @@ describe('EuiDataGridCellActions', () => { }; it('renders an expand button', () => { - const component = shallow(); - - expect(component).toMatchInlineSnapshot(` - -
- - - -
-
- - `); + const { getByTestSubject } = render( + + ); - const button: Function = component.find('EuiI18n').renderProp('children'); - expect(button('expandButtonTitle')).toMatchInlineSnapshot(` - + tabindex="-1" + title="Click or hit enter to interact with cell content" + type="button" + > +