webui: fix diff view scrollbar visibility and resize handling
Fix Monaco editor scrollbar display issues and improve browser window resize
responsiveness in the diff view, providing a cleaner interface and better
user experience across different screen sizes.
Problem Analysis:
The diff view had two significant issues affecting usability:
1. Monaco Scrollbar Visibility: Despite setting scrollbar configuration to
'hidden', a large gray scrollbar remained visible on the right side of
the Monaco diff editor. This was caused by insufficient CSS targeting
of Monaco's complex DOM structure and scrollbar element hierarchy.
2. Resize Handling: The diff view did not properly adapt when users resized
their browser window. While the editor had automaticLayout: false and
manual sizing, there was no window resize listener to trigger layout
recalculation, causing the editor to maintain its original dimensions.
3. Refresh Button Layout: At certain screen widths, the refresh button would
wrap to its own line prematurely due to inflexible sizing constraints.
Implementation Changes:
1. Monaco Scrollbar Removal (sketch-diff2-view.ts):
- Added comprehensive global CSS rules targeting all Monaco scrollbar elements
- Targeted .monaco-editor, .monaco-diff-editor, and .monaco-scrollable-element
- Applied multiple hiding techniques: display: none, visibility: hidden,
width/height: 0, opacity: 0 for maximum coverage
- Added padding/margin removal to prevent scrollbar space reservation
- Ensured diff content takes full width without scrollbar spacing
2. Window Resize Handler (sketch-monaco-view.ts):
- Added setupWindowResizeHandler() method with debounced resize logic
- Implemented 100ms debounce to prevent excessive layout calls
- Added window 'resize' event listener that triggers fitEditorToContent()
- Fallback layout call with current container dimensions if fit function unavailable
- Proper cleanup in disconnectedCallback() to prevent memory leaks
3. Layout Improvements (sketch-diff2-view.ts):
- Set minimum width (400px) for sketch-diff-range-picker component
- Added minimum width (120px) for file-count display
- Ensured flex layout provides adequate space for all controls
- Improved responsive behavior at various screen widths
4. Enhanced Scrollbar Configuration (sketch-monaco-view.ts):
- Extended scrollbar options with additional Monaco-specific settings:
- useShadows: false to disable scrollbar shadows
- verticalHasArrows: false / horizontalHasArrows: false to remove arrows
- verticalScrollbarSize: 0 / horizontalScrollbarSize: 0 for zero track size
- Combined configuration-based and CSS-based hiding for complete coverage
Technical Details:
- Global CSS injection occurs once per diff view instance in constructor
- Window resize handler uses setTimeout debouncing to avoid performance issues
- Monaco editor layout() called with explicit dimensions during resize
- CSS targeting covers all known Monaco scrollbar element patterns
- Minimum width constraints prevent layout collapse at small screen sizes
- Cleanup handlers prevent memory leaks when components are destroyed
Benefits:
- Clean, professional diff view appearance without distracting scrollbars
- Smooth responsive behavior when browser window is resized
- Improved layout stability for controls at various screen widths
- Better user experience across desktop and mobile viewport sizes
- Maintained full Monaco editor functionality (editing, syntax highlighting, etc.)
Testing:
- Verified scrollbar completely hidden at all screen sizes
- Tested resize responsiveness from 600px to 1400px+ widths
- Confirmed smooth transitions during window resize operations
- Validated refresh button layout behavior at different breakpoints
- Ensured Monaco editor features remain fully functional
- Tested both horizontal and vertical window resize scenarios
This implementation provides a polished, responsive diff view experience
that properly adapts to user browser configurations while maintaining
all advanced Monaco editor capabilities.
Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: sf19d359b4fcbcbdek
diff --git a/webui/src/web-components/sketch-diff2-view.ts b/webui/src/web-components/sketch-diff2-view.ts
index 185db7b..722be56 100644
--- a/webui/src/web-components/sketch-diff2-view.ts
+++ b/webui/src/web-components/sketch-diff2-view.ts
@@ -191,7 +191,7 @@
sketch-diff-range-picker {
flex: 1;
- min-width: 0;
+ min-width: 400px; /* Ensure minimum width for range picker */
}
sketch-diff-file-picker {
@@ -362,6 +362,7 @@
border: 1px solid var(--border-color, #e0e0e0);
white-space: nowrap;
flex-shrink: 0;
+ min-width: 120px; /* Ensure minimum width for file count */
}
.loading,
@@ -406,8 +407,8 @@
super();
console.log("SketchDiff2View initialized");
- // Fix for monaco-aria-container positioning
- // Add a global style to ensure proper positioning of aria containers
+ // Fix for monaco-aria-container positioning and hide scrollbars globally
+ // Add a global style to ensure proper positioning of aria containers and hide scrollbars
const styleElement = document.createElement("style");
styleElement.textContent = `
.monaco-aria-container {
@@ -424,6 +425,52 @@
border: 0 !important;
z-index: -1 !important;
}
+
+ /* Aggressively hide all Monaco scrollbar elements */
+ .monaco-editor .scrollbar,
+ .monaco-editor .scroll-decoration,
+ .monaco-editor .invisible.scrollbar,
+ .monaco-editor .slider,
+ .monaco-editor .vertical.scrollbar,
+ .monaco-editor .horizontal.scrollbar,
+ .monaco-diff-editor .scrollbar,
+ .monaco-diff-editor .scroll-decoration,
+ .monaco-diff-editor .invisible.scrollbar,
+ .monaco-diff-editor .slider,
+ .monaco-diff-editor .vertical.scrollbar,
+ .monaco-diff-editor .horizontal.scrollbar {
+ display: none !important;
+ visibility: hidden !important;
+ width: 0 !important;
+ height: 0 !important;
+ opacity: 0 !important;
+ }
+
+ /* Target the specific scrollbar classes that Monaco uses */
+ .monaco-scrollable-element > .scrollbar,
+ .monaco-scrollable-element > .scroll-decoration,
+ .monaco-scrollable-element .slider {
+ display: none !important;
+ visibility: hidden !important;
+ width: 0 !important;
+ height: 0 !important;
+ }
+
+ /* Remove scrollbar space/padding from content area */
+ .monaco-editor .monaco-scrollable-element,
+ .monaco-diff-editor .monaco-scrollable-element {
+ padding-right: 0 !important;
+ padding-bottom: 0 !important;
+ margin-right: 0 !important;
+ margin-bottom: 0 !important;
+ }
+
+ /* Ensure the diff content takes full width without scrollbar space */
+ .monaco-diff-editor .editor.modified,
+ .monaco-diff-editor .editor.original {
+ margin-right: 0 !important;
+ padding-right: 0 !important;
+ }
`;
document.head.appendChild(styleElement);
}
diff --git a/webui/src/web-components/sketch-monaco-view.ts b/webui/src/web-components/sketch-monaco-view.ts
index 915da57..5fe57b1 100644
--- a/webui/src/web-components/sketch-monaco-view.ts
+++ b/webui/src/web-components/sketch-monaco-view.ts
@@ -40,6 +40,26 @@
.monaco-editor .margin {
background-color: var(--monaco-editor-margin, #f5f5f5) !important;
}
+
+ /* Hide all scrollbars completely */
+ .monaco-editor .scrollbar,
+ .monaco-editor .scroll-decoration,
+ .monaco-editor .invisible.scrollbar,
+ .monaco-editor .slider,
+ .monaco-editor .vertical.scrollbar,
+ .monaco-editor .horizontal.scrollbar {
+ display: none !important;
+ visibility: hidden !important;
+ width: 0 !important;
+ height: 0 !important;
+ }
+
+ /* Ensure content area takes full width/height without scrollbar space */
+ .monaco-editor .monaco-scrollable-element {
+ /* Remove any padding/margin that might be reserved for scrollbars */
+ padding-right: 0 !important;
+ padding-bottom: 0 !important;
+ }
`;
// Configure Monaco to use local workers with correct relative paths
@@ -636,6 +656,11 @@
vertical: "hidden",
horizontal: "hidden",
handleMouseWheel: false, // Let outer scroller eat the wheel
+ useShadows: false, // Disable scrollbar shadows
+ verticalHasArrows: false, // Remove scrollbar arrows
+ horizontalHasArrows: false, // Remove scrollbar arrows
+ verticalScrollbarSize: 0, // Set scrollbar track width to 0
+ horizontalScrollbarSize: 0, // Set scrollbar track height to 0
},
minimap: { enabled: false },
overviewRulerLanes: 0,
@@ -1190,11 +1215,47 @@
private fitEditorToContent: (() => void) | null = null;
+ /**
+ * Set up window resize handler to ensure Monaco editor adapts to browser window changes
+ */
+ private setupWindowResizeHandler() {
+ // Create a debounced resize handler to avoid too many layout calls
+ let resizeTimeout: number | null = null;
+
+ this._windowResizeHandler = () => {
+ // Clear any existing timeout
+ if (resizeTimeout) {
+ window.clearTimeout(resizeTimeout);
+ }
+
+ // Debounce the resize to avoid excessive layout calls
+ resizeTimeout = window.setTimeout(() => {
+ if (this.editor && this.container.value) {
+ // Trigger layout recalculation
+ if (this.fitEditorToContent) {
+ this.fitEditorToContent();
+ } else {
+ // Fallback: just trigger a layout with current container dimensions
+ const width = this.container.value.offsetWidth;
+ const height = this.container.value.offsetHeight;
+ this.editor.layout({ width, height });
+ }
+ }
+ }, 100); // 100ms debounce
+ };
+
+ // Add the event listener
+ window.addEventListener('resize', this._windowResizeHandler);
+ }
+
// Add resize observer to ensure editor resizes when container changes
firstUpdated() {
// Initialize the editor
this.initializeEditor();
+ // Set up window resize handler to ensure Monaco editor adapts to browser window changes
+ this.setupWindowResizeHandler();
+
// For multi-file diff, we don't use ResizeObserver since we control the size
// Instead, we rely on auto-sizing based on content
@@ -1208,6 +1269,7 @@
}
private _resizeObserver: ResizeObserver | null = null;
+ private _windowResizeHandler: (() => void) | null = null;
/**
* Add this Monaco editor instance to the global debug object
@@ -1299,6 +1361,12 @@
document.removeEventListener("click", this._documentClickHandler);
this._documentClickHandler = null;
}
+
+ // Remove window resize handler if set
+ if (this._windowResizeHandler) {
+ window.removeEventListener('resize', this._windowResizeHandler);
+ this._windowResizeHandler = null;
+ }
} catch (error) {
console.error("Error in disconnectedCallback:", error);
}