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:
@ -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}
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export const RecordTableNoRecordGroupRows = () => {
|
|||||||
})}
|
})}
|
||||||
<RecordTableBodyFetchMoreLoader />
|
<RecordTableBodyFetchMoreLoader />
|
||||||
{!isRecordTableInitialLoading && allRecordIds.length > 0 && (
|
{!isRecordTableInitialLoading && allRecordIds.length > 0 && (
|
||||||
<RecordTableAggregateFooter endOfTableSticky />
|
<RecordTableAggregateFooter />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 <></>;
|
||||||
|
};
|
||||||
@ -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}
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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,
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user