diff --git a/package.json b/package.json index ec2cc7cf42..5b5405392c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/twenty-front/__mocks__/styleMock.js b/packages/twenty-front/__mocks__/styleMock.js new file mode 100644 index 0000000000..602eb23ee2 --- /dev/null +++ b/packages/twenty-front/__mocks__/styleMock.js @@ -0,0 +1 @@ +export default 'test-file-stub'; diff --git a/packages/twenty-front/jest.config.ts b/packages/twenty-front/jest.config.ts index efcfabc111..fff48ece1d 100644 --- a/packages/twenty-front/jest.config.ts +++ b/packages/twenty-front/jest.config.ts @@ -13,6 +13,7 @@ export default { '@testing/(.+)': '/src/testing/$1', '\\.(jpg|jpeg|png|gif|webp|svg|svg\\?react)$': '/__mocks__/imageMock.js', + '\\.css$': '/__mocks__/styleMock.js', }, extensionsToTreatAsEsm: ['.ts', '.tsx'], coverageThreshold: { diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailSection.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailSection.tsx index 8a9218ae9a..9d1e29b10e 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailSection.tsx @@ -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; `; diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx index ce1f2a9420..ccb821faf5 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx @@ -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 ( - - {tabs - .filter((tab) => !tab.hide) - .map((tab) => ( - { - setActiveTabId(tab.id); - }} - disabled={tab.disabled} - hasBetaPill={tab.hasBetaPill} - /> - ))} - + + + {tabs + .filter((tab) => !tab.hide) + .map((tab) => ( + { + setActiveTabId(tab.id); + }} + disabled={tab.disabled} + hasBetaPill={tab.hasBetaPill} + /> + ))} + + ); }; diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx b/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx index f5e50f5f08..c2b857836e 100644 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx @@ -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>({ current: null, @@ -10,11 +16,9 @@ export const ScrollWrapperContext = createContext>({ 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(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 ( diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/__tests__/useListenScroll.test.tsx b/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/__tests__/useListenScroll.test.tsx deleted file mode 100644 index 4d254e0f99..0000000000 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/__tests__/useListenScroll.test.tsx +++ /dev/null @@ -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(); - -const Wrapper = ({ children }: { children: React.ReactNode }) => ( - -
- {children} -
-
-); - -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); - }); -}); diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useListenScroll.ts b/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useListenScroll.ts deleted file mode 100644 index 7607c51fef..0000000000 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useListenScroll.ts +++ /dev/null @@ -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 = ({ - scrollableRef, -}: { - scrollableRef: React.RefObject; -}) => { - 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, - ]); -}; diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/states/isScrollingState.ts b/packages/twenty-front/src/modules/ui/utilities/scroll/states/isScrollingState.ts deleted file mode 100644 index 08ba13a85b..0000000000 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/states/isScrollingState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createState } from '@/ui/utilities/state/utils/createState'; - -export const isScrollingState = createState({ - key: 'scroll/isScollingState', - defaultValue: false, -}); diff --git a/yarn.lock b/yarn.lock index 03fa2cec31..0c9a021a7e 100644 --- a/yarn.lock +++ b/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"