From 79c38be81cc258c048287bf64ac8d0bcaecbcea0 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 07:15:16 +0100 Subject: [PATCH] Enable horizontal scrolling with snap-to-default on mobile topic tree (#1034) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com> --- app/src/components/Tree/index.tsx | 84 ++++++++++++++++++++++++++----- 1 file changed, 71 insertions(+), 13 deletions(-) diff --git a/app/src/components/Tree/index.tsx b/app/src/components/Tree/index.tsx index b4f486a..94e48aa 100644 --- a/app/src/components/Tree/index.tsx +++ b/app/src/components/Tree/index.tsx @@ -13,6 +13,9 @@ const MovingAverage = require('moving-average') const averagingTimeInterval = 10 * 1000 const average = MovingAverage(averagingTimeInterval) +// Mobile viewport breakpoint - matches CSS media queries in ContentView +const MOBILE_BREAKPOINT = 768 + declare var window: any interface Props { @@ -26,6 +29,7 @@ interface Props { interface State { lastUpdate: number + isMobile: boolean } function useArrowKeyEventHandler(actions: typeof treeActions) { @@ -53,12 +57,16 @@ function useArrowKeyEventHandler(actions: typeof treeActions) { class TreeComponent extends React.PureComponent { private updateTimer?: any + private resizeTimer?: any private perf: number = 0 private renderTime = 0 constructor(props: any) { super(props) - this.state = { lastUpdate: 0 } + this.state = { + lastUpdate: 0, + isMobile: typeof window !== 'undefined' && window.innerWidth <= MOBILE_BREAKPOINT, + } } private keyEventHandler = useArrowKeyEventHandler(this.props.actions) @@ -66,6 +74,27 @@ class TreeComponent extends React.PureComponent { average.push(Date.now(), ms) } + private handleResize = () => { + // Debounce resize events - only update after user stops resizing + if (this.resizeTimer) { + clearTimeout(this.resizeTimer) + } + + this.resizeTimer = setTimeout(() => { + const isMobile = typeof window !== 'undefined' && window.innerWidth <= MOBILE_BREAKPOINT + if (this.state.isMobile !== isMobile) { + this.setState({ isMobile }) + } + this.resizeTimer = undefined + }, 150) // Wait 150ms after last resize event + } + + public componentDidMount() { + if (typeof window !== 'undefined') { + window.addEventListener('resize', this.handleResize) + } + } + public componentWillReceiveProps(nextProps: Props) { if (this.props.tree !== nextProps.tree) { if (this.props.tree) { @@ -80,6 +109,18 @@ class TreeComponent extends React.PureComponent { public componentWillUnmount() { this.props.tree && this.props.tree.didUpdate.unsubscribe(this.throttledTreeUpdate) + if (typeof window !== 'undefined') { + window.removeEventListener('resize', this.handleResize) + } + // Clean up any pending timers to prevent memory leaks + if (this.resizeTimer) { + clearTimeout(this.resizeTimer) + this.resizeTimer = undefined + } + if (this.updateTimer) { + clearTimeout(this.updateTimer) + this.updateTimer = undefined + } } public throttledTreeUpdate = () => { @@ -127,30 +168,47 @@ class TreeComponent extends React.PureComponent { return null } + const { isMobile } = this.state + const style: React.CSSProperties = { lineHeight: '1.1', cursor: 'default', overflowY: 'scroll', - overflowX: 'hidden', + overflowX: isMobile ? 'auto' : 'hidden', // Enable horizontal scrolling on mobile height: '100%', width: '100%', outline: '24px black !important', paddingBottom: '16px', // avoid conflict with chart panel Resizer + // Scroll snap to default position on mobile + ...(isMobile && { + scrollSnapType: 'x mandatory', + WebkitOverflowScrolling: 'touch', // Smooth scrolling on iOS + }), } + const treeNode = ( + + ) + return (
- + {isMobile ? ( +
+ {treeNode} +
+ ) : ( + treeNode + )}
) }