Skip to content

Commit

Permalink
Update scroll provider so that it only captures the scroll on div con…
Browse files Browse the repository at this point in the history
…tainer and not the whole window
  • Loading branch information
joel-jeremy committed Oct 19, 2024
1 parent 5c94e38 commit 0e85c5b
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 131 deletions.
48 changes: 23 additions & 25 deletions packages/desktop-client/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,36 +166,34 @@ export function App() {
<SidebarProvider>
<BudgetMonthCountProvider>
<DndProvider backend={HTML5Backend}>
<ScrollProvider>
<View
data-theme={theme}
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
<View
data-theme={theme}
key={
hiddenScrollbars ? 'hidden-scrollbars' : 'scrollbars'
}
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
overflow: 'hidden',
...styles.lightScrollbar,
}}
>
<View
key={
hiddenScrollbars ? 'hidden-scrollbars' : 'scrollbars'
}
style={{
flexGrow: 1,
overflow: 'hidden',
...styles.lightScrollbar,
}}
>
<ErrorBoundary FallbackComponent={ErrorFallback}>
{process.env.REACT_APP_REVIEW_ID &&
!Platform.isPlaywright && <DevelopmentTopBar />}
<AppInner />
</ErrorBoundary>
<ThemeStyle />
<Modals />
<UpdateNotification />
</View>
<ErrorBoundary FallbackComponent={ErrorFallback}>
{process.env.REACT_APP_REVIEW_ID &&
!Platform.isPlaywright && <DevelopmentTopBar />}
<AppInner />
</ErrorBoundary>
<ThemeStyle />
<Modals />
<UpdateNotification />
</View>
</ScrollProvider>
</View>
</DndProvider>
</BudgetMonthCountProvider>
</SidebarProvider>
Expand Down
191 changes: 100 additions & 91 deletions packages/desktop-client/src/components/FinancesApp.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @ts-strict-ignore
import React, { type ReactElement, useEffect } from 'react';
import React, { type ReactElement, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import {
Expand Down Expand Up @@ -34,6 +34,7 @@ import { ManagePayeesPage } from './payees/ManagePayeesPage';
import { Reports } from './reports';
import { LoadingIndicator } from './reports/LoadingIndicator';
import { NarrowAlternate, WideComponent } from './responsive';
import { ScrollProvider } from './ScrollProvider';
import { Settings } from './settings';
import { FloatableSidebar } from './sidebar';
import { Titlebar } from './Titlebar';
Expand Down Expand Up @@ -133,6 +134,8 @@ export function FinancesApp() {
run();
}, [lastUsedVersion, setLastUsedVersion]);

const scrollableRef = useRef<HTMLDivElement>(null);

return (
<View style={{ height: '100%' }}>
<RouterBehaviors />
Expand All @@ -156,113 +159,119 @@ export function FinancesApp() {
width: '100%',
}}
>
<View
style={{
flex: 1,
overflow: 'auto',
position: 'relative',
}}
<ScrollProvider
isDisabled={!isNarrowWidth}
scrollableRef={scrollableRef}
>
<Titlebar
<View
ref={scrollableRef}
style={{
WebkitAppRegion: 'drag',
position: 'absolute',
top: 0,
left: 0,
right: 0,
zIndex: 1000,
flex: 1,
overflow: 'auto',
position: 'relative',
}}
/>
<Notifications />
<BankSyncStatus />
>
<Titlebar
style={{
WebkitAppRegion: 'drag',
position: 'absolute',
top: 0,
left: 0,
right: 0,
zIndex: 1000,
}}
/>
<Notifications />
<BankSyncStatus />

<Routes>
<Route
path="/"
element={
accountsLoaded ? (
accounts.length > 0 ? (
<Navigate to="/budget" replace />
<Routes>
<Route
path="/"
element={
accountsLoaded ? (
accounts.length > 0 ? (
<Navigate to="/budget" replace />
) : (
// If there are no accounts, we want to redirect the user to
// the All Accounts screen which will prompt them to add an account
<Navigate to="/accounts" replace />
)
) : (
// If there are no accounts, we want to redirect the user to
// the All Accounts screen which will prompt them to add an account
<Navigate to="/accounts" replace />
<LoadingIndicator />
)
) : (
<LoadingIndicator />
)
}
/>
}
/>

<Route path="/reports/*" element={<Reports />} />
<Route path="/reports/*" element={<Reports />} />

<Route
path="/budget"
element={<NarrowAlternate name="Budget" />}
/>
<Route
path="/budget"
element={<NarrowAlternate name="Budget" />}
/>

<Route
path="/schedules"
element={
<NarrowNotSupported>
<WideComponent name="Schedules" />
</NarrowNotSupported>
}
/>
<Route
path="/schedules"
element={
<NarrowNotSupported>
<WideComponent name="Schedules" />
</NarrowNotSupported>
}
/>

<Route path="/payees" element={<ManagePayeesPage />} />
<Route path="/rules" element={<ManageRulesPage />} />
<Route path="/settings" element={<Settings />} />
<Route path="/payees" element={<ManagePayeesPage />} />
<Route path="/rules" element={<ManageRulesPage />} />
<Route path="/settings" element={<Settings />} />

<Route
path="/gocardless/link"
element={
<NarrowNotSupported>
<WideComponent name="GoCardlessLink" />
</NarrowNotSupported>
}
/>
<Route
path="/gocardless/link"
element={
<NarrowNotSupported>
<WideComponent name="GoCardlessLink" />
</NarrowNotSupported>
}
/>

<Route
path="/accounts"
element={<NarrowAlternate name="Accounts" />}
/>
<Route
path="/accounts"
element={<NarrowAlternate name="Accounts" />}
/>

<Route
path="/accounts/:id"
element={<NarrowAlternate name="Account" />}
/>
<Route
path="/accounts/:id"
element={<NarrowAlternate name="Account" />}
/>

<Route
path="/transactions/:transactionId"
element={
<WideNotSupported>
<TransactionEdit />
</WideNotSupported>
}
/>
<Route
path="/transactions/:transactionId"
element={
<WideNotSupported>
<TransactionEdit />
</WideNotSupported>
}
/>

<Route
path="/categories/:id"
element={
<WideNotSupported>
<Category />
</WideNotSupported>
}
/>
<Route
path="/categories/:id"
element={
<WideNotSupported>
<Category />
</WideNotSupported>
}
/>

{/* redirect all other traffic to the budget page */}
<Route path="/*" element={<Navigate to="/budget" replace />} />
</Routes>
</View>
{/* redirect all other traffic to the budget page */}
<Route path="/*" element={<Navigate to="/budget" replace />} />
</Routes>
</View>

<Routes>
<Route path="/budget" element={<MobileNavTabs />} />
<Route path="/accounts" element={<MobileNavTabs />} />
<Route path="/settings" element={<MobileNavTabs />} />
<Route path="/reports" element={<MobileNavTabs />} />
<Route path="*" element={null} />
</Routes>
<Routes>
<Route path="/budget" element={<MobileNavTabs />} />
<Route path="/accounts" element={<MobileNavTabs />} />
<Route path="/settings" element={<MobileNavTabs />} />
<Route path="/reports" element={<MobileNavTabs />} />
<Route path="*" element={null} />
</Routes>
</ScrollProvider>
</View>
</View>
</View>
Expand Down
57 changes: 42 additions & 15 deletions packages/desktop-client/src/components/ScrollProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @ts-strict-ignore
import React, {
type ReactNode,
type RefObject,
createContext,
useState,
useContext,
Expand All @@ -17,37 +17,60 @@ type IScrollContext = {

const ScrollContext = createContext<IScrollContext | undefined>(undefined);

type ScrollProviderProps = {
type ScrollProviderProps<T extends Element> = {
scrollableRef: RefObject<T>;
isDisabled: boolean;
children?: ReactNode;
};

export function ScrollProvider({ children }: ScrollProviderProps) {
const [scrollY, setScrollY] = useState(undefined);
const [scrollHeight, setScrollHeight] = useState(undefined);
const [clientHeight, setClientHeight] = useState(undefined);
export function ScrollProvider<T extends Element>({
scrollableRef,
isDisabled,
children,
}: ScrollProviderProps<T>) {
const [scrollY, setScrollY] = useState<number | undefined>(undefined);
const [scrollHeight, setScrollHeight] = useState<number | undefined>(
undefined,
);
const [clientHeight, setClientHeight] = useState<number | undefined>(
undefined,
);

const hasScrolledToBottom = useCallback(
(tolerance = 1) => scrollHeight - scrollY <= clientHeight + tolerance,
(tolerance = 1) => {
if (scrollHeight && scrollY && clientHeight) {
return scrollHeight - scrollY <= clientHeight + tolerance;
}
return false;
},
[clientHeight, scrollHeight, scrollY],
);

useEffect(() => {
const listenToScroll = debounce(e => {
if (isDisabled) {
return;
}

const listenToScroll = debounce((e: Event) => {
const target = e.target;
setScrollY(target?.scrollTop || 0);
setScrollHeight(target?.scrollHeight || 0);
setClientHeight(target?.clientHeight || 0);
if (target instanceof Element) {
setScrollY(target.scrollTop || 0);
setScrollHeight(target.scrollHeight || 0);
setClientHeight(target.clientHeight || 0);
}
}, 10);

window.addEventListener('scroll', listenToScroll, {
const ref = scrollableRef.current;

ref?.addEventListener('scroll', listenToScroll, {
capture: true,
passive: true,
});
return () =>
window.removeEventListener('scroll', listenToScroll, {
ref?.removeEventListener('scroll', listenToScroll, {
capture: true,
});
}, []);
}, [isDisabled, scrollableRef]);

return (
<ScrollContext.Provider value={{ scrollY, hasScrolledToBottom }}>
Expand All @@ -57,5 +80,9 @@ export function ScrollProvider({ children }: ScrollProviderProps) {
}

export function useScroll(): IScrollContext {
return useContext(ScrollContext);
const context = useContext(ScrollContext);
if (!context) {
throw new Error('useScroll must be used within a ScrollProvider');
}
return context;
}

0 comments on commit 0e85c5b

Please sign in to comment.