Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/static/css/pad/form.css
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,23 @@ select, .nice-select {
bottom: calc(100% + 5px);
top: auto;
}
.toolbar .nice-select .list {
.toolbar .nice-select .list,
/* Popups are scroll containers (see popup.css), which would otherwise clip the
absolutely-positioned dropdown list. Float it above the popup with fixed
positioning, matching how the toolbar dropdowns escape their container. */
.popup .nice-select .list {
position: fixed;
top: auto;
left: auto;
}
/* The default .reverse rule above sets bottom: calc(100% + 5px) so an
absolutely-positioned list opens upward inside its parent. Once the list is
position:fixed, that percentage resolves against the viewport instead and
would push the list off-screen, so we let JS place it via `top` only. */
.toolbar .nice-select.reverse .list,
.popup .nice-select.reverse .list {
bottom: auto;
}
.nice-select .list:hover .option:not(:hover) {
background-color: transparent !important;
}
Expand Down
15 changes: 11 additions & 4 deletions src/static/css/pad/popup.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@
background: #f7f7f7;
min-width: min(300px, 90vw);
max-width: min(600px, 95vw);
/* Constrain height so popups (notably Settings with Pad-wide Settings
enabled) scroll instead of cropping items off-screen on short windows.
Fixes #7696. */
max-height: calc(100vh - 20px);
overflow-y: auto;
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
}

/* Chat manages its own scroll and floats author-colour pickers outside the
popup, so it must not become a scroll container. */
.popup#users .popup-content {
overflow: visible;
}
.popup input[type=text] {
width: 100%;
Expand Down Expand Up @@ -76,10 +87,6 @@
}
.popup-content {
max-height: 80vh;
overflow: auto;
}
.popup#users .popup-content {
overflow: visible;
}
}
/* Move popup to the bottom, except popup linked to left toolbar, like hyperklink popup */
Expand Down
19 changes: 19 additions & 0 deletions src/static/js/vendors/nice-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,25 @@
}
$dropdown.find('.list').css('max-height', $maxListHeight + 'px');

// Popups are scroll containers (since #7696) which would clip the
// absolutely-positioned dropdown list. The list is repositioned with
// `position: fixed` (see form.css) so it floats above the popup; we
// need viewport-relative coordinates here. Done after the reverse
// class is decided so we know which side of the dropdown to anchor.
if ($dropdown.closest('.toolbar').length === 0
&& $dropdown.closest('.popup-content').length > 0) {
var rect = $dropdown[0].getBoundingClientRect();
var $list = $dropdown.find('.list');
$list.css('left', rect.left);
$list.css('min-width', $dropdown.outerWidth() + 'px');
// Clear .reverse's `bottom: calc(100% + 5px)` — with position:fixed
// it would resolve against the viewport and push the list offscreen.
$list.css('bottom', 'auto');
$list.css('top', $dropdown.hasClass('reverse')
? rect.top - $maxListHeight - 5
: rect.bottom);
}

} else {
$dropdown.trigger('focus');
}
Expand Down
53 changes: 53 additions & 0 deletions src/tests/frontend-new/specs/pad_settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,59 @@ test.describe('creator-owned pad settings', () => {
await context2.close();
});

// #7696: on a short viewport the settings popup must scroll so items in
// Pad-wide Settings (notably "Delete pad") stay reachable instead of being
// cropped off-screen with no scrollbar.
test('settings popup stays scrollable when the viewport is short', async ({page}) => {
await page.setViewportSize({width: 900, height: 500});
await goToNewPad(page);
await showSettings(page);

const popupContent = page.locator('#settings > .popup-content');
await expect(popupContent).toBeVisible();
await expect(page.locator('#pad-settings-section')).toBeVisible();

// The popup must declare scrollable overflow (otherwise the previous bug
// recurs even if content happens to fit by coincidence).
await expect(popupContent).toHaveCSS('overflow-y', 'auto');

// Delete pad sits at the bottom of Pad-wide Settings; on a short viewport
// it starts off-screen and must become reachable by scrolling the popup.
const deletePad = page.locator('#delete-pad');
await expect(deletePad).not.toBeInViewport();
await deletePad.scrollIntoViewIfNeeded();
await expect(deletePad).toBeInViewport();
});

// #7696 follow-up: the Pad-wide font/language nice-select dropdowns sit
// near the bottom of the popup, so opening one triggers the .reverse path
// (open upward). Floating the list with position:fixed must not pick up
// the default `.reverse { bottom: calc(100% + 5px) }` rule, which would
// resolve against the viewport and place the list off-screen.
test('Pad-wide font dropdown opens visibly when popup is scrolled to bottom', async ({page}) => {
await page.setViewportSize({width: 900, height: 500});
await goToNewPad(page);
await showSettings(page);

// Force the font dropdown into the lower portion of the viewport so
// .reverse triggers and the list opens upward.
await page.locator('#settings > .popup-content').evaluate((el) => {
el.scrollTop = el.scrollHeight;
});

const fontDropdown = page.locator('#padsettings-viewfontmenu + .nice-select');
await expect(fontDropdown).toBeInViewport();

await fontDropdown.click();
const list = fontDropdown.locator('.list');
await expect(list).toBeVisible();
await expect(list).toBeInViewport();

// The first option must be reachable so users can actually pick a font.
await fontDropdown.locator('.option').first().click();
await expect(fontDropdown).not.toHaveClass(/open/);
});

// #7592: ticking "Disable chat" must visibly disable the dependent
// "Chat always on screen" / "Show Chat and Users" toggles, not just
// make the underlying inputs non-interactive.
Expand Down
Loading