Repository navigation
Conversation
|
This PR exceeds the recommended size of 1000 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size. |
This fixes a number of layout bugs I found in the web viewer. I moved its implementation to web components and css grid, which should make it significantly more stable. Signed-off-by: Ethan Mahintorabi <ethanmoon@google.com>
There was a problem hiding this comment.
Code Review
This pull request replaces the external GoldenLayout dependency with a custom, native CSS Grid-based layout manager (GridLayoutManager) along with custom elements (, , and ) to handle window panes, drag-to-resize splitters, and tabs. It also updates the main application, styles, and adds comprehensive unit tests. The review feedback highlights several areas for improvement: adding truthiness checks for the global app object to prevent potential TypeError crashes, reusing restorePane and collapsePane helper methods to ensure UI states and local storage persistence remain synchronized, and binding pointer drag event listeners to window instead of the narrow splitter elements to prevent drag operations from breaking.
| if (typeof app !== 'undefined' && app.map) { | ||
| setTimeout(() => app.map.invalidateSize({ animate: false }), 0); | ||
| } |
There was a problem hiding this comment.
If app is null, typeof app !== 'undefined' evaluates to true (since typeof null is 'object'). Accessing app.map will then throw a TypeError. Adding a truthiness check for app ensures robust defensive programming.
| if (typeof app !== 'undefined' && app.map) { | |
| setTimeout(() => app.map.invalidateSize({ animate: false }), 0); | |
| } | |
| if (typeof app !== 'undefined' && app && app.map) { | |
| setTimeout(() => app.map.invalidateSize({ animate: false }), 0); | |
| } |
| if (typeof app !== 'undefined') { | ||
| if (componentType === 'LayoutViewer' && app.map) { | ||
| setTimeout(() => app.map.invalidateSize({ animate: false }), 0); | ||
| } | ||
| if (componentType === '3DViewer' && app.threeDViewerWidget) { | ||
| setTimeout(() => app.threeDViewerWidget.onOpen(), 0); | ||
| } | ||
| } |
There was a problem hiding this comment.
If app is null, typeof app !== 'undefined' evaluates to true, and accessing app.map or app.threeDViewerWidget will throw a TypeError. Adding a truthiness check for app prevents potential runtime crashes.
| if (typeof app !== 'undefined') { | |
| if (componentType === 'LayoutViewer' && app.map) { | |
| setTimeout(() => app.map.invalidateSize({ animate: false }), 0); | |
| } | |
| if (componentType === '3DViewer' && app.threeDViewerWidget) { | |
| setTimeout(() => app.threeDViewerWidget.onOpen(), 0); | |
| } | |
| } | |
| if (typeof app !== 'undefined' && app) { | |
| if (componentType === 'LayoutViewer' && app.map) { | |
| setTimeout(() => app.map.invalidateSize({ animate: false }), 0); | |
| } | |
| if (componentType === '3DViewer' && app.threeDViewerWidget) { | |
| setTimeout(() => app.threeDViewerWidget.onOpen(), 0); | |
| } | |
| } |
| if (comp.paneName === 'left') { | ||
| this.containerEl.classList.remove('hide-left'); | ||
| const btn = this.containerEl.querySelector('#btn-collapse-left'); | ||
| if (btn) { btn.textContent = '◀'; btn.title = 'Collapse Left Panel'; } | ||
| } else if (comp.paneName === 'right') { | ||
| this.containerEl.classList.remove('hide-right'); | ||
| const btn = this.containerEl.querySelector('#btn-collapse-right'); | ||
| if (btn) { btn.textContent = '▶'; btn.title = 'Collapse Right Panel'; } | ||
| } else if (comp.paneName === 'bottom' && this.centerEl) { | ||
| this.centerEl.classList.remove('hide-bottom'); | ||
| const btn = this.containerEl.querySelector('#btn-collapse-bottom'); | ||
| if (btn) { btn.textContent = '▼'; btn.title = 'Collapse Console'; } | ||
| } |
There was a problem hiding this comment.
This block manually removes the hide-* classes and updates the collapse buttons, which duplicates the logic in restorePane and _setPaneCollapsed. Crucially, it misses calling setCollapsed(false) on the pane component and saveLayout(), causing the custom element's internal state to desynchronize and the uncollapsed state to not be persisted. Reusing restorePane resolves these issues cleanly.
if (['left', 'right', 'bottom'].includes(comp.paneName)) {
this.restorePane(comp.paneName);
}| this.activeTabs[comp.paneName] = null; | ||
| if (comp.paneName === 'left') this.containerEl.classList.add('hide-left'); | ||
| if (comp.paneName === 'right') this.containerEl.classList.add('hide-right'); | ||
| if (comp.paneName === 'bottom' && this.centerEl) this.centerEl.classList.add('hide-bottom'); | ||
| } |
There was a problem hiding this comment.
Manually adding the hide-* classes here duplicates the collapsing logic and bypasses collapsePane. This results in the custom element's internal state and collapse button icon/title not being updated, and the collapsed state not being persisted to localStorage. Reusing collapsePane ensures consistent state and proper persistence.
| this.activeTabs[comp.paneName] = null; | |
| if (comp.paneName === 'left') this.containerEl.classList.add('hide-left'); | |
| if (comp.paneName === 'right') this.containerEl.classList.add('hide-right'); | |
| if (comp.paneName === 'bottom' && this.centerEl) this.centerEl.classList.add('hide-bottom'); | |
| } | |
| this.activeTabs[comp.paneName] = null; | |
| if (['left', 'right', 'bottom'].includes(comp.paneName)) { | |
| this.collapsePane(comp.paneName); | |
| } |
| if (savedState.hideLeft) this.containerEl.classList.add('hide-left'); | ||
| if (savedState.hideRight) this.containerEl.classList.add('hide-right'); | ||
| if (savedState.hideBottom && this.centerEl) this.centerEl.classList.add('hide-bottom'); |
There was a problem hiding this comment.
Manually adding the hide-* classes on load bypasses the custom element state updates and collapse button icon/title synchronization. This causes the collapse buttons to display incorrect icons (e.g., showing 'Collapse' instead of 'Expand') when starting with a collapsed pane. Reusing collapsePane ensures the UI is fully synchronized on initial load.
| if (savedState.hideLeft) this.containerEl.classList.add('hide-left'); | |
| if (savedState.hideRight) this.containerEl.classList.add('hide-right'); | |
| if (savedState.hideBottom && this.centerEl) this.centerEl.classList.add('hide-bottom'); | |
| if (savedState.hideLeft) this.collapsePane('left'); | |
| if (savedState.hideRight) this.collapsePane('right'); | |
| if (savedState.hideBottom) this.collapsePane('bottom'); |
| this.addEventListener('pointermove', this._onPointerMove); | ||
| this.addEventListener('pointerup', this._onPointerUp); | ||
| this.addEventListener('pointercancel', this._onPointerUp); |
There was a problem hiding this comment.
Binding the pointermove, pointerup, and pointercancel listeners to this (the splitter element) can cause the drag operation to break if pointer capture fails or is lost and the pointer leaves the narrow 4px splitter boundaries. Binding these listeners to window ensures a highly robust and smooth dragging experience. Remember to also update the corresponding removeEventListener calls in _onPointerUp to target window.
| this.addEventListener('pointermove', this._onPointerMove); | |
| this.addEventListener('pointerup', this._onPointerUp); | |
| this.addEventListener('pointercancel', this._onPointerUp); | |
| window.addEventListener('pointermove', this._onPointerMove); | |
| window.addEventListener('pointerup', this._onPointerUp); | |
| window.addEventListener('pointercancel', this._onPointerUp); |
| splitterEl.addEventListener('pointermove', onMove); | ||
| splitterEl.addEventListener('pointerup', onUp); | ||
| splitterEl.addEventListener('pointercancel', onUp); |
There was a problem hiding this comment.
Binding the pointermove, pointerup, and pointercancel listeners to splitterEl can cause the drag operation to break immediately when the pointer leaves the narrow 4px splitter boundaries if pointer capture fails or is not supported. Binding these listeners to window ensures robust dragging. Remember to also update the corresponding removeEventListener calls in onUp to target window.
| splitterEl.addEventListener('pointermove', onMove); | |
| splitterEl.addEventListener('pointerup', onUp); | |
| splitterEl.addEventListener('pointercancel', onUp); | |
| window.addEventListener('pointermove', onMove); | |
| window.addEventListener('pointerup', onUp); | |
| window.addEventListener('pointercancel', onUp); |
c05101c to
4854c17
Compare
|
@gadfort would you mind giving this a try and seeing if it feels more stable than baseline? |
This fixes a number of layout bugs I found in the web viewer.
I moved its implementation to web components and css grid, which should make it significantly more stable.