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:
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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,
|
||||
),
|
||||
};
|
||||
};
|
||||
@ -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);
|
||||
};
|
||||
@ -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);
|
||||
};
|
||||
@ -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 };
|
||||
};
|
||||
@ -0,0 +1,4 @@
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
export const ScrollWrapperComponentInstanceContext =
|
||||
createComponentInstanceContext();
|
||||
@ -1,7 +0,0 @@
|
||||
import { OverlayScrollbars } from 'overlayscrollbars';
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const overlayScrollbarsState = createState<OverlayScrollbars | null>({
|
||||
key: 'scroll/overlayScrollbarsState',
|
||||
defaultValue: null,
|
||||
});
|
||||
@ -1,6 +0,0 @@
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const scrollLeftComponentState = createComponentState<number>({
|
||||
key: 'scroll/scrollLeftComponentState',
|
||||
defaultValue: 0,
|
||||
});
|
||||
@ -1,6 +0,0 @@
|
||||
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
||||
|
||||
export const scrollPositionState = createFamilyState({
|
||||
key: 'scroll/scrollPositionState',
|
||||
defaultValue: 0,
|
||||
});
|
||||
@ -1,6 +0,0 @@
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const scrollTopComponentState = createComponentState<number>({
|
||||
key: 'scroll/scrollTopComponentState',
|
||||
defaultValue: 0,
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
Reference in New Issue
Block a user