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 { 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 = () => {
<RecordTableRecordGroupsBody />
)}
<RecordTableStickyEffect />
<RecordTableStickyBottomEffect />
</StyledTable>
<DragSelect
dragSelectable={tableBodyRef}

View File

@ -28,7 +28,7 @@ export const RecordTableNoRecordGroupRows = () => {
})}
<RecordTableBodyFetchMoreLoader />
{!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 { 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 (
<StyledTd
isDragging={isDragging}
zIndex={zIndex}
backgroundColor={tdBackgroundColor}
borderColor={borderColor}
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 { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isUndefined } from '@sniptt/guards';
import { MOBILE_VIEWPORT } from 'twenty-ui';
const StyledTd = styled.td`
@ -13,26 +14,30 @@ const StyledTd = styled.td`
`;
const StyledTableRow = styled.tr<{
endOfTableSticky?: boolean;
hasHorizontalOverflow?: boolean;
}>`
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 = ({
<StyledTableRow
id={`record-table-footer${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`}
data-select-disable
endOfTableSticky={endOfTableSticky}
hasHorizontalOverflow={hasHorizontalOverflow}
hasHorizontalOverflow={
hasHorizontalOverflow && isUndefined(currentRecordGroupId)
}
>
<StyledTd />
{visibleTableColumns.map((column, index) => {

View File

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

View File

@ -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();

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,
});