From 163890f6c824a0107633616b6ed00d9651eaf8b6 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 19 Sep 2025 10:40:32 +0200 Subject: [PATCH] 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 image image # After image image --- .../RecordTableScrollAndZIndexEffect.tsx | 58 ++++++++++--------- .../components/RecordTableStyleWrapper.tsx | 57 ++++++++++++++---- ...alScrollShadowVisibilityCssVariableName.ts | 2 + ...alScrollShadowVisibilityCssVariableName.ts | 2 + .../src/theme/constants/BoxShadowDark.ts | 1 + .../src/theme/constants/BoxShadowLight.ts | 1 + 6 files changed, 85 insertions(+), 36 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/constants/RecordTableHorizontalScrollShadowVisibilityCssVariableName.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/constants/RecordTableVerticalScrollShadowVisibilityCssVariableName.ts diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollAndZIndexEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollAndZIndexEffect.tsx index 0cb80e31e1..ec6df71956 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollAndZIndexEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableScrollAndZIndexEffect.tsx @@ -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, ]); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStyleWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStyleWrapper.tsx index 8656e8d8eb..580d5c1c1a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStyleWrapper.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStyleWrapper.tsx @@ -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} { diff --git a/packages/twenty-front/src/modules/object-record/record-table/constants/RecordTableHorizontalScrollShadowVisibilityCssVariableName.ts b/packages/twenty-front/src/modules/object-record/record-table/constants/RecordTableHorizontalScrollShadowVisibilityCssVariableName.ts new file mode 100644 index 0000000000..3c688eca8a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/constants/RecordTableHorizontalScrollShadowVisibilityCssVariableName.ts @@ -0,0 +1,2 @@ +export const RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME = + '--record-table-horizontal-scroll-visibility'; diff --git a/packages/twenty-front/src/modules/object-record/record-table/constants/RecordTableVerticalScrollShadowVisibilityCssVariableName.ts b/packages/twenty-front/src/modules/object-record/record-table/constants/RecordTableVerticalScrollShadowVisibilityCssVariableName.ts new file mode 100644 index 0000000000..5f74792de5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/constants/RecordTableVerticalScrollShadowVisibilityCssVariableName.ts @@ -0,0 +1,2 @@ +export const RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME = + '--record-table-vertical-scroll-visibility'; diff --git a/packages/twenty-ui/src/theme/constants/BoxShadowDark.ts b/packages/twenty-ui/src/theme/constants/BoxShadowDark.ts index fff90eaec5..f5dd8abc65 100644 --- a/packages/twenty-ui/src/theme/constants/BoxShadowDark.ts +++ b/packages/twenty-ui/src/theme/constants/BoxShadowDark.ts @@ -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, diff --git a/packages/twenty-ui/src/theme/constants/BoxShadowLight.ts b/packages/twenty-ui/src/theme/constants/BoxShadowLight.ts index 4e1d98b4a5..3173fc6fb0 100644 --- a/packages/twenty-ui/src/theme/constants/BoxShadowLight.ts +++ b/packages/twenty-ui/src/theme/constants/BoxShadowLight.ts @@ -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,