Created a specific scroll wrapper context per scroll wrapper and made ScrollTop and ScrollLeft componentStates (#6645)

@lucasbordeau @charlesBochet 

Issue #4826 

Could u review this changes.

Let me know what do you think.

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
nitin 2024-08-22 21:51:14 +05:30 committed by GitHub
parent 0a7700351f
commit 1030ff43d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 205 additions and 64 deletions

View File

@ -46,7 +46,7 @@ export const EventList = ({ events, targetableObject }: EventListProps) => {
const groupedEvents = groupEventsByMonth(filteredEvents);
return (
<ScrollWrapper>
<ScrollWrapper contextProviderName="eventList">
<StyledTimelineContainer>
{groupedEvents.map((group, index) => (
<EventsGroup

View File

@ -349,7 +349,7 @@ export const CommandMenu = () => {
)}
</StyledInputContainer>
<StyledList>
<ScrollWrapper>
<ScrollWrapper contextProviderName="commandMenu">
<StyledInnerList isMobile={isMobile}>
<SelectableList
selectableListId="command-menu-list"

View File

@ -148,7 +148,7 @@ export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => {
>
<StyledWrapper>
<StyledBoardHeader />
<ScrollWrapper>
<ScrollWrapper contextProviderName="recordBoard">
<StyledContainer ref={boardRef}>
<DragDropContext onDragEnd={onDragEnd}>
{columnIds.map((columnId) => (

View File

@ -23,7 +23,7 @@ import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState';
import { contextMenuPositionState } from '@/ui/navigation/context-menu/states/contextMenuPositionState';
import { AnimatedEaseInOut } from '@/ui/utilities/animation/components/AnimatedEaseInOut';
import { ScrollWrapperContext } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { RecordBoardScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
const StyledBoardCard = styled.div<{ selected: boolean }>`
background-color: ${({ theme, selected }) =>
@ -199,10 +199,10 @@ export const RecordBoardCard = () => {
return [updateEntity, { loading: false }];
};
const scrollWrapperRef = useContext(ScrollWrapperContext);
const scrollWrapperRef = useContext(RecordBoardScrollWrapperContext);
const { ref: cardRef, inView } = useInView({
root: scrollWrapperRef.current,
root: scrollWrapperRef?.ref.current,
rootMargin: '1000px',
});

View File

@ -70,7 +70,7 @@ export const RecordTableWithWrappers = ({
return (
<EntityDeleteContext.Provider value={deleteOneRecord}>
<ScrollWrapper>
<ScrollWrapper contextProviderName="recordTableWithWrappers">
<RecordUpdateContext.Provider value={updateRecordMutation}>
<StyledTableWithHeader>
<StyledTableContainer>

View File

@ -10,8 +10,8 @@ import { useRecordTableStates } from '@/object-record/record-table/hooks/interna
import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState';
import { isRecordTableScrolledTopComponentState } from '@/object-record/record-table/states/isRecordTableScrolledTopComponentState';
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
import { scrollLeftState } from '@/ui/utilities/scroll/states/scrollLeftState';
import { scrollTopState } from '@/ui/utilities/scroll/states/scrollTopState';
import { useScrollLeftValue } from '@/ui/utilities/scroll/hooks/useScrollLeftValue';
import { useScrollTopValue } from '@/ui/utilities/scroll/hooks/useScrollTopValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { isNonEmptyString } from '@sniptt/guards';
import { useScrollToPosition } from '~/hooks/useScrollToPosition';
@ -38,7 +38,7 @@ export const RecordTableBodyEffect = () => {
const tableLastRowVisible = useRecoilValue(tableLastRowVisibleState);
const scrollTop = useRecoilValue(scrollTopState);
const scrollTop = useScrollTopValue('recordTableWithWrappers');
const setIsRecordTableScrolledTop = useSetRecoilComponentState(
isRecordTableScrolledTopComponentState,
);
@ -57,7 +57,7 @@ export const RecordTableBodyEffect = () => {
}
}, [scrollTop, setIsRecordTableScrolledTop]);
const scrollLeft = useRecoilValue(scrollLeftState);
const scrollLeft = useScrollLeftValue('recordTableWithWrappers');
const setIsRecordTableScrolledLeft = useSetRecoilComponentState(
isRecordTableScrolledLeftComponentState,

View File

@ -1,13 +1,13 @@
import styled from '@emotion/styled';
import { useContext } from 'react';
import { useInView } from 'react-intersection-observer';
import styled from '@emotion/styled';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { GRAY_SCALE } from 'twenty-ui';
import { useLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLoadRecordIndexTable';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
import { ScrollWrapperContext } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { RecordTableWithWrappersScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
type RecordTableBodyFetchMoreLoaderProps = {
objectNameSingular: string;
@ -40,12 +40,14 @@ export const RecordTableBodyFetchMoreLoader = ({
[setRecordTableLastRowVisible],
);
const scrollWrapperRef = useContext(ScrollWrapperContext);
const scrollWrapperRef = useContext(
RecordTableWithWrappersScrollWrapperContext,
);
const { ref: tbodyRef } = useInView({
onChange: onLastRowVisible,
rootMargin: '1000px',
root: scrollWrapperRef.current?.querySelector(
root: scrollWrapperRef?.ref.current?.querySelector(
'[data-overlayscrollbars-viewport="scrollbarHidden"]',
),
});

View File

@ -54,11 +54,11 @@ const HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID =
export const RecordTableHeaderLastColumn = () => {
const { theme } = useContext(ThemeContext);
const scrollWrapper = useScrollWrapperScopedRef();
const scrollWrapper = useScrollWrapperScopedRef('recordTableWithWrappers');
const isTableWiderThanScreen =
(scrollWrapper.current?.clientWidth ?? 0) <
(scrollWrapper.current?.scrollWidth ?? 0);
(scrollWrapper.ref.current?.clientWidth ?? 0) <
(scrollWrapper.ref.current?.scrollWidth ?? 0);
const { hiddenTableColumnsSelector } = useRecordTableStates();

View File

@ -10,7 +10,7 @@ import { RecordTableContext } from '@/object-record/record-table/contexts/Record
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr';
import { ScrollWrapperContext } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { RecordTableWithWrappersScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
export const RecordTableRowWrapper = ({
recordId,
@ -31,10 +31,12 @@ export const RecordTableRowWrapper = ({
const { isRowSelectedFamilyState } = useRecordTableStates();
const currentRowSelected = useRecoilValue(isRowSelectedFamilyState(recordId));
const scrollWrapperRef = useContext(ScrollWrapperContext);
const scrollWrapperRef = useContext(
RecordTableWithWrappersScrollWrapperContext,
);
const { ref: elementRef, inView } = useInView({
root: scrollWrapperRef.current?.querySelector(
root: scrollWrapperRef.ref.current?.querySelector(
'[data-overlayscrollbars-viewport="scrollbarHidden"]',
),
rootMargin: '1000px',

View File

@ -34,7 +34,7 @@ export const SettingsPageContainer = ({
}: {
children: ReactNode;
}) => (
<StyledScrollWrapper>
<StyledScrollWrapper contextProviderName="settingsPageContainer">
<StyledSettingsPageContainer>{children}</StyledSettingsPageContainer>
</StyledScrollWrapper>
);

View File

@ -45,7 +45,7 @@ export const DropdownMenuItemsContainer = ({
return (
<StyledDropdownMenuItemsExternalContainer hasMaxHeight={hasMaxHeight}>
{hasMaxHeight ? (
<StyledScrollWrapper>
<StyledScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<StyledDropdownMenuItemsInternalContainer>
{children}
</StyledDropdownMenuItemsInternalContainer>

View File

@ -32,7 +32,7 @@ export const ShowPageContainer = ({ children }: ShowPageContainerProps) => {
const isMobile = useIsMobile();
return isMobile ? (
<StyledOuterContainer>
<StyledScrollWrapper>
<StyledScrollWrapper contextProviderName="showPageContainer">
<StyledInnerContainer>{children}</StyledInnerContainer>
</StyledScrollWrapper>
</StyledOuterContainer>

View File

@ -1,5 +1,5 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled';
import { ReactNode } from 'react';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
@ -46,7 +46,7 @@ export const ShowPageLeftContainer = ({
{children}
</StyledInnerContainer>
) : (
<ScrollWrapper>
<ScrollWrapper contextProviderName="showPageLeftContainer">
<StyledIntermediateContainer>
<StyledInnerContainer isMobile={isMobile}>
{children}

View File

@ -1,5 +1,5 @@
import * as React from 'react';
import styled from '@emotion/styled';
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { IconComponent } from 'twenty-ui';
@ -53,7 +53,7 @@ export const TabList = ({
return (
<TabListScope tabListScopeId={tabListId}>
<ScrollWrapper hideY>
<ScrollWrapper hideY contextProviderName="tabList">
<StyledContainer className={className}>
{tabs
.filter((tab) => !tab.hide)

View File

@ -1,19 +1,18 @@
import { createContext, RefObject, useEffect, useRef } from 'react';
import styled from '@emotion/styled';
import { OverlayScrollbars } from 'overlayscrollbars';
import { useOverlayScrollbars } from 'overlayscrollbars-react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useEffect, useRef } from 'react';
import { useSetRecoilState } from 'recoil';
import {
ContextProviderName,
getContextByProviderName,
} from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
import { useScrollStates } from '@/ui/utilities/scroll/hooks/internal/useScrollStates';
import { overlayScrollbarsState } from '@/ui/utilities/scroll/states/overlayScrollbarsState';
import { scrollLeftState } from '@/ui/utilities/scroll/states/scrollLeftState';
import { scrollTopState } from '@/ui/utilities/scroll/states/scrollTopState';
import 'overlayscrollbars/overlayscrollbars.css';
export const ScrollWrapperContext = createContext<RefObject<HTMLDivElement>>({
current: null,
});
const StyledScrollWrapper = styled.div`
display: flex;
height: 100%;
@ -29,6 +28,7 @@ export type ScrollWrapperProps = {
className?: string;
hideY?: boolean;
hideX?: boolean;
contextProviderName: ContextProviderName;
};
export const ScrollWrapper = ({
@ -36,18 +36,21 @@ export const ScrollWrapper = ({
className,
hideX,
hideY,
contextProviderName,
}: ScrollWrapperProps) => {
const scrollableRef = useRef<HTMLDivElement>(null);
const Context = getContextByProviderName(contextProviderName);
const handleScroll = useRecoilCallback(
({ set }) =>
(overlayScroll: OverlayScrollbars) => {
const target = overlayScroll.elements().scrollOffsetElement;
set(scrollTopState, target.scrollTop);
set(scrollLeftState, target.scrollLeft);
},
[],
);
const { scrollTopComponentState, scrollLeftComponentState } =
useScrollStates(contextProviderName);
const setScrollTop = useSetRecoilState(scrollTopComponentState);
const setScrollLeft = useSetRecoilState(scrollLeftComponentState);
const handleScroll = (overlayScroll: OverlayScrollbars) => {
const target = overlayScroll.elements().scrollOffsetElement;
setScrollTop(target.scrollTop);
setScrollLeft(target.scrollLeft);
};
const setOverlayScrollbars = useSetRecoilState(overlayScrollbarsState);
@ -75,10 +78,10 @@ export const ScrollWrapper = ({
}, [instance, setOverlayScrollbars]);
return (
<ScrollWrapperContext.Provider value={scrollableRef}>
<Context.Provider value={{ ref: scrollableRef, id: contextProviderName }}>
<StyledScrollWrapper ref={scrollableRef} className={className}>
{children}
</StyledScrollWrapper>
</ScrollWrapperContext.Provider>
</Context.Provider>
);
};

View File

@ -0,0 +1,78 @@
import { createContext, RefObject } from 'react';
type ScrollWrapperContextValue = {
ref: RefObject<HTMLDivElement>;
id: string;
};
export type ContextProviderName =
| 'eventList'
| 'commandMenu'
| 'recordBoard'
| 'recordTableWithWrappers'
| 'settingsPageContainer'
| 'dropdownMenuItemsContainer'
| 'showPageContainer'
| 'showPageLeftContainer'
| 'tabList'
| 'releases'
| 'test';
const createScrollWrapperContext = (id: string) =>
createContext<ScrollWrapperContextValue>({
ref: { current: null },
id,
});
export const EventListScrollWrapperContext =
createScrollWrapperContext('eventList');
export const CommandMenuScrollWrapperContext =
createScrollWrapperContext('commandMenu');
export const RecordBoardScrollWrapperContext =
createScrollWrapperContext('recordBoard');
export const RecordTableWithWrappersScrollWrapperContext =
createScrollWrapperContext('recordTableWithWrappers');
export const SettingsPageContainerScrollWrapperContext =
createScrollWrapperContext('settingsPageContainer');
export const DropdownMenuItemsContainerScrollWrapperContext =
createScrollWrapperContext('dropdownMenuItemsContainer');
export const ShowPageContainerScrollWrapperContext =
createScrollWrapperContext('showPageContainer');
export const ShowPageLeftContainerScrollWrapperContext =
createScrollWrapperContext('showPageLeftContainer');
export const TabListScrollWrapperContext =
createScrollWrapperContext('tabList');
export const ReleasesScrollWrapperContext =
createScrollWrapperContext('releases');
export const TestScrollWrapperContext = createScrollWrapperContext('test');
export const getContextByProviderName = (
contextProviderName: ContextProviderName,
) => {
switch (contextProviderName) {
case 'eventList':
return EventListScrollWrapperContext;
case 'commandMenu':
return CommandMenuScrollWrapperContext;
case 'recordBoard':
return RecordBoardScrollWrapperContext;
case 'recordTableWithWrappers':
return RecordTableWithWrappersScrollWrapperContext;
case 'settingsPageContainer':
return SettingsPageContainerScrollWrapperContext;
case 'dropdownMenuItemsContainer':
return DropdownMenuItemsContainerScrollWrapperContext;
case 'showPageContainer':
return ShowPageContainerScrollWrapperContext;
case 'showPageLeftContainer':
return ShowPageLeftContainerScrollWrapperContext;
case 'tabList':
return TabListScrollWrapperContext;
case 'releases':
return ReleasesScrollWrapperContext;
case 'test':
return TestScrollWrapperContext;
default:
throw new Error('Context Provider not available');
}
};

View File

@ -12,7 +12,7 @@ jest.mock('react', () => {
describe('useScrollWrapperScopedRef', () => {
it('should return the scrollWrapperRef if available', () => {
const { result } = renderHook(() => useScrollWrapperScopedRef());
const { result } = renderHook(() => useScrollWrapperScopedRef('test'));
expect(result.current).toBeDefined();
});

View File

@ -0,0 +1,32 @@
import {
ContextProviderName,
getContextByProviderName,
} from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
import { scrollLeftComponentState } from '@/ui/utilities/scroll/states/scrollLeftComponentState';
import { scrollTopComponentState } from '@/ui/utilities/scroll/states/scrollTopComponentState';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { useContext } from 'react';
export const useScrollStates = (contextProviderName: ContextProviderName) => {
const Context = getContextByProviderName(contextProviderName);
const context = useContext(Context);
if (!context) {
throw new Error('Context not found');
}
const { id: scopeId } = context;
return {
scrollLeftComponentState: extractComponentState(
scrollLeftComponentState,
scopeId,
),
scrollTopComponentState: extractComponentState(
scrollTopComponentState,
scopeId,
),
};
};

View File

@ -0,0 +1,10 @@
import { ContextProviderName } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
import { useScrollStates } from '@/ui/utilities/scroll/hooks/internal/useScrollStates';
import { useRecoilValue } from 'recoil';
export const useScrollLeftValue = (
contextProviderName: ContextProviderName,
) => {
const { scrollLeftComponentState } = useScrollStates(contextProviderName);
return useRecoilValue(scrollLeftComponentState);
};

View File

@ -0,0 +1,8 @@
import { ContextProviderName } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
import { useScrollStates } from '@/ui/utilities/scroll/hooks/internal/useScrollStates';
import { useRecoilValue } from 'recoil';
export const useScrollTopValue = (contextProviderName: ContextProviderName) => {
const { scrollTopComponentState } = useScrollStates(contextProviderName);
return useRecoilValue(scrollTopComponentState);
};

View File

@ -2,10 +2,16 @@ import { useContext } from 'react';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { ScrollWrapperContext } from '../components/ScrollWrapper';
import {
ContextProviderName,
getContextByProviderName,
} from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
export const useScrollWrapperScopedRef = () => {
const scrollWrapperRef = useContext(ScrollWrapperContext);
export const useScrollWrapperScopedRef = (
contextProviderName: ContextProviderName,
) => {
const Context = getContextByProviderName(contextProviderName);
const scrollWrapperRef = useContext(Context);
if (isUndefinedOrNull(scrollWrapperRef))
throw new Error(

View File

@ -0,0 +1,6 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const scrollLeftComponentState = createComponentState<number>({
key: 'scroll/scrollLeftComponentState',
defaultValue: 0,
});

View File

@ -1,6 +0,0 @@
import { createState } from 'twenty-ui';
export const scrollLeftState = createState<number>({
key: 'scroll/scrollLeftState',
defaultValue: 0,
});

View File

@ -0,0 +1,6 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const scrollTopComponentState = createComponentState<number>({
key: 'scroll/scrollTopComponentState',
defaultValue: 0,
});

View File

@ -1,6 +0,0 @@
import { createState } from 'twenty-ui';
export const scrollTopState = createState<number>({
key: 'scroll/scrollTopState',
defaultValue: 0,
});

View File

@ -111,7 +111,7 @@ export const Releases = () => {
<SubMenuTopBarContainer Icon={IconRocket} title="Releases">
<SettingsPageContainer>
<StyledH1Title title="Releases" />
<ScrollWrapper>
<ScrollWrapper contextProviderName="releases">
<StyledReleaseContainer>
{releases.map((release) => (
<React.Fragment key={release.slug}>