Introduce IsScrollable on ScrollWrapper (#9724)

It's beautiful!

Using same techniques as for the Header! Was not that easy but looks
very nice now!
This commit is contained in:
Charles Bochet
2025-01-17 19:26:34 +01:00
committed by GitHub
parent 18dea07344
commit 8572471973
8 changed files with 93 additions and 43 deletions

View File

@ -3,6 +3,7 @@ import { isNonEmptyString } from '@sniptt/guards';
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector'; import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; 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 { RecordTableStickyEffect } from '@/object-record/record-table/components/RecordTableStickyEffect';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId'; import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
@ -26,6 +27,10 @@ const StyledTable = styled.table`
border-spacing: 0; border-spacing: 0;
table-layout: fixed; table-layout: fixed;
width: 100%; width: 100%;
.footer-sticky tr:nth-last-child(2) td {
border-bottom-color: ${({ theme }) => theme.background.transparent};
}
`; `;
export const RecordTable = () => { export const RecordTable = () => {
@ -90,6 +95,7 @@ export const RecordTable = () => {
<RecordTableRecordGroupsBody /> <RecordTableRecordGroupsBody />
)} )}
<RecordTableStickyEffect /> <RecordTableStickyEffect />
<RecordTableStickyBottomEffect />
</StyledTable> </StyledTable>
<DragSelect <DragSelect
dragSelectable={tableBodyRef} dragSelectable={tableBodyRef}

View File

@ -28,7 +28,7 @@ export const RecordTableNoRecordGroupRows = () => {
})} })}
<RecordTableBodyFetchMoreLoader /> <RecordTableBodyFetchMoreLoader />
{!isRecordTableInitialLoading && allRecordIds.length > 0 && ( {!isRecordTableInitialLoading && allRecordIds.length > 0 && (
<RecordTableAggregateFooter endOfTableSticky /> <RecordTableAggregateFooter />
)} )}
</> </>
); );

View File

@ -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 <></>;
};

View File

@ -3,12 +3,9 @@ import { styled } from '@linaria/react';
import { ReactNode, useContext } from 'react'; import { ReactNode, useContext } from 'react';
import { MOBILE_VIEWPORT, ThemeContext } from 'twenty-ui'; import { MOBILE_VIEWPORT, ThemeContext } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined';
export const RECORD_TABLE_TD_WIDTH = '32px'; export const RECORD_TABLE_TD_WIDTH = '32px';
const StyledTd = styled.td<{ const StyledTd = styled.td<{
zIndex?: number;
backgroundColor: string; backgroundColor: string;
borderColor: string; borderColor: string;
isDragging?: boolean; isDragging?: boolean;
@ -33,7 +30,6 @@ const StyledTd = styled.td<{
text-align: left; text-align: left;
background: ${({ backgroundColor }) => backgroundColor}; background: ${({ backgroundColor }) => backgroundColor};
z-index: ${({ zIndex }) => (isDefined(zIndex) ? zIndex : 'auto')};
${({ isDragging }) => ${({ isDragging }) =>
isDragging isDragging
? ` ? `
@ -53,7 +49,6 @@ const StyledTd = styled.td<{
export const RecordTableTd = ({ export const RecordTableTd = ({
children, children,
zIndex,
isSelected, isSelected,
isDragging, isDragging,
sticky, sticky,
@ -67,7 +62,6 @@ export const RecordTableTd = ({
}: { }: {
className?: string; className?: string;
children?: ReactNode; children?: ReactNode;
zIndex?: number;
isSelected?: boolean; isSelected?: boolean;
isDragging?: boolean; isDragging?: boolean;
sticky?: boolean; sticky?: boolean;
@ -90,7 +84,6 @@ export const RecordTableTd = ({
return ( return (
<StyledTd <StyledTd
isDragging={isDragging} isDragging={isDragging}
zIndex={zIndex}
backgroundColor={tdBackgroundColor} backgroundColor={tdBackgroundColor}
borderColor={borderColor} borderColor={borderColor}
fontColor={fontColor} fontColor={fontColor}

View File

@ -6,6 +6,7 @@ import { FIRST_TH_WIDTH } from '@/object-record/record-table/record-table-header
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState'; import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isUndefined } from '@sniptt/guards';
import { MOBILE_VIEWPORT } from 'twenty-ui'; import { MOBILE_VIEWPORT } from 'twenty-ui';
const StyledTd = styled.td` const StyledTd = styled.td`
@ -13,26 +14,30 @@ const StyledTd = styled.td`
`; `;
const StyledTableRow = styled.tr<{ const StyledTableRow = styled.tr<{
endOfTableSticky?: boolean;
hasHorizontalOverflow?: boolean; hasHorizontalOverflow?: boolean;
}>` }>`
td { z-index: 5;
border-top: 1px solid ${({ theme }) => theme.border.color.light}; 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; cursor: pointer;
td:nth-of-type(1) { td:nth-of-type(1) {
width: ${FIRST_TH_WIDTH}; width: ${FIRST_TH_WIDTH};
left: 0; left: 0;
border-right-color: ${({ theme }) => theme.background.primary};
border-top: none; border-top: none;
} }
td:nth-of-type(2) {
border-right-color: ${({ theme }) => theme.background.primary};
}
&.first-columns-sticky { &.first-columns-sticky {
td:nth-of-type(2) { td:nth-of-type(2) {
position: sticky; position: sticky;
z-index: 5; z-index: 10;
transition: 0.3s ease; transition: 0.3s ease;
&::after { &::after {
content: ''; content: '';
@ -50,37 +55,32 @@ const StyledTableRow = styled.tr<{
} }
} }
} }
position: sticky;
z-index: 5;
background: ${({ theme }) => theme.background.primary}; background: ${({ theme }) => theme.background.primary};
${({ endOfTableSticky, hasHorizontalOverflow }) => ${({ hasHorizontalOverflow }) =>
endOfTableSticky && `.footer-sticky {
` bottom: ${hasHorizontalOverflow ? '10px' : '0'};
bottom: ${hasHorizontalOverflow ? '10px' : '0'}; ${
${ hasHorizontalOverflow &&
hasHorizontalOverflow && `
` &::after {
&::after { content: '';
content: ''; position: absolute;
position: absolute; bottom: -10px;
bottom: -10px; left: 0;
left: 0; right: 0;
right: 0; height: 10px;
height: 10px; background: inherit;
background: inherit; }
} }
` `
} }
`} `}
`; `;
export const RecordTableAggregateFooter = ({ export const RecordTableAggregateFooter = ({
currentRecordGroupId, currentRecordGroupId,
endOfTableSticky,
}: { }: {
currentRecordGroupId?: string; currentRecordGroupId?: string;
endOfTableSticky?: boolean;
}) => { }) => {
const visibleTableColumns = useRecoilComponentValueV2( const visibleTableColumns = useRecoilComponentValueV2(
visibleTableColumnsComponentSelector, visibleTableColumnsComponentSelector,
@ -99,8 +99,9 @@ export const RecordTableAggregateFooter = ({
<StyledTableRow <StyledTableRow
id={`record-table-footer${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`} id={`record-table-footer${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`}
data-select-disable data-select-disable
endOfTableSticky={endOfTableSticky} hasHorizontalOverflow={
hasHorizontalOverflow={hasHorizontalOverflow} hasHorizontalOverflow && isUndefined(currentRecordGroupId)
}
> >
<StyledTd /> <StyledTd />
{visibleTableColumns.map((column, index) => { {visibleTableColumns.map((column, index) => {

View File

@ -6,10 +6,6 @@ const StyledTr = styled.tr<{ isDragging: boolean }>`
? `1px solid ${theme.border.color.medium}` ? `1px solid ${theme.border.color.medium}`
: '1px solid transparent'}; : '1px solid transparent'};
transition: border-left-color 0.2s ease-in-out; transition: border-left-color 0.2s ease-in-out;
&:nth-last-child(2) td {
border-bottom: none;
}
`; `;
export const RecordTableTr = StyledTr; export const RecordTableTr = StyledTr;

View File

@ -9,6 +9,7 @@ import {
} from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts'; } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext'; 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 { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
import { scrollWrapperScrollLeftComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollLeftComponentState'; import { scrollWrapperScrollLeftComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollLeftComponentState';
import { scrollWrapperScrollTopComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollTopComponentState'; import { scrollWrapperScrollTopComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollTopComponentState';
@ -106,10 +107,18 @@ export const ScrollWrapper = ({
componentInstanceId, componentInstanceId,
); );
const setScrollBottom = useSetRecoilComponentStateV2(
scrollWrapperScrollBottomComponentState,
componentInstanceId,
);
const handleScroll = (overlayScroll: OverlayScrollbars) => { const handleScroll = (overlayScroll: OverlayScrollbars) => {
const target = overlayScroll.elements().scrollOffsetElement; const target = overlayScroll.elements().scrollOffsetElement;
setScrollTop(target.scrollTop); setScrollTop(target.scrollTop);
setScrollLeft(target.scrollLeft); setScrollLeft(target.scrollLeft);
setScrollBottom(
target.scrollHeight - target.clientHeight - target.scrollTop,
);
}; };
const setOverlayScrollbars = useSetRecoilComponentStateV2( const setOverlayScrollbars = useSetRecoilComponentStateV2(
@ -129,6 +138,12 @@ export const ScrollWrapper = ({
}, },
}, },
events: { events: {
updated: (osInstance) => {
const { scrollOffsetElement: target } = osInstance.elements();
setScrollBottom(
target.scrollHeight - target.clientHeight - target.scrollTop,
);
},
scroll: (osInstance) => { scroll: (osInstance) => {
const { scrollOffsetElement: target, scrollbarVertical } = const { scrollOffsetElement: target, scrollbarVertical } =
osInstance.elements(); osInstance.elements();

View File

@ -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<number>({
key: 'scrollWrapperScrollBottomComponentState',
defaultValue: 0,
componentInstanceContext: ScrollWrapperComponentInstanceContext,
});