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 averagingTimeInterval = 10 * 1000
const average = MovingAverage(averagingTimeInterval) const average = MovingAverage(averagingTimeInterval)
// Mobile viewport breakpoint - matches CSS media queries in ContentView
const MOBILE_BREAKPOINT = 768
declare var window: any declare var window: any
interface Props { interface Props {
@@ -26,6 +29,7 @@ interface Props {
interface State { interface State {
lastUpdate: number lastUpdate: number
isMobile: boolean
} }
function useArrowKeyEventHandler(actions: typeof treeActions) { function useArrowKeyEventHandler(actions: typeof treeActions) {
@@ -53,12 +57,16 @@ function useArrowKeyEventHandler(actions: typeof treeActions) {
class TreeComponent extends React.PureComponent<Props, State> { class TreeComponent extends React.PureComponent<Props, State> {
private updateTimer?: any private updateTimer?: any
private resizeTimer?: any
private perf: number = 0 private perf: number = 0
private renderTime = 0 private renderTime = 0
constructor(props: any) { constructor(props: any) {
super(props) super(props)
this.state = { lastUpdate: 0 } this.state = {
lastUpdate: 0,
isMobile: typeof window !== 'undefined' && window.innerWidth <= MOBILE_BREAKPOINT,
}
} }
private keyEventHandler = useArrowKeyEventHandler(this.props.actions) private keyEventHandler = useArrowKeyEventHandler(this.props.actions)
@@ -66,6 +74,27 @@ class TreeComponent extends React.PureComponent<Props, State> {
average.push(Date.now(), ms) 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) { public componentWillReceiveProps(nextProps: Props) {
if (this.props.tree !== nextProps.tree) { if (this.props.tree !== nextProps.tree) {
if (this.props.tree) { if (this.props.tree) {
@@ -80,6 +109,18 @@ class TreeComponent extends React.PureComponent<Props, State> {
public componentWillUnmount() { public componentWillUnmount() {
this.props.tree && this.props.tree.didUpdate.unsubscribe(this.throttledTreeUpdate) 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 = () => { public throttledTreeUpdate = () => {
@@ -127,19 +168,25 @@ class TreeComponent extends React.PureComponent<Props, State> {
return null return null
} }
const { isMobile } = this.state
const style: React.CSSProperties = { const style: React.CSSProperties = {
lineHeight: '1.1', lineHeight: '1.1',
cursor: 'default', cursor: 'default',
overflowY: 'scroll', overflowY: 'scroll',
overflowX: 'hidden', overflowX: isMobile ? 'auto' : 'hidden', // Enable horizontal scrolling on mobile
height: '100%', height: '100%',
width: '100%', width: '100%',
outline: '24px black !important', outline: '24px black !important',
paddingBottom: '16px', // avoid conflict with chart panel Resizer 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 ( const treeNode = (
<div style={style} tabIndex={0} onKeyDown={this.keyEventHandler}>
<TreeNode <TreeNode
key={tree.hash()} key={tree.hash()}
isRoot={true} isRoot={true}
@@ -151,6 +198,17 @@ class TreeComponent extends React.PureComponent<Props, State> {
actions={this.props.actions} actions={this.props.actions}
selectTopicAction={this.props.actions.selectTopic} 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> </div>
) )
} }