diff --git a/changelogs/upcoming/TBD.md b/changelogs/upcoming/TBD.md new file mode 100644 index 00000000000..14e262d98d2 --- /dev/null +++ b/changelogs/upcoming/TBD.md @@ -0,0 +1,11 @@ +**CSS-in-JS conversions** + +- Removed the following `EuiTable` Sass variables: + - `$euiTableHoverColor` + - `$euiTableSelectedColor` + - `$euiTableHoverSelectedColor` + - `$euiTableActionsBorderColor` + - `$euiTableHoverClickableColor` + - `$euiTableFocusClickableColor` +- Removed the following `EuiTable` Sass mixins: + - `euiTableActionsBackgroundMobile` diff --git a/src/components/basic_table/__snapshots__/basic_table.test.tsx.snap b/src/components/basic_table/__snapshots__/basic_table.test.tsx.snap index d5b80dc5add..f7c7e222ae1 100644 --- a/src/components/basic_table/__snapshots__/basic_table.test.tsx.snap +++ b/src/components/basic_table/__snapshots__/basic_table.test.tsx.snap @@ -44,7 +44,7 @@ exports[`EuiBasicTable renders (bare-bones) 1`] = ` class="css-0" > { + const cellContentPadding = euiTheme.size.s; + const compressedCellContentPadding = euiTheme.size.xs; + + const mobileSizes = { + actions: { + width: euiTheme.size.xxl, + offset: mathWithUnits(cellContentPadding, (x) => x * 2), + }, + checkbox: { + width: mathWithUnits( + [euiTheme.size.xl, euiTheme.size.xs], + (x, y) => x + y + ), + offset: mathWithUnits(cellContentPadding, (x) => x / 2), + }, + }; + + return { + cellContentPadding, + compressedCellContentPadding, + mobileSizes, + }; +}; export const euiTableStyles = (euiThemeContext: UseEuiTheme) => { const { euiTheme } = euiThemeContext; - const cellContentPadding = euiTheme.size.s; - const compressedCellContentPadding = euiTheme.size.xs; + const { cellContentPadding, compressedCellContentPadding } = + euiTableVariables(euiThemeContext); return { euiTable: css` diff --git a/src/components/table/table_row.stories.tsx b/src/components/table/table_row.stories.tsx new file mode 100644 index 00000000000..0310aaaa0f1 --- /dev/null +++ b/src/components/table/table_row.stories.tsx @@ -0,0 +1,98 @@ +/* + * 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 type { Meta, StoryObj } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; + +import { EuiButtonIcon } from '../button'; +import { EuiCheckbox } from '../form'; +import { + EuiTable, + EuiTableBody, + EuiTableRowCell, + EuiTableRowCellCheckbox, +} from './index'; + +import { EuiTableRow, EuiTableRowProps } from './table_row'; + +const meta: Meta = { + title: 'Tabular Content/EuiTable/EuiTableRow', + component: EuiTableRow, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + argTypes: { + // For quicker/easier testing + onClick: { control: 'boolean' }, + }, + args: { + // @ts-ignore - using a switch for easiser testing + onClick: false, + // Set default booleans for easier toggling/testing + hasActions: false, + isExpandable: false, + isExpandedRow: false, + isSelectable: false, + isSelected: false, + }, + render: ({ + onClick, + isSelectable, + hasActions, + isExpandable, + isExpandedRow, + ...args + }) => ( + // Note: This is an approximate mock of what `EuiBasicTable` does for selection/actions/expansion + + + + {isSelectable && ( + + {}} + /> + + )} + First name + Last name + Some other data + {hasActions && ( + + + + )} + {isExpandable && ( + + + + )} + + {isExpandedRow && ( + + + expanded content + + + )} + + + ), +}; diff --git a/src/components/table/table_row.styles.ts b/src/components/table/table_row.styles.ts new file mode 100644 index 00000000000..5d2f4229fa7 --- /dev/null +++ b/src/components/table/table_row.styles.ts @@ -0,0 +1,205 @@ +/* + * 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, tint, shade, transparentize } from '../../services'; +import { euiBackgroundColor, logicalCSS } from '../../global_styling'; +import { euiShadow } from '../../themes/amsterdam/global_styling/mixins'; + +import { euiTableVariables } from './table.styles'; + +export const euiTableRowStyles = (euiThemeContext: UseEuiTheme) => { + const { euiTheme } = euiThemeContext; + + const rowColors = _rowColorVariables(euiThemeContext); + const expandedAnimationCss = _expandedRowAnimation(euiThemeContext); + + const { cellContentPadding, mobileSizes } = + euiTableVariables(euiThemeContext); + + return { + euiTableRow: css``, + + desktop: { + desktop: css` + &:hover { + background-color: ${rowColors.hover}; + } + `, + expanded: css` + background-color: ${rowColors.hover}; + ${expandedAnimationCss} + `, + clickable: css` + &:hover { + background-color: ${rowColors.clickable.hover}; + cursor: pointer; + } + + &:focus { + background-color: ${rowColors.clickable.focus}; + } + `, + selected: css` + &, + & + .euiTableRow-isExpandedRow { + background-color: ${rowColors.selected.color}; + } + + &:hover, + &:hover + .euiTableRow-isExpandedRow { + background-color: ${rowColors.selected.hover}; + } + `, + }, + + mobile: { + mobile: css` + position: relative; + display: flex; + flex-wrap: wrap; + padding: ${cellContentPadding}; + ${logicalCSS('margin-bottom', cellContentPadding)} + + /* EuiPanel styling */ + ${euiShadow(euiThemeContext, 's')} + background-color: ${euiBackgroundColor(euiThemeContext, 'plain')}; + border-radius: ${euiTheme.border.radius.medium}; + `, + selected: css` + &, + & + .euiTableRow-isExpandedRow { + background-color: ${rowColors.selected.color}; + } + `, + /** + * Left column offset (no border) + * Used for selection checkbox + */ + selectable: css` + ${logicalCSS('padding-left', mobileSizes.checkbox.width)} + + .euiTableRowCellCheckbox { + position: absolute; + ${logicalCSS('top', cellContentPadding)} + ${logicalCSS('left', mobileSizes.checkbox.offset)} + } + `, + /** + * Right column styles + border + * Used for cell actions and row expander arrow + */ + hasRightColumn: css` + ${logicalCSS('padding-right', mobileSizes.actions.width)} + + &::after { + content: ''; + position: absolute; + ${logicalCSS('vertical', 0)} + ${logicalCSS('right', mobileSizes.actions.width)} + ${logicalCSS('width', euiTheme.border.width.thin)} + background-color: ${euiTheme.border.color}; + } + `, + rightColumnContent: ` + position: absolute; + ${logicalCSS('right', 0)} + /* TODO: remove !important once euiTableRowCell is converted to Emotion */ + ${logicalCSS('min-width', '0 !important')} + ${logicalCSS('width', mobileSizes.actions.width)} + + .euiTableCellContent { + display: flex; + flex-direction: column; + align-items: center; + gap: ${euiTheme.size.s}; + padding: 0; + } + `, + get actions() { + return css` + .euiTableRowCell--hasActions { + ${this.rightColumnContent} + ${logicalCSS('top', mobileSizes.actions.offset)} + } + `; + }, + get expandable() { + return css` + .euiTableRowCell--isExpander { + ${this.rightColumnContent} + ${logicalCSS('bottom', mobileSizes.actions.offset)} + } + `; + }, + /** + * Bottom of card - expanded rows + */ + expanded: css` + ${logicalCSS('margin-top', `-${mobileSizes.actions.offset}`)} + /* Padding accounting for the checkbox is already applied via the content */ + ${logicalCSS('padding-left', cellContentPadding)} + + ${logicalCSS('border-top', euiTheme.border.thin)} + ${logicalCSS('border-top-left-radius', 0)} + ${logicalCSS('border-top-right-radius', 0)} + + .euiTableRowCell { + ${logicalCSS('width', '100%')} + } + + ${expandedAnimationCss} + `, + }, + }; +}; + +const _expandedRowAnimation = ({ euiTheme }: UseEuiTheme) => { + // Do not attempt to animate to height auto - down that road dragons lie + // @see https://github.com/elastic/eui/pull/6826 + const expandRow = keyframes` + 0% { + opacity: 0; + transform: translateY(-${euiTheme.size.m}); + } + 100% { + opacity: 1; + transform: translateY(0); + } + `; + + // Animation must be on the contents div inside, not the row itself + return css` + .euiTableCellContent { + animation: ${euiTheme.animation.fast} ${euiTheme.animation.resistance} 1 + normal none ${expandRow}; + } + `; +}; + +const _rowColorVariables = ({ euiTheme, colorMode }: UseEuiTheme) => ({ + hover: + colorMode === 'DARK' + ? euiTheme.colors.lightestShade + : tint(euiTheme.colors.lightestShade, 0.5), + selected: { + color: + colorMode === 'DARK' + ? shade(euiTheme.colors.primary, 0.7) + : tint(euiTheme.colors.primary, 0.96), + hover: + colorMode === 'DARK' + ? shade(euiTheme.colors.primary, 0.75) + : tint(euiTheme.colors.primary, 0.9), + }, + clickable: { + hover: transparentize(euiTheme.colors.primary, 0.05), + focus: transparentize(euiTheme.colors.primary, 0.1), + }, +}); diff --git a/src/components/table/table_row.tsx b/src/components/table/table_row.tsx index f937a35d0ad..2dc42435146 100644 --- a/src/components/table/table_row.tsx +++ b/src/components/table/table_row.tsx @@ -15,7 +15,10 @@ import React, { } from 'react'; import classNames from 'classnames'; import { CommonProps } from '../common'; -import { keys } from '../../services'; +import { keys, useEuiMemoizedStyles } from '../../services'; + +import { useEuiTableIsResponsive } from './mobile/responsive_context'; +import { euiTableRowStyles } from './table_row.styles'; export interface EuiTableRowProps { /** @@ -59,6 +62,28 @@ export const EuiTableRow: FunctionComponent = ({ onClick, ...rest }) => { + const isResponsive = useEuiTableIsResponsive(); + const styles = useEuiMemoizedStyles(euiTableRowStyles); + const cssStyles = isResponsive + ? [ + styles.euiTableRow, + styles.mobile.mobile, + isSelected && styles.mobile.selected, + isSelectable && styles.mobile.selectable, + hasActions && styles.mobile.actions, + isExpandable && styles.mobile.expandable, + isExpandedRow && styles.mobile.expanded, + (hasActions || isExpandable || isExpandedRow) && + styles.mobile.hasRightColumn, + ] + : [ + styles.euiTableRow, + styles.desktop.desktop, + isSelected && styles.desktop.selected, + isExpandedRow && styles.desktop.expanded, + onClick && styles.desktop.clickable, + ]; + const classes = classNames('euiTableRow', className, { 'euiTableRow-isSelectable': isSelectable, 'euiTableRow-isSelected': isSelected, @@ -70,7 +95,7 @@ export const EuiTableRow: FunctionComponent = ({ if (!onClick) { return ( - + {children} ); @@ -90,6 +115,7 @@ export const EuiTableRow: FunctionComponent = ({ return ( = ({ }, ...rest }) => { + const isResponsive = useEuiTableIsResponsive(); + const cellClasses = classNames('euiTableRowCell', { 'euiTableRowCell--hasActions': hasActions, 'euiTableRowCell--isExpander': isExpander, @@ -169,10 +171,11 @@ export const EuiTableRowCell: FunctionComponent = ({ euiTableCellContent__hoverItem: showOnHover, }); - const widthValue = - useEuiTableIsResponsive() && mobileOptions.width - ? mobileOptions.width - : width; + const widthValue = isResponsive + ? hasActions || isExpander + ? undefined // On mobile, actions are shifted to a right column via CSS + : mobileOptions.width + : width; const styleObj = resolveWidthAsStyle(style, widthValue);