From 85724719730787f41f076c44575ee2d829df479d Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Fri, 17 Jan 2025 19:26:34 +0100 Subject: [PATCH] Introduce IsScrollable on ScrollWrapper (#9724) It's beautiful! Using same techniques as for the Header! Was not that easy but looks very nice now! --- .../record-table/components/RecordTable.tsx | 6 ++ .../RecordTableNoRecordGroupRows.tsx | 2 +- .../RecordTableStickyBottomEffect.tsx | 30 +++++++++ .../components/RecordTableTd.tsx | 7 --- .../components/RecordTableAggregateFooter.tsx | 63 ++++++++++--------- .../components/RecordTableTr.tsx | 4 -- .../scroll/components/ScrollWrapper.tsx | 15 +++++ .../scrollWrappeScrollBottomComponentState.ts | 9 +++ 8 files changed, 93 insertions(+), 43 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyBottomEffect.tsx create mode 100644 packages/twenty-front/src/modules/ui/utilities/scroll/states/scrollWrappeScrollBottomComponentState.ts diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx index 4a8fde833..a0787d1ce 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx @@ -3,6 +3,7 @@ import { isNonEmptyString } from '@sniptt/guards'; import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; +import { RecordTableStickyBottomEffect } from '@/object-record/record-table/components/RecordTableStickyBottomEffect'; import { RecordTableStickyEffect } from '@/object-record/record-table/components/RecordTableStickyEffect'; import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; @@ -26,6 +27,10 @@ const StyledTable = styled.table` border-spacing: 0; table-layout: fixed; width: 100%; + + .footer-sticky tr:nth-last-child(2) td { + border-bottom-color: ${({ theme }) => theme.background.transparent}; + } `; export const RecordTable = () => { @@ -90,6 +95,7 @@ export const RecordTable = () => { )} + { })} {!isRecordTableInitialLoading && allRecordIds.length > 0 && ( - + )} ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyBottomEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyBottomEffect.tsx new file mode 100644 index 000000000..7e259e825 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyBottomEffect.tsx @@ -0,0 +1,30 @@ +import { useEffect } from 'react'; + +import { scrollWrapperScrollBottomComponentState } from '@/ui/utilities/scroll/states/scrollWrappeScrollBottomComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; + +export const RecordTableStickyBottomEffect = () => { + const scrollBottom = useRecoilComponentValueV2( + scrollWrapperScrollBottomComponentState, + ); + + useEffect(() => { + if (scrollBottom > 1) { + document + .getElementById('record-table-body') + ?.classList.add('footer-sticky'); + document + .getElementById('record-table-footer') + ?.classList.add('footer-sticky'); + } else { + document + .getElementById('record-table-body') + ?.classList.remove('footer-sticky'); + document + .getElementById('record-table-footer') + ?.classList.remove('footer-sticky'); + } + }, [scrollBottom]); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx index 1c95f0287..4f558161f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx @@ -3,12 +3,9 @@ import { styled } from '@linaria/react'; import { ReactNode, useContext } from 'react'; import { MOBILE_VIEWPORT, ThemeContext } from 'twenty-ui'; -import { isDefined } from '~/utils/isDefined'; - export const RECORD_TABLE_TD_WIDTH = '32px'; const StyledTd = styled.td<{ - zIndex?: number; backgroundColor: string; borderColor: string; isDragging?: boolean; @@ -33,7 +30,6 @@ const StyledTd = styled.td<{ text-align: left; background: ${({ backgroundColor }) => backgroundColor}; - z-index: ${({ zIndex }) => (isDefined(zIndex) ? zIndex : 'auto')}; ${({ isDragging }) => isDragging ? ` @@ -53,7 +49,6 @@ const StyledTd = styled.td<{ export const RecordTableTd = ({ children, - zIndex, isSelected, isDragging, sticky, @@ -67,7 +62,6 @@ export const RecordTableTd = ({ }: { className?: string; children?: ReactNode; - zIndex?: number; isSelected?: boolean; isDragging?: boolean; sticky?: boolean; @@ -90,7 +84,6 @@ export const RecordTableTd = ({ return ( ` - td { - border-top: 1px solid ${({ theme }) => theme.border.color.light}; + z-index: 5; + position: sticky; + border: none; + + &.footer-sticky { + td { + border-top: ${({ theme }) => `1px solid ${theme.border.color.light}`}; + z-index: 5; + position: sticky; + bottom: 0; + } } cursor: pointer; td:nth-of-type(1) { width: ${FIRST_TH_WIDTH}; left: 0; - border-right-color: ${({ theme }) => theme.background.primary}; border-top: none; } - td:nth-of-type(2) { - border-right-color: ${({ theme }) => theme.background.primary}; - } &.first-columns-sticky { td:nth-of-type(2) { position: sticky; - z-index: 5; + z-index: 10; transition: 0.3s ease; &::after { content: ''; @@ -50,37 +55,32 @@ const StyledTableRow = styled.tr<{ } } } - position: sticky; - z-index: 5; background: ${({ theme }) => theme.background.primary}; - ${({ endOfTableSticky, hasHorizontalOverflow }) => - endOfTableSticky && - ` - bottom: ${hasHorizontalOverflow ? '10px' : '0'}; - ${ - hasHorizontalOverflow && - ` - &::after { - content: ''; - position: absolute; - bottom: -10px; - left: 0; - right: 0; - height: 10px; - background: inherit; + ${({ hasHorizontalOverflow }) => + `.footer-sticky { + bottom: ${hasHorizontalOverflow ? '10px' : '0'}; + ${ + hasHorizontalOverflow && + ` + &::after { + content: ''; + position: absolute; + bottom: -10px; + left: 0; + right: 0; + height: 10px; + background: inherit; + } } ` - } + } `} `; export const RecordTableAggregateFooter = ({ currentRecordGroupId, - endOfTableSticky, }: { currentRecordGroupId?: string; - - endOfTableSticky?: boolean; }) => { const visibleTableColumns = useRecoilComponentValueV2( visibleTableColumnsComponentSelector, @@ -99,8 +99,9 @@ export const RecordTableAggregateFooter = ({ {visibleTableColumns.map((column, index) => { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx index f7ff0f425..0ccfc98ac 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx @@ -6,10 +6,6 @@ const StyledTr = styled.tr<{ isDragging: boolean }>` ? `1px solid ${theme.border.color.medium}` : '1px solid transparent'}; transition: border-left-color 0.2s ease-in-out; - - &:nth-last-child(2) td { - border-bottom: none; - } `; export const RecordTableTr = StyledTr; diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx b/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx index bdc131a28..9709422b0 100644 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx @@ -9,6 +9,7 @@ import { } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts'; import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext'; +import { scrollWrapperScrollBottomComponentState } from '@/ui/utilities/scroll/states/scrollWrappeScrollBottomComponentState'; import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState'; import { scrollWrapperScrollLeftComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollLeftComponentState'; import { scrollWrapperScrollTopComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollTopComponentState'; @@ -106,10 +107,18 @@ export const ScrollWrapper = ({ componentInstanceId, ); + const setScrollBottom = useSetRecoilComponentStateV2( + scrollWrapperScrollBottomComponentState, + componentInstanceId, + ); + const handleScroll = (overlayScroll: OverlayScrollbars) => { const target = overlayScroll.elements().scrollOffsetElement; setScrollTop(target.scrollTop); setScrollLeft(target.scrollLeft); + setScrollBottom( + target.scrollHeight - target.clientHeight - target.scrollTop, + ); }; const setOverlayScrollbars = useSetRecoilComponentStateV2( @@ -129,6 +138,12 @@ export const ScrollWrapper = ({ }, }, events: { + updated: (osInstance) => { + const { scrollOffsetElement: target } = osInstance.elements(); + setScrollBottom( + target.scrollHeight - target.clientHeight - target.scrollTop, + ); + }, scroll: (osInstance) => { const { scrollOffsetElement: target, scrollbarVertical } = osInstance.elements(); diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/states/scrollWrappeScrollBottomComponentState.ts b/packages/twenty-front/src/modules/ui/utilities/scroll/states/scrollWrappeScrollBottomComponentState.ts new file mode 100644 index 000000000..07b1cb9a0 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/states/scrollWrappeScrollBottomComponentState.ts @@ -0,0 +1,9 @@ +import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const scrollWrapperScrollBottomComponentState = + createComponentStateV2({ + key: 'scrollWrapperScrollBottomComponentState', + defaultValue: 0, + componentInstanceContext: ScrollWrapperComponentInstanceContext, + });