Remove overlay-scroll-bar (#11258)

## What

- Deprecate overlayscrollbars as we decided to follow the native
behavior
- rework on performances (avoid calling recoil states too much at field
level which is quite expensive)
- Also implements:
https://github.com/twentyhq/core-team-issues/issues/569

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Charles Bochet
2025-04-04 16:13:48 +02:00
committed by GitHub
parent 6b184cc641
commit 2308091b13
101 changed files with 664 additions and 952 deletions

View File

@ -55,7 +55,6 @@ export const DropdownMenuItemsContainer = ({
>
{hasMaxHeight ? (
<StyledScrollWrapper
contextProviderName="dropdownMenuItemsContainer"
componentInstanceId={`scroll-wrapper-dropdown-menu-${id}`}
>
<StyledDropdownMenuItemsInternalContainer>
@ -69,10 +68,7 @@ export const DropdownMenuItemsContainer = ({
)}
</StyledDropdownMenuItemsExternalContainer>
) : (
<ScrollWrapper
contextProviderName="dropdownMenuItemsContainer"
componentInstanceId={`scroll-wrapper-dropdown-menu-${id}`}
>
<ScrollWrapper componentInstanceId={`scroll-wrapper-dropdown-menu-${id}`}>
<StyledDropdownMenuItemsExternalContainer
hasMaxHeight={hasMaxHeight}
className={className}

View File

@ -30,17 +30,7 @@ const StyledLayout = styled.div`
scrollbar-width: 4px;
width: 100%;
*::-webkit-scrollbar {
height: 4px;
width: 4px;
}
*::-webkit-scrollbar-corner {
background-color: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: ${({ theme }) => theme.border.radius.sm};
}
`;

View File

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

View File

@ -70,7 +70,6 @@ export const ShowPageActivityContainer = ({
return (
<ScrollWrapper
contextProviderName="showPageActivityContainer"
componentInstanceId={`scroll-wrapper-tab-list-${targetableObject.id}`}
>
<StyledShowPageActivityContainer>

View File

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

View File

@ -79,7 +79,6 @@ export const TabList = ({
/>
<ScrollWrapper
defaultEnableYScroll={false}
contextProviderName="tabList"
componentInstanceId={`scroll-wrapper-tab-list-${componentInstanceId}`}
>
<StyledContainer className={className}>

View File

@ -1,6 +1,6 @@
import { createState } from 'twenty-ui/utilities';
import { INITIAL_HOTKEYS_SCOPE } from '../../constants/InitialHotkeysScope';
import { HotkeyScope } from '../../types/HotkeyScope';
import { createState } from 'twenty-ui/utilities';
export const currentHotkeyScopeState = createState<HotkeyScope>({
key: 'currentHotkeyScopeState',

View File

@ -1,102 +1,44 @@
import styled from '@emotion/styled';
import { OverlayScrollbars } from 'overlayscrollbars';
import { useOverlayScrollbars } from 'overlayscrollbars-react';
import { useEffect, useRef } from 'react';
import {
ContextProviderName,
getContextByProviderName,
} from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
import { ScrollWrapperInitEffect } from '@/ui/utilities/scroll/components/internal/ScrollWrapperInitEffect';
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 { scrollWrapperScrollBottomComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollBottomComponentState';
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 { css } from '@emotion/react';
import 'overlayscrollbars/overlayscrollbars.css';
type HeightMode = 'full' | 'fit-content';
const StyledScrollWrapper = styled.div<{
heightMode: HeightMode;
scrollbarVariant: 'with-padding' | 'no-padding';
}>`
const StyledScrollWrapper = styled.div`
&.scroll-wrapper-x-enabled {
overflow-x: scroll;
}
&.scroll-wrapper-y-enabled {
overflow-y: scroll;
}
display: flex;
height: ${({ heightMode }) => {
switch (heightMode) {
case 'full':
return '100%';
case 'fit-content':
return 'fit-content';
}
}};
width: 100%;
.os-scrollbar-handle {
background-color: ${({ theme }) => theme.border.color.strong};
}
// Keep horizontal scrollbar always visible
.os-scrollbar-horizontal {
&.os-scrollbar-auto-hide {
opacity: 1;
visibility: visible;
}
.os-scrollbar-track {
visibility: visible !important;
}
}
.os-scrollbar {
transition:
opacity 300ms,
visibility 300ms,
top 300ms,
right 300ms,
bottom 300ms,
left 300ms;
}
${({ scrollbarVariant }) =>
scrollbarVariant === 'no-padding' &&
css`
.os-scrollbar {
--os-size: 6px;
padding: 0px;
}
`}
height: 100%;
`;
const StyledInnerContainer = styled.div`
height: 100%;
width: 100%;
`;
export type ScrollWrapperProps = {
children: React.ReactNode;
className?: string;
heightMode?: HeightMode;
defaultEnableXScroll?: boolean;
defaultEnableYScroll?: boolean;
contextProviderName: ContextProviderName;
componentInstanceId: string;
scrollbarVariant?: 'with-padding' | 'no-padding';
};
export const ScrollWrapper = ({
componentInstanceId,
children,
className,
heightMode = 'full',
defaultEnableXScroll = true,
defaultEnableYScroll = true,
contextProviderName,
scrollbarVariant = 'with-padding',
}: ScrollWrapperProps) => {
const scrollableRef = useRef<HTMLDivElement>(null);
const Context = getContextByProviderName(contextProviderName);
const setScrollTop = useSetRecoilComponentStateV2(
scrollWrapperScrollTopComponentState,
componentInstanceId,
@ -112,8 +54,8 @@ export const ScrollWrapper = ({
componentInstanceId,
);
const handleScroll = (overlayScroll: OverlayScrollbars) => {
const target = overlayScroll.elements().scrollOffsetElement;
const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
const target = event.currentTarget;
setScrollTop(target.scrollTop);
setScrollLeft(target.scrollLeft);
setScrollBottom(
@ -121,103 +63,21 @@ export const ScrollWrapper = ({
);
};
const setOverlayScrollbars = useSetRecoilComponentStateV2(
scrollWrapperInstanceComponentState,
componentInstanceId,
);
const [initialize, instance] = useOverlayScrollbars({
options: {
scrollbars: {
autoHide: 'scroll',
autoHideDelay: 500,
},
overflow: {
x: defaultEnableXScroll ? undefined : 'hidden',
y: defaultEnableYScroll ? undefined : 'hidden',
},
},
events: {
updated: (osInstance) => {
const {
scrollOffsetElement: target,
scrollbarVertical,
scrollbarHorizontal,
} = osInstance.elements();
if (scrollbarVertical !== null) {
scrollbarVertical.track.dataset.selectDisable = 'true';
}
if (scrollbarHorizontal !== null) {
scrollbarHorizontal.track.dataset.selectDisable = 'true';
}
setScrollBottom(
target.scrollHeight - target.clientHeight - target.scrollTop,
);
},
scroll: (osInstance) => {
const { scrollOffsetElement: target, scrollbarVertical } =
osInstance.elements();
// Hide vertical scrollbar by default
if (scrollbarVertical !== null) {
scrollbarVertical.track.style.visibility = 'hidden';
}
// Show vertical scrollbar based on scroll direction
const isVerticalScroll =
target.scrollTop !== Number(target.dataset.lastScrollTop || '0');
if (
isVerticalScroll === true &&
scrollbarVertical !== null &&
target.scrollHeight > target.clientHeight
) {
scrollbarVertical.track.style.visibility = 'visible';
}
// Update vertical scroll positions
target.dataset.lastScrollTop = target.scrollTop.toString();
handleScroll(osInstance);
},
},
});
useEffect(() => {
const currentRef = scrollableRef.current;
if (currentRef !== null) {
initialize(currentRef);
}
return () => {
// Reset vertical scroll component-specific Recoil state
setScrollTop(0);
setOverlayScrollbars(null);
instance()?.destroy();
};
}, [initialize, instance, setScrollTop, setOverlayScrollbars]);
useEffect(() => {
setOverlayScrollbars(instance());
}, [instance, setOverlayScrollbars]);
return (
<ScrollWrapperComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
>
<Context.Provider
value={{
ref: scrollableRef,
id: contextProviderName,
}}
<ScrollWrapperInitEffect
defaultEnableXScroll={defaultEnableXScroll}
defaultEnableYScroll={defaultEnableYScroll}
/>
<StyledScrollWrapper
id={`scroll-wrapper-${componentInstanceId}`}
className={className}
onScroll={handleScroll}
>
<StyledScrollWrapper
ref={scrollableRef}
className={className}
heightMode={heightMode}
scrollbarVariant={scrollbarVariant}
>
<StyledInnerContainer>{children}</StyledInnerContainer>
</StyledScrollWrapper>
</Context.Provider>
<StyledInnerContainer>{children}</StyledInnerContainer>
</StyledScrollWrapper>
</ScrollWrapperComponentInstanceContext.Provider>
);
};

View File

@ -0,0 +1,27 @@
import { useToggleScrollWrapper } from '@/ui/utilities/scroll/hooks/useToggleScrollWrapper';
import { useEffect } from 'react';
export type ScrollWrapperInitEffectProps = {
defaultEnableXScroll?: boolean;
defaultEnableYScroll?: boolean;
};
export const ScrollWrapperInitEffect = ({
defaultEnableXScroll = true,
defaultEnableYScroll = true,
}: ScrollWrapperInitEffectProps) => {
const { toggleScrollXWrapper, toggleScrollYWrapper } =
useToggleScrollWrapper();
useEffect(() => {
toggleScrollXWrapper(defaultEnableXScroll);
toggleScrollYWrapper(defaultEnableYScroll);
}, [
defaultEnableXScroll,
defaultEnableYScroll,
toggleScrollXWrapper,
toggleScrollYWrapper,
]);
return <></>;
};

View File

@ -1,98 +0,0 @@
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'
| 'showPageActivityContainer'
| 'navigationDrawer'
| 'aggregateFooterCell'
| 'modalContent';
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 ShowPageActivityContainerScrollWrapperContext =
createScrollWrapperContext('showPageActivityContainer');
export const NavigationDrawerScrollWrapperContext =
createScrollWrapperContext('navigationDrawer');
export const TestScrollWrapperContext = createScrollWrapperContext('test');
export const AggregateFooterCellScrollWrapperContext =
createScrollWrapperContext('aggregateFooterCell');
export const ModalContentScrollWrapperContext =
createScrollWrapperContext('modalContent');
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;
case 'showPageActivityContainer':
return ShowPageActivityContainerScrollWrapperContext;
case 'navigationDrawer':
return NavigationDrawerScrollWrapperContext;
case 'aggregateFooterCell':
return AggregateFooterCellScrollWrapperContext;
case 'modalContent':
return ModalContentScrollWrapperContext;
default:
throw new Error('Context Provider not available');
}
};

View File

@ -1,19 +0,0 @@
import { renderHook } from '@testing-library/react';
import { useScrollWrapperScopedRef } from '@/ui/utilities/scroll/hooks/useScrollWrapperScopedRef';
jest.mock('react', () => {
const originalModule = jest.requireActual('react');
return {
...originalModule,
useContext: () => ({ current: {} }),
};
});
describe('useScrollWrapperScopedRef', () => {
it('should return the scrollWrapperRef if available', () => {
const { result } = renderHook(() => useScrollWrapperScopedRef('test'));
expect(result.current).toBeDefined();
});
});

View File

@ -0,0 +1,11 @@
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
export const useScrollToPosition = () => {
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
const scrollToPosition = (scrollPositionInPx: number) => {
scrollWrapperHTMLElement?.scrollTo({ top: scrollPositionInPx });
};
return { scrollToPosition };
};

View File

@ -0,0 +1,17 @@
import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
export const useScrollWrapperElement = (targetComponentInstanceId?: string) => {
const instanceId = useAvailableComponentInstanceIdOrThrow(
ScrollWrapperComponentInstanceContext,
targetComponentInstanceId,
);
const scrollWrapperHTMLElement = document.getElementById(
`scroll-wrapper-${instanceId}`,
);
return {
scrollWrapperHTMLElement,
};
};

View File

@ -1,22 +0,0 @@
import { useContext } from 'react';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import {
ContextProviderName,
getContextByProviderName,
} from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
export const useScrollWrapperScopedRef = (
contextProviderName: ContextProviderName,
) => {
const Context = getContextByProviderName(contextProviderName);
const scrollWrapperRef = useContext(Context);
if (isUndefinedOrNull(scrollWrapperRef))
throw new Error(
`Using a scroll ref without a ScrollWrapper : verify that you are using a ScrollWrapper if you intended to do so.`,
);
return scrollWrapperRef;
};

View File

@ -1,34 +1,38 @@
import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
export const useToggleScrollWrapper = () => {
const instanceOverlay = useRecoilComponentValueV2(
scrollWrapperInstanceComponentState,
export const useToggleScrollWrapper = (targetComponentInstanceId?: string) => {
const instanceId = useAvailableComponentInstanceIdOrThrow(
ScrollWrapperComponentInstanceContext,
targetComponentInstanceId,
);
const toggleScrollXWrapper = (isEnabled: boolean) => {
if (!instanceOverlay) {
return;
if (isEnabled) {
document
.getElementById(`scroll-wrapper-${instanceId}`)
?.classList.add('scroll-wrapper-x-enabled');
} else {
document
.getElementById(`scroll-wrapper-${instanceId}`)
?.classList.remove('scroll-wrapper-x-enabled');
}
instanceOverlay.options({
overflow: {
x: isEnabled ? 'scroll' : 'hidden',
},
});
};
const toggleScrollYWrapper = (isEnabled: boolean) => {
if (!instanceOverlay) {
return;
if (isEnabled) {
document
.getElementById(`scroll-wrapper-${instanceId}`)
?.classList.add('scroll-wrapper-y-enabled');
} else {
document
.getElementById(`scroll-wrapper-${instanceId}`)
?.classList.remove('scroll-wrapper-y-enabled');
}
instanceOverlay.options({
overflow: {
y: isEnabled ? 'scroll' : 'hidden',
},
});
};
return { toggleScrollXWrapper, toggleScrollYWrapper };
return {
toggleScrollXWrapper,
toggleScrollYWrapper,
};
};

View File

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