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:
@@ -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,30 +168,47 @@ 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
|
||||
}),
|
||||
}
|
||||
|
||||
const treeNode = (
|
||||
<TreeNode
|
||||
key={tree.hash()}
|
||||
isRoot={true}
|
||||
treeNode={tree}
|
||||
name={this.props.host}
|
||||
collapsed={false}
|
||||
settings={this.props.settings}
|
||||
lastUpdate={tree.lastUpdate}
|
||||
actions={this.props.actions}
|
||||
selectTopicAction={this.props.actions.selectTopic}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={style} tabIndex={0} onKeyDown={this.keyEventHandler}>
|
||||
<TreeNode
|
||||
key={tree.hash()}
|
||||
isRoot={true}
|
||||
treeNode={tree}
|
||||
name={this.props.host}
|
||||
collapsed={false}
|
||||
settings={this.props.settings}
|
||||
lastUpdate={tree.lastUpdate}
|
||||
actions={this.props.actions}
|
||||
selectTopicAction={this.props.actions.selectTopic}
|
||||
/>
|
||||
{isMobile ? (
|
||||
<div style={{ scrollSnapAlign: 'start', minWidth: '100%' }}>
|
||||
{treeNode}
|
||||
</div>
|
||||
) : (
|
||||
treeNode
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user