Add back shadow on frozen columns and header in table (#14593)

This PR adds back box-shadow on header and first frozen columns of the
table, they appear at scroll.

With the recent refactor and the removal of a lot of re-render, and the
usage of CSS variables, it is now completely fluid.

# Before 

<img width="500" height="234" alt="image"
src="https://github.com/user-attachments/assets/9735acb0-5951-4a2d-845c-49e917e99053"
/>

<img width="576" height="242" alt="image"
src="https://github.com/user-attachments/assets/328654bf-97c7-43a2-86b1-0f100ed60daa"
/>

# After 

<img width="455" height="250" alt="image"
src="https://github.com/user-attachments/assets/3450a63f-4d6f-45ed-bfd2-3c10856b21a7"
/>

<img width="488" height="244" alt="image"
src="https://github.com/user-attachments/assets/1057a391-3a98-4b5e-97f1-890f79e6f8d0"
/>
This commit is contained in:
Lucas Bordeau
2025-09-19 10:40:32 +02:00
committed by GitHub
parent 6d5635f4e2
commit 163890f6c8
6 changed files with 85 additions and 36 deletions
@@ -1,25 +1,25 @@
import { RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableHorizontalScrollShadowVisibilityCssVariableName';
import { RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableVerticalScrollShadowVisibilityCssVariableName';
import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState';
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
import { updateRecordTableCSSVariable } from '@/object-record/record-table/utils/updateRecordTableCSSVariable';
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const RecordTableScrollAndZIndexEffect = () => {
const { scrollWrapperHTMLElement } = useScrollWrapperHTMLElement();
const setIsRecordTableScrolledHorizontally = useSetRecoilComponentState(
isRecordTableScrolledHorizontallyComponentState,
);
const [
isRecordTableScrolledHorizontally,
setIsRecordTableScrolledHorizontally,
] = useRecoilComponentState(isRecordTableScrolledHorizontallyComponentState);
const setIsRecordTableScrolledVertically = useSetRecoilComponentState(
isRecordTableScrolledVerticallyComponentState,
);
const [isScrolledVertically, setIsScrolledVertically] = useState(false);
const [isScrolledHorizontally, setIsScrolledHorizontally] = useState(false);
const [isRecordTableScrolledVertically, setIsRecordTableScrolledVertically] =
useRecoilComponentState(isRecordTableScrolledVerticallyComponentState);
useEffect(() => {
if (!isDefined(scrollWrapperHTMLElement)) {
@@ -29,29 +29,35 @@ export const RecordTableScrollAndZIndexEffect = () => {
const handleScroll = (event: any) => {
const target = event.currentTarget;
let somethingHasChanged = false;
const newIsScrolledVertically = target?.scrollTop > 0;
if (newIsScrolledVertically !== isScrolledVertically) {
setIsScrolledVertically(newIsScrolledVertically);
if (newIsScrolledVertically !== isRecordTableScrolledVertically) {
setIsRecordTableScrolledVertically(newIsScrolledVertically);
somethingHasChanged = true;
const newVisibilityOfShadows = newIsScrolledVertically
? 'visible'
: 'hidden';
updateRecordTableCSSVariable(
RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME,
newVisibilityOfShadows,
);
}
const newIsScrolledHorizontally = target?.scrollLeft > 0;
if (newIsScrolledHorizontally !== isScrolledHorizontally) {
setIsScrolledHorizontally(newIsScrolledHorizontally);
if (newIsScrolledHorizontally !== isRecordTableScrolledHorizontally) {
setIsRecordTableScrolledHorizontally(newIsScrolledHorizontally);
somethingHasChanged = true;
}
if (!somethingHasChanged) {
return;
}
const newVisibilityOfShadows = newIsScrolledHorizontally
? 'visible'
: 'hidden';
// TODO: insert imperative CSS update here
updateRecordTableCSSVariable(
RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME,
newVisibilityOfShadows,
);
}
};
scrollWrapperHTMLElement?.addEventListener('scroll', handleScroll);
@@ -61,8 +67,8 @@ export const RecordTableScrollAndZIndexEffect = () => {
};
}, [
scrollWrapperHTMLElement,
isScrolledVertically,
isScrolledHorizontally,
isRecordTableScrolledVertically,
isRecordTableScrolledHorizontally,
setIsRecordTableScrolledVertically,
setIsRecordTableScrolledHorizontally,
]);
@@ -9,14 +9,54 @@ import { RECORD_TABLE_COLUMN_LAST_EMPTY_COLUMN_WIDTH_CLASS_NAME } from '@/object
import { RECORD_TABLE_COLUMN_LAST_EMPTY_COLUMN_WIDTH_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableColumnLastEmptyColumnWidthVariableName';
import { RECORD_TABLE_COLUMN_WITH_GROUP_LAST_EMPTY_COLUMN_WIDTH_CLASS_NAME } from '@/object-record/record-table/constants/RecordTableColumnWithGroupLastEmptyColumnWidthClassName';
import { RECORD_TABLE_COLUMN_WITH_GROUP_LAST_EMPTY_COLUMN_WIDTH_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableColumnWithGroupLastEmptyColumnWidthVariableName';
import { RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableHorizontalScrollShadowVisibilityCssVariableName';
import { RECORD_TABLE_LABEL_IDENTIFIER_COLUMN_WIDTH_ON_MOBILE } from '@/object-record/record-table/constants/RecordTableLabelIdentifierColumnWidthOnMobile';
import { RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableVerticalScrollShadowVisibilityCssVariableName';
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
import { getRecordTableColumnFieldWidthClassName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthClassName';
import { getRecordTableColumnFieldWidthCSSVariableName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthCSSVariableName';
import { css, type Theme } from '@emotion/react';
import styled from '@emotion/styled';
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
const VerticalScrollBoxShadowCSS = ({ theme }: { theme: Theme }) => css`
&::before {
bottom: -1px;
box-shadow:
0px 2px 4px 0px ${theme.boxShadow.color},
0px 0px 4px 0px ${theme.boxShadow.color};
clip-path: inset(0px 0px -4px 0px);
content: '';
height: 4px;
position: absolute;
visibility: var(
${RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME},
hidden
);
width: 100%;
}
`;
const HorizontalScrollBoxShadowCSS = ({ theme }: { theme: Theme }) => css`
&::after {
content: '';
position: absolute;
top: -1px;
height: calc(100% + 2px);
width: 4px;
right: -1px;
box-shadow:
2px 0px 4px 0px ${theme.boxShadow.color},
0px 0px 4px 0px ${theme.boxShadow.color};
clip-path: inset(0px -4px 0px 0px);
visibility: var(
${RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME},
hidden
);
}
`;
const StyledTable = styled.div<{
isDragging?: boolean;
visibleRecordFields: RecordField[];
@@ -35,6 +75,8 @@ const StyledTable = styled.div<{
div.header-cell {
position: sticky;
top: 0;
${VerticalScrollBoxShadowCSS}
}
div.header-cell:nth-of-type(n + 5) {
@@ -78,16 +120,7 @@ const StyledTable = styled.div<{
? TABLE_Z_INDEX.headerColumns.withGroups.headerColumnsSticky
: TABLE_Z_INDEX.headerColumns.withoutGroups.headerColumnsSticky};
// &::after {
// content: '';
// position: absolute;
// top: -1px;
// height: calc(100% + 2px);
// width: 4px;
// right: 0px;
// box-shadow: ${({ theme }) => theme.boxShadow.light};
// clip-path: inset(0px -4px 0px 0px);
// }
${HorizontalScrollBoxShadowCSS}
@media (max-width: ${MOBILE_VIEWPORT}px) {
width: ${RECORD_TABLE_LABEL_IDENTIFIER_COLUMN_WIDTH_ON_MOBILE}px;
@@ -117,6 +150,8 @@ const StyledTable = styled.div<{
div.table-cell-0-0 {
position: sticky;
left: 48px;
${HorizontalScrollBoxShadowCSS}
}
div.table-cell:nth-of-type(3) {
@@ -126,6 +161,8 @@ const StyledTable = styled.div<{
hasRecordGroups
? TABLE_Z_INDEX.cell.withGroups.sticky
: TABLE_Z_INDEX.cell.withoutGroups.sticky};
${HorizontalScrollBoxShadowCSS}
}
div.${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH_CLASS_NAME} {
@@ -0,0 +1,2 @@
export const RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME =
'--record-table-horizontal-scroll-visibility';
@@ -0,0 +1,2 @@
export const RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME =
'--record-table-vertical-scroll-visibility';
@@ -2,6 +2,7 @@ import { GRAY_SCALE } from './GrayScale';
import { RGBA } from './Rgba';
export const BOX_SHADOW_DARK = {
color: RGBA(GRAY_SCALE.gray100, 0.6),
light: `0px 2px 4px 0px ${RGBA(
GRAY_SCALE.gray100,
0.04,
@@ -2,6 +2,7 @@ import { GRAY_SCALE } from './GrayScale';
import { RGBA } from './Rgba';
export const BOX_SHADOW_LIGHT = {
color: RGBA(GRAY_SCALE.gray100, 0.04),
light: `0px 2px 4px 0px ${RGBA(
GRAY_SCALE.gray100,
0.04,