@atlaskit/editor-plugin-table
Version:
Table plugin for the @atlaskit/editor
587 lines (532 loc) • 19.5 kB
JavaScript
/* eslint-disable @atlaskit/design-system/no-css-tagged-template-expression */
// eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
import { css } from '@emotion/react';
import { tableMarginTop } from '@atlaskit/editor-common/styles';
import { akEditorSmallZIndex, akEditorTableNumberColumnWidth, akEditorUnitZIndex } from '@atlaskit/editor-shared-styles';
import { TableCssClassName as ClassName } from '../types';
import { nativeStickyHeaderZIndex, tableBorderColor, tableBorderDeleteColor, tableBorderSelectedColor } from './consts';
const roundedTableCellCornerStyles = () => css`
.${ClassName.TABLE_NODE_WRAPPER} > table {
/* Round table corner cells (including merged cells that span to the edge)
and their interaction overlays. The data-reaches-* attributes are set by the
TableCell node view based on each cell's position + rowspan/colspan. */
> tbody > tr > td[data-reaches-top][data-reaches-left],
> tbody > tr > th[data-reaches-top][data-reaches-left] {
border-top-left-radius: ${"var(--ds-radius-xlarge, 12px)"};
&::after,
&.${ClassName.HOVERED_CELL_IN_DANGER}::after,
&.${ClassName.HOVERED_NO_HIGHLIGHT}.${ClassName.HOVERED_CELL_IN_DANGER}::after {
border-top-left-radius: ${"var(--ds-radius-xlarge, 12px)"};
}
}
> tbody > tr > td[data-reaches-top][data-reaches-right],
> tbody > tr > th[data-reaches-top][data-reaches-right] {
border-top-right-radius: ${"var(--ds-radius-xlarge, 12px)"};
&::after,
&.${ClassName.HOVERED_CELL_IN_DANGER}::after,
&.${ClassName.HOVERED_NO_HIGHLIGHT}.${ClassName.HOVERED_CELL_IN_DANGER}::after {
border-top-right-radius: ${"var(--ds-radius-xlarge, 12px)"};
}
}
> tbody > tr > td[data-reaches-bottom][data-reaches-left],
> tbody > tr > th[data-reaches-bottom][data-reaches-left] {
border-bottom-left-radius: ${"var(--ds-radius-xlarge, 12px)"};
&::after,
&.${ClassName.HOVERED_CELL_IN_DANGER}::after,
&.${ClassName.HOVERED_NO_HIGHLIGHT}.${ClassName.HOVERED_CELL_IN_DANGER}::after {
border-bottom-left-radius: ${"var(--ds-radius-xlarge, 12px)"};
}
}
> tbody > tr > td[data-reaches-bottom][data-reaches-right],
> tbody > tr > th[data-reaches-bottom][data-reaches-right] {
border-bottom-right-radius: ${"var(--ds-radius-xlarge, 12px)"};
&::after,
&.${ClassName.HOVERED_CELL_IN_DANGER}::after,
&.${ClassName.HOVERED_NO_HIGHLIGHT}.${ClassName.HOVERED_CELL_IN_DANGER}::after {
border-bottom-right-radius: ${"var(--ds-radius-xlarge, 12px)"};
}
}
}
`;
const roundedTableInteractionOverlayStyles = () => css`
.${ClassName.TABLE_NODE_WRAPPER} > table {
/* Active-cell highlight base properties (replaces activeCellHighlightStyles).
width/height: auto overrides the base cell ::after which uses width: 100%; height: 100%,
so that left/right/top/bottom determine the size instead. */
td.${ClassName.TABLE_CELL}.${ClassName.ACTIVE_CURSOR_CELL}::after,
th.${ClassName.TABLE_HEADER_CELL}.${ClassName.ACTIVE_CURSOR_CELL}::after {
border: 1px solid ${"var(--ds-border-selected, #1868DB)"};
box-shadow: ${"var(--ds-shadow-raised, 0px 1px 1px #1E1F2140, 0px 0px 1px #1E1F214f)"};
content: '';
position: absolute;
top: -1px;
left: -1px;
right: -1px;
bottom: -1px;
width: auto;
height: auto;
z-index: ${akEditorSmallZIndex};
pointer-events: none;
}
/* Normalize selected/hover/danger overlays to the same box model as active-cell.
width/height: auto overrides the base cell ::after which uses width: 100%; height: 100%. */
td.${ClassName.HOVERED_CELL}::after,
td.${ClassName.SELECTED_CELL}::after,
th.${ClassName.TABLE_HEADER_CELL}.${ClassName.SELECTED_CELL}::after,
th.${ClassName.TABLE_HEADER_CELL}.${ClassName.HOVERED_CELL}::after,
th.${ClassName.TABLE_HEADER_CELL}.${ClassName.HOVERED_CELL_IN_DANGER}::after,
td.${ClassName.TABLE_CELL}.${ClassName.HOVERED_CELL_IN_DANGER}::after {
left: -1px;
right: -1px;
top: -1px;
bottom: -1px;
width: auto;
height: auto;
}
/* Preserve interaction border colours on table edges without changing the shared -1px overlay inset. */
> tbody
> tr
> td:is(
.${ClassName.SELECTED_CELL}, .${ClassName.HOVERED_CELL}, .${ClassName.ACTIVE_CURSOR_CELL}
),
> tbody
> tr
> th:is(
.${ClassName.SELECTED_CELL}, .${ClassName.HOVERED_CELL}, .${ClassName.ACTIVE_CURSOR_CELL}
) {
&[data-reaches-top] {
border-top-color: transparent;
&::after {
top: -1px;
bottom: -1px;
border-top-color: ${tableBorderSelectedColor};
}
}
&[data-reaches-left] {
border-left-color: transparent;
&::after {
border-left-color: ${tableBorderSelectedColor};
}
}
&[data-reaches-right] {
border-right-color: transparent;
&::after {
border-right-color: ${tableBorderSelectedColor};
}
}
&[data-reaches-bottom] {
border-bottom-color: transparent;
&::after {
border-bottom-color: ${tableBorderSelectedColor};
}
}
}
> tbody
> tr
> td.${ClassName.HOVERED_CELL_IN_DANGER},
> tbody
> tr
> th.${ClassName.HOVERED_CELL_IN_DANGER} {
&[data-reaches-top] {
border-top-color: transparent;
&::after {
top: -1px;
bottom: -1px;
border-top-color: ${tableBorderDeleteColor};
}
}
&[data-reaches-left] {
border-left-color: transparent;
&::after {
border-left-color: ${tableBorderDeleteColor};
}
}
&[data-reaches-right] {
border-right-color: transparent;
&::after {
border-right-color: ${tableBorderDeleteColor};
}
}
&[data-reaches-bottom] {
border-bottom-color: transparent;
&::after {
border-bottom-color: ${tableBorderDeleteColor};
}
}
}
}
`;
const roundedTableNumberedColumnStyles = () => css`
/* Numbered columns are separate, so they need their own rounded edge owner. */
.${ClassName.TABLE_CONTAINER}[data-number-column='true'] {
/* Override the inline/container left border and replace it with one rounded pseudo-border. */
> .${ClassName.ROW_CONTROLS_WRAPPER}
.${ClassName.NUMBERED_COLUMN},
> .${ClassName.DRAG_ROW_CONTROLS_WRAPPER}
.${ClassName.NUMBERED_COLUMN} {
position: relative;
border-left: 0;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 100%;
border-left: 1px solid ${tableBorderColor};
border-top-left-radius: ${"var(--ds-radius-xlarge, 12px)"};
border-bottom-left-radius: ${"var(--ds-radius-xlarge, 12px)"};
pointer-events: none;
z-index: ${akEditorUnitZIndex};
}
}
/* Prevent individual number buttons from drawing a straight left border. */
> .${ClassName.ROW_CONTROLS_WRAPPER}
.${ClassName.NUMBERED_COLUMN_BUTTON},
> .${ClassName.DRAG_ROW_CONTROLS_WRAPPER}
.${ClassName.NUMBERED_COLUMN_BUTTON} {
border-left-color: transparent;
}
> .${ClassName.ROW_CONTROLS_WRAPPER}
.${ClassName.NUMBERED_COLUMN_BUTTON}.${ClassName.HOVERED_CELL_IN_DANGER},
> .${ClassName.DRAG_ROW_CONTROLS_WRAPPER}
.${ClassName.NUMBERED_COLUMN_BUTTON}.${ClassName.HOVERED_CELL_IN_DANGER},
> .${ClassName.ROW_CONTROLS_WRAPPER}
.${ClassName.NUMBERED_COLUMN_BUTTON}.${ClassName.HOVERED_CELL_ACTIVE},
> .${ClassName.DRAG_ROW_CONTROLS_WRAPPER}
.${ClassName.NUMBERED_COLUMN_BUTTON}.${ClassName.HOVERED_CELL_ACTIVE},
> .${ClassName.ROW_CONTROLS_WRAPPER}
.${ClassName.NUMBERED_COLUMN_BUTTON}.active,
> .${ClassName.DRAG_ROW_CONTROLS_WRAPPER}
.${ClassName.NUMBERED_COLUMN_BUTTON}.active {
border-left-color: transparent;
}
/* When numbered column is present, the visual left edge belongs to the number column widget.
Zero out any left-side border-radius on the cell and its overlays/pseudo-borders —
but leave right-side radii untouched so right-edge cells still round correctly.
::before is intentionally excluded here because on the sticky header it carries the
numbered-column mask which needs to round its own top-left corner (see overrides
below and in roundedTableStickyHeaderStyles). */
> .${ClassName.TABLE_NODE_WRAPPER}
> table
> tbody
> tr
> th[data-reaches-top][data-reaches-left],
> .${ClassName.TABLE_NODE_WRAPPER}
> table
> tbody
> tr
> td[data-reaches-top][data-reaches-left] {
border-top-left-radius: 0;
&::after {
border-top-left-radius: 0;
}
}
> .${ClassName.TABLE_NODE_WRAPPER}
> table
> tbody
> tr
> th[data-reaches-bottom][data-reaches-left],
> .${ClassName.TABLE_NODE_WRAPPER}
> table
> tbody
> tr
> td[data-reaches-bottom][data-reaches-left] {
border-bottom-left-radius: 0;
&::after,
&::before {
border-bottom-left-radius: 0;
}
}
/* Preserve rounded numbered-column corners across normal, active, and danger states. */
.${ClassName.NUMBERED_COLUMN_BUTTON}:first-of-type,
.${ClassName.NUMBERED_COLUMN_BUTTON}.${ClassName.HOVERED_CELL_IN_DANGER}:first-of-type,
.${ClassName.NUMBERED_COLUMN_BUTTON}.${ClassName.HOVERED_CELL_ACTIVE}:first-of-type,
.${ClassName.NUMBERED_COLUMN_BUTTON}.active:first-of-type {
border-top-left-radius: ${"var(--ds-radius-xlarge, 12px)"};
}
.${ClassName.NUMBERED_COLUMN_BUTTON}:last-of-type,
.${ClassName.NUMBERED_COLUMN_BUTTON}.${ClassName.HOVERED_CELL_IN_DANGER}:last-of-type,
.${ClassName.NUMBERED_COLUMN_BUTTON}.${ClassName.HOVERED_CELL_ACTIVE}:last-of-type,
.${ClassName.NUMBERED_COLUMN_BUTTON}.active:last-of-type {
border-bottom-left-radius: ${"var(--ds-radius-xlarge, 12px)"};
}
.${ClassName.NUMBERED_COLUMN_BUTTON}.${ClassName.HOVERED_CELL_IN_DANGER}:first-of-type::after {
border-top-left-radius: ${"var(--ds-radius-xlarge, 12px)"};
}
.${ClassName.NUMBERED_COLUMN_BUTTON}.${ClassName.HOVERED_CELL_IN_DANGER}:last-of-type::after {
border-bottom-left-radius: ${"var(--ds-radius-xlarge, 12px)"};
}
/* Sticky numbered-column mask also needs the same top-left radius. */
> .${ClassName.TABLE_NODE_WRAPPER_NO_OVERFLOW}
tr
th[data-reaches-top][data-reaches-left]::before {
border-top-left-radius: ${"var(--ds-radius-xlarge, 12px)"};
}
}
`;
const roundedTableStickyHeaderCellCornerStyles = () => css`
/* Sticky header rows have independent border/shadow/mask painting, so patch the sticky-only painters too. */
.${ClassName.TABLE_NODE_WRAPPER}
> table
> tbody
> tr.${ClassName.NATIVE_STICKY},
.${ClassName.TABLE_NODE_WRAPPER}
> table.${ClassName.TABLE_STICKY}
> tbody
> tr.sticky {
> th[data-reaches-left],
> td[data-reaches-left] {
border-top-left-radius: ${"var(--ds-radius-xlarge, 12px)"};
&::after,
&::before {
border-top-left-radius: ${"var(--ds-radius-xlarge, 12px)"};
}
}
> td[data-reaches-right],
> th[data-reaches-right] {
border-top-right-radius: ${"var(--ds-radius-xlarge, 12px)"};
&::after,
&::before {
border-top-right-radius: ${"var(--ds-radius-xlarge, 12px)"};
}
}
}
.${ClassName.TABLE_STICKY} .${ClassName.COLUMN_CONTROLS_DECORATIONS}::after {
border-left-color: transparent;
}
`;
const roundedTableStickyHeaderNumberColumnStyles = () => css`
.${ClassName.TABLE_CONTAINER}[data-number-column='true'] .${ClassName.TABLE_NODE_WRAPPER_NO_OVERFLOW} tr th[data-reaches-top][data-reaches-left]::before,
.${ClassName.TABLE_CONTAINER}[data-number-column='true'] .${ClassName.TABLE_NODE_WRAPPER_NO_OVERFLOW} tr.${ClassName.NATIVE_STICKY} th[data-reaches-left]::before,
.${ClassName.TABLE_CONTAINER}[data-number-column='true'] .${ClassName.TABLE_NODE_WRAPPER_NO_OVERFLOW} .${ClassName.NATIVE_STICKY_ACTIVE} th[data-reaches-left]::before {
border-top-left-radius: ${"var(--ds-radius-xlarge, 12px)"};
border-bottom: none;
}
`;
const roundedTableStickyHeaderCornerMaskStyles = () => css`
/* Same z-index as the sticky row; DOM order keeps the row above this mask and the table border below it. */
.${ClassName.TABLE_CORNER_MASK} {
display: none;
position: sticky;
top: calc(${tableMarginTop}px - 1px);
width: 12px;
height: 12px;
margin-bottom: -12px;
background: ${"var(--ds-surface, #FFFFFF)"};
pointer-events: none;
z-index: ${nativeStickyHeaderZIndex};
}
.${ClassName.TABLE_CONTAINER}[data-number-column='true']
.${ClassName.TABLE_CORNER_MASK}[data-corner='left'] {
margin-left: -${akEditorTableNumberColumnWidth}px;
}
.${ClassName.TABLE_CORNER_MASK}[data-corner='right'] {
left: calc(100% - 11px);
}
/*
Scope the :has() selectors to a DIRECT child table so a nested table that
has its own sticky row does not pull the OUTER wrapper's masks into the
sticky / fallback layout. The masks belong to the wrapper of the table
that owns the sticky row, never to an ancestor wrapper.
*/
.${ClassName.TABLE_NODE_WRAPPER}:has(> table > tbody > tr.${ClassName.NATIVE_STICKY_ACTIVE})
> .${ClassName.TABLE_CORNER_MASK},
.${ClassName.TABLE_NODE_WRAPPER}:has(> table.${ClassName.TABLE_STICKY} > tbody > tr.sticky)
> .${ClassName.TABLE_CORNER_MASK} {
display: block;
}
/*
When the table overflows horizontally, sticky headers fall back from native
position: sticky to a fixed-position header row. Match that positioning so
the corner masks continue to cover the rounded corners in the fallback path.
The direct-child combinator inside :has() is critical: without it a nested
table entering the legacy fallback would incorrectly trigger this rule on
the OUTER wrapper, detaching the outer table's masks from its corners.
*/
.${ClassName.TABLE_NODE_WRAPPER}:has(> table.${ClassName.TABLE_STICKY} > tbody > tr.sticky)
> .${ClassName.TABLE_CORNER_MASK} {
position: fixed;
top: 30px;
z-index: 1;
}
`;
const roundedTableStickyHeaderOverlayStyles = () => css`
/*
Paint the pinned sticky row's rounded top edge via each cell's ::after.
top: -1px places it in the reserved transparent border slot, aligned with
the row's inset-shadow bottom border. Longhand border-top-* lets
interaction-state rules override only the colour.
*/
.${ClassName.TABLE_NODE_WRAPPER}
> table
> tbody
> tr.${ClassName.NATIVE_STICKY}.${ClassName.NATIVE_STICKY_ACTIVE}
> th.${ClassName.TABLE_HEADER_CELL}[data-reaches-top]::after,
.${ClassName.TABLE_NODE_WRAPPER}
> table
> tbody
> tr.${ClassName.NATIVE_STICKY}.${ClassName.NATIVE_STICKY_ACTIVE}
> td.${ClassName.TABLE_CELL}[data-reaches-top]::after {
content: '';
position: absolute;
left: -1px;
right: -1px;
top: -1px;
bottom: -1px;
width: auto;
height: auto;
pointer-events: none;
border-top-width: 1px;
border-top-style: solid;
border-top-color: ${tableBorderColor};
}
/* Selection / hover / active-cursor colour for the painted top edge. */
.${ClassName.TABLE_NODE_WRAPPER}
> table
> tbody
> tr.${ClassName.NATIVE_STICKY}.${ClassName.NATIVE_STICKY_ACTIVE}
> th.${ClassName.TABLE_HEADER_CELL}[data-reaches-top]:is(
.${ClassName.SELECTED_CELL},
.${ClassName.HOVERED_CELL},
.${ClassName.ACTIVE_CURSOR_CELL}
)::after,
.${ClassName.TABLE_NODE_WRAPPER}
> table
> tbody
> tr.${ClassName.NATIVE_STICKY}.${ClassName.NATIVE_STICKY_ACTIVE}
> td.${ClassName.TABLE_CELL}[data-reaches-top]:is(
.${ClassName.SELECTED_CELL},
.${ClassName.HOVERED_CELL},
.${ClassName.ACTIVE_CURSOR_CELL}
)::after {
border-top-color: ${tableBorderSelectedColor};
}
/* Danger colour for the painted top edge. */
.${ClassName.TABLE_NODE_WRAPPER}
> table
> tbody
> tr.${ClassName.NATIVE_STICKY}.${ClassName.NATIVE_STICKY_ACTIVE}
> th.${ClassName.TABLE_HEADER_CELL}[data-reaches-top].${ClassName.HOVERED_CELL_IN_DANGER}::after,
.${ClassName.TABLE_NODE_WRAPPER}
> table
> tbody
> tr.${ClassName.NATIVE_STICKY}.${ClassName.NATIVE_STICKY_ACTIVE}
> td.${ClassName.TABLE_CELL}[data-reaches-top].${ClassName.HOVERED_CELL_IN_DANGER}::after {
border-top-color: ${tableBorderDeleteColor};
}
:where(.${ClassName.TABLE_NODE_WRAPPER} > table > tbody > tr.${ClassName.NATIVE_STICKY})
> th.${ClassName.TABLE_HEADER_CELL}[data-reaches-left]::after {
border-left-color: transparent;
}
`;
const roundedTableStickyHeaderShadowStyles = () => css`
/* Native sticky row */
.${ClassName.TABLE_NODE_WRAPPER}
> table
> tbody
> tr.${ClassName.NATIVE_STICKY},
/* Legacy sticky row */
.${ClassName.TABLE_NODE_WRAPPER}
> table.${ClassName.TABLE_STICKY}
> tbody
> tr.sticky {
box-shadow: inset 0 -1px ${tableBorderColor} ;
}
/*
Box-shadow below active sticky header.
*/
.${ClassName.TABLE_NODE_WRAPPER}
> table
> tbody
> tr.${ClassName.NATIVE_STICKY}.${ClassName.NATIVE_STICKY_ACTIVE} {
box-shadow:
inset 0 -1px ${tableBorderColor},
0 6px 4px -4px ${"var(--ds-shadow-overflow-perimeter, #1E1F211f)"} ;
}
/*
Sticky headers leave the table's rounded outer overlay behind, so make the
reserved 1px top-border slot visible on the pinned row.
*/
.${ClassName.TABLE_NODE_WRAPPER}
> table
> tbody
> tr.${ClassName.NATIVE_STICKY}.${ClassName.NATIVE_STICKY_ACTIVE}
> th.${ClassName.TABLE_HEADER_CELL}[data-reaches-top],
.${ClassName.TABLE_NODE_WRAPPER}
> table
> tbody
> tr.${ClassName.NATIVE_STICKY}.${ClassName.NATIVE_STICKY_ACTIVE}
> td.${ClassName.TABLE_CELL}[data-reaches-top],
.${ClassName.TABLE_NODE_WRAPPER}
> table.${ClassName.TABLE_STICKY}
> tbody
> tr.sticky
> th.${ClassName.TABLE_HEADER_CELL}[data-reaches-top],
.${ClassName.TABLE_NODE_WRAPPER}
> table.${ClassName.TABLE_STICKY}
> tbody
> tr.sticky
> td.${ClassName.TABLE_CELL}[data-reaches-top] {
border-top-color: ${tableBorderColor};
}
/*
Restore legacy sticky corner side borders after the row is pinned outside the
table's outer overlay. (Pinned row top edge is painted by the cell ::after
overlay in roundedTableStickyHeaderOverlayStyles.)
*/
.${ClassName.TABLE_NODE_WRAPPER}
> table.${ClassName.TABLE_STICKY}
> tbody
> tr.sticky
> th.${ClassName.TABLE_HEADER_CELL}[data-reaches-left],
.${ClassName.TABLE_NODE_WRAPPER}
> table.${ClassName.TABLE_STICKY}
> tbody
> tr.sticky
> td.${ClassName.TABLE_CELL}[data-reaches-left] {
border-left-color: ${tableBorderColor};
}
.${ClassName.TABLE_NODE_WRAPPER}
> table.${ClassName.TABLE_STICKY}
> tbody
> tr.sticky
> th.${ClassName.TABLE_HEADER_CELL}[data-reaches-right],
.${ClassName.TABLE_NODE_WRAPPER}
> table.${ClassName.TABLE_STICKY}
> tbody
> tr.sticky
> td.${ClassName.TABLE_CELL}[data-reaches-right] {
border-right: 1px solid ${tableBorderColor};
}
/*
Paint the legacy sticky bottom edge on cells so it aligns under display: grid.
*/
.${ClassName.TABLE_NODE_WRAPPER}
> table.${ClassName.TABLE_STICKY}
> tbody
> tr.sticky
> th.${ClassName.TABLE_HEADER_CELL},
.${ClassName.TABLE_NODE_WRAPPER}
> table.${ClassName.TABLE_STICKY}
> tbody
> tr.sticky
> td.${ClassName.TABLE_CELL} {
border-bottom: 1px solid ${tableBorderColor};
}
.${ClassName.TABLE_CONTAINER}.${ClassName.WITH_CONTROLS}:has(tr.sticky)
.${ClassName.NUMBERED_COLUMN}
.${ClassName.NUMBERED_COLUMN_BUTTON}:first-of-type {
box-shadow: none ;
}
`;
export const roundedTableOverrides = () => css`
${roundedTableCellCornerStyles()}
${roundedTableInteractionOverlayStyles()}
${roundedTableNumberedColumnStyles()}
${roundedTableStickyHeaderCellCornerStyles()}
${roundedTableStickyHeaderNumberColumnStyles()}
${roundedTableStickyHeaderCornerMaskStyles()}
${roundedTableStickyHeaderOverlayStyles()}
${roundedTableStickyHeaderShadowStyles()}
`;