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:
brendanlaschke 2024-03-31 10:53:37 +02:00 committed by GitHub
parent da8f1b0a66
commit aacb3763e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 89 additions and 147 deletions

View File

@ -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",

View File

@ -0,0 +1 @@
export default 'test-file-stub';

View File

@ -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: {

View File

@ -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;
`;

View File

@ -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>
);
};

View File

@ -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}>

View File

@ -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);
});
});

View File

@ -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,
]);
};

View File

@ -1,6 +0,0 @@
import { createState } from '@/ui/utilities/state/utils/createState';
export const isScrollingState = createState({
key: 'scroll/isScollingState',
defaultValue: false,
});

View File

@ -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"