Skip to content

Commit c4d0ede

Browse files
authored
fix(ui): prevent whole-UI window scrollbars (#111)
* fix(ui): prevent whole-UI window scrollbars Add overflow hidden rules to global styles, component host selectors, and shell container components to prevent full-page UI scrollbars. Allow vertical scrolling within sub-views requiring internal scroll. * test(shell): address PR #111 review comments Remove redundant host binding from ComposerShell component and remove JSDOM unit test for CSS overflow. Add Playwright E2E test suite verifying root overflow container styling, window scrollbar prevention under tall content injection, and view-level internal scrolling.
1 parent 0e7cad2 commit c4d0ede

6 files changed

Lines changed: 141 additions & 2 deletions

File tree

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {test, expect} from '@playwright/test';
18+
19+
test.beforeEach(async ({page}) => {
20+
page.on('pageerror', err => {
21+
console.error(`Unhandled page error: ${err.message}`);
22+
});
23+
});
24+
25+
test.describe('Whole-UI Window Scrollbar Prevention', () => {
26+
test('verifies body, html, and shell layout containers apply overflow hidden styling', async ({
27+
page,
28+
}) => {
29+
await page.goto('/');
30+
await expect(page.locator('a2ui-composer-shell')).toBeVisible();
31+
32+
const styles = await page.evaluate(() => {
33+
const bodyStyle = window.getComputedStyle(document.body);
34+
const shellEl = document.querySelector('a2ui-composer-shell');
35+
const shellStyle = shellEl ? window.getComputedStyle(shellEl) : null;
36+
const workspaceEl = document.querySelector('a2ui-composer-workspace');
37+
const workspaceStyle = workspaceEl ? window.getComputedStyle(workspaceEl) : null;
38+
39+
return {
40+
bodyOverflow: bodyStyle.overflow,
41+
bodyOverflowY: bodyStyle.overflowY,
42+
shellOverflow: shellStyle ? shellStyle.overflow : null,
43+
workspaceOverflow: workspaceStyle ? workspaceStyle.overflow : null,
44+
};
45+
});
46+
47+
expect(styles.bodyOverflow === 'hidden' || styles.bodyOverflowY === 'hidden').toBe(true);
48+
expect(styles.shellOverflow).toBe('hidden');
49+
expect(styles.workspaceOverflow).toBe('hidden');
50+
});
51+
52+
test('prevents window-level scrolling even when tall content is dynamically present', async ({
53+
page,
54+
}) => {
55+
await page.goto('/');
56+
await expect(page.locator('a2ui-composer-shell')).toBeVisible();
57+
58+
// Inject an oversized container to force overflow conditions
59+
await page.evaluate(() => {
60+
const spacer = document.createElement('div');
61+
spacer.id = 'e2e-overflow-spacer';
62+
spacer.style.height = '5000px';
63+
spacer.style.width = '100%';
64+
document.body.appendChild(spacer);
65+
});
66+
67+
// Attempt to scroll the main document window
68+
await page.evaluate(() => {
69+
window.scrollTo(0, 1000);
70+
});
71+
72+
const scrollY = await page.evaluate(() => window.scrollY);
73+
const scrollTop = await page.evaluate(
74+
() => document.documentElement.scrollTop || document.body.scrollTop,
75+
);
76+
77+
expect(scrollY).toBe(0);
78+
expect(scrollTop).toBe(0);
79+
80+
// Clean up injected spacer element
81+
await page.evaluate(() => {
82+
document.getElementById('e2e-overflow-spacer')?.remove();
83+
});
84+
});
85+
86+
test('allows internal vertical scrolling in settings view while preventing whole-window scrollbars', async ({
87+
page,
88+
}) => {
89+
await page.goto('/settings');
90+
await page.waitForURL('**/settings');
91+
await expect(page.locator('.settings-container')).toBeVisible();
92+
93+
const bodyOverflow = await page.evaluate(() => window.getComputedStyle(document.body).overflow);
94+
expect(bodyOverflow === 'hidden' || bodyOverflow === 'clip').toBe(true);
95+
96+
const settingsHostOverflowY = await page.evaluate(() => {
97+
const settingsEl = document.querySelector('a2ui-composer-settings');
98+
return settingsEl ? window.getComputedStyle(settingsEl).overflowY : null;
99+
});
100+
101+
expect(settingsHostOverflowY).toBe('auto');
102+
103+
// Verify main window scroll is 0
104+
await page.evaluate(() => window.scrollTo(0, 500));
105+
const windowScrollY = await page.evaluate(() => window.scrollY);
106+
expect(windowScrollY).toBe(0);
107+
});
108+
109+
test('preserves window scrollbar prevention in components gallery view', async ({page}) => {
110+
await page.goto('/gallery');
111+
await page.waitForURL('**/gallery');
112+
await expect(page.locator('.gallery-container')).toBeVisible();
113+
114+
const bodyOverflow = await page.evaluate(() => window.getComputedStyle(document.body).overflow);
115+
expect(bodyOverflow === 'hidden' || bodyOverflow === 'clip').toBe(true);
116+
117+
await page.evaluate(() => window.scrollTo(0, 500));
118+
const windowScrollY = await page.evaluate(() => window.scrollY);
119+
expect(windowScrollY).toBe(0);
120+
});
121+
});

shell/src/app/app.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@
1818
:host {
1919
display: block;
2020
height: 100%;
21+
overflow: hidden;
2122
}

shell/src/app/settings/settings-view/settings.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
* limitations under the License.
1616
*/
1717

18+
:host {
19+
display: block;
20+
height: 100%;
21+
overflow-y: auto;
22+
}
23+
1824
.settings-container {
1925
padding: 24px;
2026
max-width: 600px;

shell/src/app/shell/composer-shell/composer-shell.scss

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
display: flex;
1919
flex-direction: column;
2020
height: 100%;
21+
overflow: hidden;
2122
}
23+
2224
.composer-header {
2325
position: relative;
2426
z-index: 2;
@@ -32,22 +34,25 @@
3234
.spacer {
3335
flex: 1 1 auto;
3436
}
37+
3538
.composer-sidenav-container {
3639
flex: 1;
3740
min-height: 0;
41+
overflow: hidden;
3842
}
43+
3944
.composer-sidenav {
4045
width: 250px;
4146
background-color: var(--mat-sys-surface-container);
4247
border-right: 1px solid var(--mat-sys-outline-variant);
4348
--mat-sidenav-container-shape: 0px;
4449
--mat-drawer-container-shape: 0px;
45-
border-radius: 0px;
50+
border-radius: 0;
4651
--mat-sidenav-background-color: var(--mat-sys-surface-container);
4752
transition: width 200ms ease-in-out;
4853

4954
::ng-deep .mat-drawer-inner-container {
50-
border-radius: 0px;
55+
border-radius: 0;
5156
white-space: nowrap;
5257
overflow-x: hidden;
5358
}
@@ -88,10 +93,13 @@
8893
padding: 0;
8994
box-sizing: border-box;
9095
height: 100%;
96+
overflow: hidden;
9197
}
98+
9299
.hamburger-button {
93100
margin-right: 8px;
94101
}
102+
95103
.theme-toggle-button {
96104
margin-left: 8px;
97105
}

shell/src/app/shell/composer-workspace/composer-workspace.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
:host {
1818
display: block;
1919
height: 100%;
20+
overflow: hidden;
2021
}
2122

2223
.workspace-container {
2324
display: flex;
2425
height: 100%;
2526
box-sizing: border-box;
27+
overflow: hidden;
2628

2729
&.extension-mode {
2830
flex-direction: column;

shell/src/global_styles.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ body {
229229
height: 100%;
230230
margin: 0;
231231
padding: 0;
232+
overflow: hidden;
232233
background-color: var(--mat-sys-background);
233234
color: var(--mat-sys-on-background);
234235
font-family: Roboto, 'Helvetica Neue', sans-serif;

0 commit comments

Comments
 (0)