mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-23 03:51:36 +03:00
Fix overlay scroll gaps (#4512)
* fix overlay scroll leaving gap * fixed tests --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
parent
da8f1b0a66
commit
aacb3763e7
@ -129,6 +129,8 @@
|
||||
"next-mdx-remote": "^4.4.1",
|
||||
"nodemailer": "^6.9.8",
|
||||
"openapi-types": "^12.1.3",
|
||||
"overlayscrollbars": "^2.6.1",
|
||||
"overlayscrollbars-react": "^0.5.4",
|
||||
"passport": "^0.7.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
|
1
packages/twenty-front/__mocks__/styleMock.js
Normal file
1
packages/twenty-front/__mocks__/styleMock.js
Normal file
@ -0,0 +1 @@
|
||||
export default 'test-file-stub';
|
@ -13,6 +13,7 @@ export default {
|
||||
'@testing/(.+)': '<rootDir>/src/testing/$1',
|
||||
'\\.(jpg|jpeg|png|gif|webp|svg|svg\\?react)$':
|
||||
'<rootDir>/__mocks__/imageMock.js',
|
||||
'\\.css$': '<rootDir>/__mocks__/styleMock.js',
|
||||
},
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
coverageThreshold: {
|
||||
|
@ -5,6 +5,7 @@ import { Section } from '@/ui/layout/section/components/Section';
|
||||
const StyledRecordDetailSection = styled(Section)`
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
padding: ${({ theme }) => theme.spacing(3)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
width: auto;
|
||||
`;
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { useRecoilValue } from 'recoil';
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
import { TabListScope } from '@/ui/layout/tab/scopes/TabListScope';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
|
||||
import { Tab } from './Tab';
|
||||
|
||||
@ -30,7 +31,6 @@ const StyledContainer = styled.div`
|
||||
height: 40px;
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
user-select: none;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
export const TabList = ({ tabs, tabListId }: TabListProps) => {
|
||||
@ -46,24 +46,26 @@ export const TabList = ({ tabs, tabListId }: TabListProps) => {
|
||||
|
||||
return (
|
||||
<TabListScope tabListScopeId={tabListId}>
|
||||
<StyledContainer>
|
||||
{tabs
|
||||
.filter((tab) => !tab.hide)
|
||||
.map((tab) => (
|
||||
<Tab
|
||||
id={tab.id}
|
||||
key={tab.id}
|
||||
title={tab.title}
|
||||
Icon={tab.Icon}
|
||||
active={tab.id === activeTabId}
|
||||
onClick={() => {
|
||||
setActiveTabId(tab.id);
|
||||
}}
|
||||
disabled={tab.disabled}
|
||||
hasBetaPill={tab.hasBetaPill}
|
||||
/>
|
||||
))}
|
||||
</StyledContainer>
|
||||
<ScrollWrapper hideY>
|
||||
<StyledContainer>
|
||||
{tabs
|
||||
.filter((tab) => !tab.hide)
|
||||
.map((tab) => (
|
||||
<Tab
|
||||
id={tab.id}
|
||||
key={tab.id}
|
||||
title={tab.title}
|
||||
Icon={tab.Icon}
|
||||
active={tab.id === activeTabId}
|
||||
onClick={() => {
|
||||
setActiveTabId(tab.id);
|
||||
}}
|
||||
disabled={tab.disabled}
|
||||
hasBetaPill={tab.hasBetaPill}
|
||||
/>
|
||||
))}
|
||||
</StyledContainer>
|
||||
</ScrollWrapper>
|
||||
</TabListScope>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,13 @@
|
||||
import { createContext, RefObject, useRef } from 'react';
|
||||
import { createContext, RefObject, useEffect, useRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { OverlayScrollbars } from 'overlayscrollbars';
|
||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useListenScroll } from '../hooks/useListenScroll';
|
||||
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,
|
||||
@ -10,11 +16,9 @@ export const ScrollWrapperContext = createContext<RefObject<HTMLDivElement>>({
|
||||
const StyledScrollWrapper = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
scrollbar-gutter: stable;
|
||||
width: 100%;
|
||||
|
||||
&.scrolling::-webkit-scrollbar-thumb {
|
||||
.os-scrollbar-handle {
|
||||
background-color: ${({ theme }) => theme.border.color.medium};
|
||||
}
|
||||
`;
|
||||
@ -22,15 +26,47 @@ const StyledScrollWrapper = styled.div`
|
||||
export type ScrollWrapperProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
hideY?: boolean;
|
||||
hideX?: boolean;
|
||||
};
|
||||
|
||||
export const ScrollWrapper = ({ children, className }: ScrollWrapperProps) => {
|
||||
export const ScrollWrapper = ({
|
||||
children,
|
||||
className,
|
||||
hideX,
|
||||
hideY,
|
||||
}: ScrollWrapperProps) => {
|
||||
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useListenScroll({
|
||||
scrollableRef,
|
||||
const handleScroll = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(overlayScroll: OverlayScrollbars) => {
|
||||
const target = overlayScroll.elements().scrollOffsetElement;
|
||||
set(scrollTopState(), target.scrollTop);
|
||||
set(scrollLeftState(), target.scrollLeft);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const [initialize] = useOverlayScrollbars({
|
||||
options: {
|
||||
scrollbars: { autoHide: 'scroll' },
|
||||
overflow: {
|
||||
y: hideY ? 'hidden' : undefined,
|
||||
x: hideX ? 'hidden' : undefined,
|
||||
},
|
||||
},
|
||||
events: {
|
||||
scroll: handleScroll,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollableRef?.current !== null) {
|
||||
initialize(scrollableRef.current);
|
||||
}
|
||||
}, [initialize, scrollableRef]);
|
||||
|
||||
return (
|
||||
<ScrollWrapperContext.Provider value={scrollableRef}>
|
||||
<StyledScrollWrapper ref={scrollableRef} className={className}>
|
||||
|
@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
import { expect } from '@storybook/test';
|
||||
import { act, fireEvent, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
|
||||
import { useListenScroll } from '@/ui/utilities/scroll/hooks/useListenScroll';
|
||||
import { isScrollingState } from '@/ui/utilities/scroll/states/isScrollingState';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const containerRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<div id="container" ref={containerRef}>
|
||||
{children}
|
||||
</div>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe('useListenScroll', () => {
|
||||
it('should trigger the callback when scrolling', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
useListenScroll({ scrollableRef: containerRef });
|
||||
const isScrolling = useRecoilValue(isScrollingState);
|
||||
|
||||
return { isScrolling };
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current.isScrolling).toBe(false);
|
||||
|
||||
jest.advanceTimersByTime(500);
|
||||
|
||||
const container = document.querySelector('#container');
|
||||
|
||||
act(() => {
|
||||
if (isDefined(container)) fireEvent.scroll(container);
|
||||
});
|
||||
|
||||
expect(result.current.isScrolling).toBe(true);
|
||||
});
|
||||
});
|
@ -1,66 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { scrollLeftState } from '@/ui/utilities/scroll/states/scrollLeftState';
|
||||
import { scrollTopState } from '@/ui/utilities/scroll/states/scrollTopState';
|
||||
|
||||
import { isScrollingState } from '../states/isScrollingState';
|
||||
|
||||
export const useListenScroll = <T extends Element>({
|
||||
scrollableRef,
|
||||
}: {
|
||||
scrollableRef: React.RefObject<T>;
|
||||
}) => {
|
||||
const hideScrollBarsCallback = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
() => {
|
||||
const isScrolling = snapshot.getLoadable(isScrollingState).getValue();
|
||||
|
||||
if (!isScrolling) {
|
||||
scrollableRef.current?.classList.remove('scrolling');
|
||||
}
|
||||
},
|
||||
[scrollableRef],
|
||||
);
|
||||
|
||||
const handleScrollStart = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(event: Event) => {
|
||||
set(isScrollingState, true);
|
||||
scrollableRef.current?.classList.add('scrolling');
|
||||
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
set(scrollTopState, target.scrollTop);
|
||||
set(scrollLeftState, target.scrollLeft);
|
||||
},
|
||||
[scrollableRef],
|
||||
);
|
||||
|
||||
const handleScrollEnd = useRecoilCallback(
|
||||
({ set }) =>
|
||||
() => {
|
||||
set(isScrollingState, false);
|
||||
debounce(hideScrollBarsCallback, 1000)();
|
||||
},
|
||||
[hideScrollBarsCallback],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const refTarget = scrollableRef.current;
|
||||
|
||||
refTarget?.addEventListener('scrollend', handleScrollEnd);
|
||||
refTarget?.addEventListener('scroll', handleScrollStart);
|
||||
|
||||
return () => {
|
||||
refTarget?.removeEventListener('scrollend', handleScrollEnd);
|
||||
refTarget?.removeEventListener('scroll', handleScrollStart);
|
||||
};
|
||||
}, [
|
||||
hideScrollBarsCallback,
|
||||
handleScrollStart,
|
||||
handleScrollEnd,
|
||||
scrollableRef,
|
||||
]);
|
||||
};
|
@ -1,6 +0,0 @@
|
||||
import { createState } from '@/ui/utilities/state/utils/createState';
|
||||
|
||||
export const isScrollingState = createState({
|
||||
key: 'scroll/isScollingState',
|
||||
defaultValue: false,
|
||||
});
|
19
yarn.lock
19
yarn.lock
@ -38090,6 +38090,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"overlayscrollbars-react@npm:^0.5.4":
|
||||
version: 0.5.4
|
||||
resolution: "overlayscrollbars-react@npm:0.5.4"
|
||||
peerDependencies:
|
||||
overlayscrollbars: ^2.0.0
|
||||
react: ">=16.8.0"
|
||||
checksum: 71efe2a3775b26d1bc7c7e4cd8cf7ea7a977864fb1286cecc3ea6e83bf56fb2c104c9860de5ad6e472598be4f010d3e125b282a2a61190f3a8eb449f85e46a5e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"overlayscrollbars@npm:^2.6.1":
|
||||
version: 2.6.1
|
||||
resolution: "overlayscrollbars@npm:2.6.1"
|
||||
checksum: fc20c787bc842a522f2498ef4a23cbd1e3849c98961bf87b6482366b0cbc325848c0e6f99deec7d035114f4f55f4efcc67939752f55742389c3ec16f2502fd1e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"override-require@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "override-require@npm:1.1.1"
|
||||
@ -46106,6 +46123,8 @@ __metadata:
|
||||
nodemailer: "npm:^6.9.8"
|
||||
nx: "npm:17.2.8"
|
||||
openapi-types: "npm:^12.1.3"
|
||||
overlayscrollbars: "npm:^2.6.1"
|
||||
overlayscrollbars-react: "npm:^0.5.4"
|
||||
passport: "npm:^0.7.0"
|
||||
passport-google-oauth20: "npm:^2.0.0"
|
||||
passport-jwt: "npm:^4.0.1"
|
||||
|
Loading…
Reference in New Issue
Block a user