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>
This commit is contained in:
Copilot
2026-01-27 07:15:16 +01:00
committed by GitHub
parent 207ded39ab
commit 79c38be81c

View File

@@ -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<Props, State> {
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<Props, State> {
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<Props, State> {
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,19 +168,25 @@ class TreeComponent extends React.PureComponent<Props, State> {
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
}),
}
return (
<div style={style} tabIndex={0} onKeyDown={this.keyEventHandler}>
const treeNode = (
<TreeNode
key={tree.hash()}
isRoot={true}
@@ -151,6 +198,17 @@ class TreeComponent extends React.PureComponent<Props, State> {
actions={this.props.actions}
selectTopicAction={this.props.actions.selectTopic}
/>
)
return (
<div style={style} tabIndex={0} onKeyDown={this.keyEventHandler}>
{isMobile ? (
<div style={{ scrollSnapAlign: 'start', minWidth: '100%' }}>
{treeNode}
</div>
) : (
treeNode
)}
</div>
)
}