Refacto scroll + Aggregate queries for view groups (#9089)

Closes https://github.com/twentyhq/private-issues/issues/217.

Refactoring scroll not to cause table-wide re-render when opening a
dropdown (triggering a scroll lock) in the table.
This commit is contained in:
Marie
2024-12-16 17:58:57 +01:00
committed by GitHub
parent c90d2fd5cc
commit 311b5f64c4
47 changed files with 374 additions and 277 deletions

View File

@ -1,5 +1,6 @@
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import styled from '@emotion/styled';
import { useId } from 'react';
const StyledDropdownMenuItemsExternalContainer = styled.div<{
hasMaxHeight?: boolean;
@ -44,13 +45,18 @@ export const DropdownMenuItemsContainer = ({
className?: string;
withoutScrollWrapper?: boolean;
}) => {
const id = useId();
return withoutScrollWrapper === true ? (
<StyledDropdownMenuItemsExternalContainer
hasMaxHeight={hasMaxHeight}
className={className}
>
{hasMaxHeight ? (
<StyledScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<StyledScrollWrapper
contextProviderName="dropdownMenuItemsContainer"
componentInstanceId={`scroll-wrapper-dropdown-menu-${id}`}
>
<StyledDropdownMenuItemsInternalContainer>
{children}
</StyledDropdownMenuItemsInternalContainer>
@ -62,7 +68,10 @@ export const DropdownMenuItemsContainer = ({
)}
</StyledDropdownMenuItemsExternalContainer>
) : (
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<ScrollWrapper
contextProviderName="dropdownMenuItemsContainer"
componentInstanceId={`scroll-wrapper-dropdown-menu-${id}`}
>
<StyledDropdownMenuItemsExternalContainer
hasMaxHeight={hasMaxHeight}
className={className}

View File

@ -31,7 +31,10 @@ export const ShowPageContainer = ({ children }: ShowPageContainerProps) => {
const isMobile = useIsMobile();
return isMobile ? (
<StyledOuterContainer>
<StyledScrollWrapper contextProviderName="showPageContainer">
<StyledScrollWrapper
contextProviderName="showPageContainer"
componentInstanceId={'scroll-wrapper-show-page-container'}
>
<StyledInnerContainer>{children}</StyledInnerContainer>
</StyledScrollWrapper>
</StyledOuterContainer>

View File

@ -23,7 +23,10 @@ export const ShowPageActivityContainer = ({
);
return !isNewViewableRecordLoading ? (
<ScrollWrapper contextProviderName="showPageActivityContainer">
<ScrollWrapper
contextProviderName="showPageActivityContainer"
componentInstanceId={`scroll-wrapper-tab-list-${targetableObject.id}`}
>
<StyledShowPageActivityContainer>
<RichTextEditor
activityId={targetableObject.id}

View File

@ -46,7 +46,10 @@ export const ShowPageLeftContainer = ({
{children}
</StyledInnerContainer>
) : (
<ScrollWrapper contextProviderName="showPageLeftContainer">
<ScrollWrapper
contextProviderName="showPageLeftContainer"
componentInstanceId={`scroll-wrapper-show-page-left-container`}
>
<StyledIntermediateContainer>
<StyledInnerContainer isMobile={isMobile}>
{children}

View File

@ -7,8 +7,9 @@ import { TabListScope } from '@/ui/layout/tab/scopes/TabListScope';
import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab/components/TabListFromUrlOptionalEffect';
import { LayoutCard } from '@/ui/layout/tab/types/LayoutCard';
import { Tab } from './Tab';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useEffect } from 'react';
import { Tab } from './Tab';
export type SingleTabProps = {
title: string;
@ -70,26 +71,32 @@ export const TabList = ({
componentInstanceId={tabListInstanceId}
tabListIds={tabs.map((tab) => tab.id)}
/>
<StyledTabsContainer>
{visibleTabs.map((tab) => (
<Tab
id={tab.id}
key={tab.id}
title={tab.title}
Icon={tab.Icon}
logo={tab.logo}
active={tab.id === activeTabId}
disabled={tab.disabled ?? loading}
pill={tab.pill}
to={behaveAsLinks ? `#${tab.id}` : undefined}
onClick={() => {
if (!behaveAsLinks) {
setActiveTabId(tab.id);
}
}}
/>
))}
</StyledTabsContainer>
<ScrollWrapper
defaultEnableYScroll={false}
contextProviderName="tabList"
componentInstanceId={`scroll-wrapper-tab-list-${tabListInstanceId}`}
>
<StyledTabsContainer>
{visibleTabs.map((tab) => (
<Tab
id={tab.id}
key={tab.id}
title={tab.title}
Icon={tab.Icon}
logo={tab.logo}
active={tab.id === activeTabId}
disabled={tab.disabled ?? loading}
pill={tab.pill}
to={behaveAsLinks ? `#${tab.id}` : undefined}
onClick={() => {
if (!behaveAsLinks) {
setActiveTabId(tab.id);
}
}}
/>
))}
</StyledTabsContainer>
</ScrollWrapper>
</TabListScope>
</StyledContainer>
);

View File

@ -2,15 +2,17 @@ import styled from '@emotion/styled';
import { OverlayScrollbars } from 'overlayscrollbars';
import { useOverlayScrollbars } from 'overlayscrollbars-react';
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 { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext';
import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
import { scrollWrapperScrollLeftComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollLeftComponentState';
import { scrollWrapperScrollTopComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollTopComponentState';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import 'overlayscrollbars/overlayscrollbars.css';
const StyledScrollWrapper = styled.div<{ scrollHide?: boolean }>`
@ -31,41 +33,52 @@ const StyledInnerContainer = styled.div`
export type ScrollWrapperProps = {
children: React.ReactNode;
className?: string;
enableXScroll?: boolean;
enableYScroll?: boolean;
defaultEnableXScroll?: boolean;
defaultEnableYScroll?: boolean;
contextProviderName: ContextProviderName;
scrollHide?: boolean;
componentInstanceId: string;
};
export const ScrollWrapper = ({
componentInstanceId,
children,
className,
enableXScroll = true,
enableYScroll = true,
defaultEnableXScroll = true,
defaultEnableYScroll = true,
contextProviderName,
scrollHide = false,
}: ScrollWrapperProps) => {
const scrollableRef = useRef<HTMLDivElement>(null);
const Context = getContextByProviderName(contextProviderName);
const { scrollTopComponentState, scrollLeftComponentState } =
useScrollStates(contextProviderName);
const setScrollTop = useSetRecoilState(scrollTopComponentState);
const setScrollLeft = useSetRecoilState(scrollLeftComponentState);
const setScrollTop = useSetRecoilComponentStateV2(
scrollWrapperScrollTopComponentState,
componentInstanceId,
);
const setScrollLeft = useSetRecoilComponentStateV2(
scrollWrapperScrollLeftComponentState,
componentInstanceId,
);
const handleScroll = (overlayScroll: OverlayScrollbars) => {
const target = overlayScroll.elements().scrollOffsetElement;
setScrollTop(target.scrollTop);
setScrollLeft(target.scrollLeft);
};
const setOverlayScrollbars = useSetRecoilState(overlayScrollbarsState);
const setOverlayScrollbars = useSetRecoilComponentStateV2(
scrollWrapperInstanceComponentState,
componentInstanceId,
);
const [initialize, instance] = useOverlayScrollbars({
options: {
scrollbars: { autoHide: 'scroll' },
overflow: {
x: enableXScroll ? undefined : 'hidden',
y: enableYScroll ? undefined : 'hidden',
x: defaultEnableXScroll ? undefined : 'hidden',
y: defaultEnableYScroll ? undefined : 'hidden',
},
},
events: {
@ -84,19 +97,23 @@ export const ScrollWrapper = ({
}, [instance, setOverlayScrollbars]);
return (
<Context.Provider
value={{
ref: scrollableRef,
id: contextProviderName,
}}
<ScrollWrapperComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
>
<StyledScrollWrapper
ref={scrollableRef}
className={className}
scrollHide={scrollHide}
<Context.Provider
value={{
ref: scrollableRef,
id: contextProviderName,
}}
>
<StyledInnerContainer>{children}</StyledInnerContainer>
</StyledScrollWrapper>
</Context.Provider>
<StyledScrollWrapper
ref={scrollableRef}
className={className}
scrollHide={scrollHide}
>
<StyledInnerContainer>{children}</StyledInnerContainer>
</StyledScrollWrapper>
</Context.Provider>
</ScrollWrapperComponentInstanceContext.Provider>
);
};

View File

@ -1,32 +0,0 @@
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

@ -1,10 +0,0 @@
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

@ -1,8 +0,0 @@
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

@ -0,0 +1,34 @@
import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const useToggleScrollWrapper = () => {
const instanceOverlay = useRecoilComponentValueV2(
scrollWrapperInstanceComponentState,
);
const toggleScrollXWrapper = (isEnabled: boolean) => {
if (!instanceOverlay) {
return;
}
instanceOverlay.options({
overflow: {
x: isEnabled ? 'scroll' : 'hidden',
},
});
};
const toggleScrollYWrapper = (isEnabled: boolean) => {
if (!instanceOverlay) {
return;
}
instanceOverlay.options({
overflow: {
y: isEnabled ? 'scroll' : 'hidden',
},
});
};
return { toggleScrollXWrapper, toggleScrollYWrapper };
};

View File

@ -0,0 +1,4 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const ScrollWrapperComponentInstanceContext =
createComponentInstanceContext();

View File

@ -1,7 +0,0 @@
import { OverlayScrollbars } from 'overlayscrollbars';
import { createState } from 'twenty-ui';
export const overlayScrollbarsState = createState<OverlayScrollbars | null>({
key: 'scroll/overlayScrollbarsState',
defaultValue: null,
});

View File

@ -1,6 +0,0 @@
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 { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
export const scrollPositionState = createFamilyState({
key: 'scroll/scrollPositionState',
defaultValue: 0,
});

View File

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

View File

@ -0,0 +1,10 @@
import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
import { OverlayScrollbars } from 'overlayscrollbars';
export const scrollWrapperInstanceComponentState =
createComponentStateV2<OverlayScrollbars | null>({
key: 'scrollWrapperInstanceComponentState',
defaultValue: null,
componentInstanceContext: ScrollWrapperComponentInstanceContext,
});

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

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