fix(web): keep registration language footer near window bottom

This commit is contained in:
qindong
2026-10-27 16:21:07 +08:00
parent 4830ce1607
commit a9e08890fd
2 changed files with 29 additions and 1 deletions
@@ -82,7 +82,6 @@
.art img { display:block;width:100%;height:100%;border-radius:8px;object-fit:cover;object-position:center; }
.onboardingFooter { position:absolute;left:32px;bottom:24px;display:flex;flex-direction:column;align-items:flex-start;gap:8px;color:#999;font-size:11px;line-height:16px; }
.onboardingFooter > span { opacity:.8; }
@media(max-height:720px) { .onboarding .panel { justify-content:flex-start; }.onboardingFooter { position:static;margin-top:32px; } }
@media(max-width:900px) { .onboarding { grid-template-columns:minmax(0,1fr);padding:0; }.art { display:none; }.onboarding .panel { padding:32px 24px 110px; }.onboardingFooter { left:24px;bottom:32px; } }
.languagePicker { display:inline-flex;align-items:center;width:132px;max-width:100%;padding:0 8px;box-sizing:border-box;border:1px solid #e5e5e5;border-radius:100px;background:#f5f5f5;color:#666; }
@@ -215,3 +214,12 @@
.root:not(.onboarding) .remove:hover:not(:disabled) { background:#f2f2f2;color:#333; }
.root:not(.onboarding) :is(.cancel,.submit) { line-height:19.6px; }
/* Short windows keep the language/copyright footer at the bottom when there
is room, and let it follow overflowing content without covering actions. */
@media(max-height:720px) {
.onboarding .panel { min-height:0;justify-content:flex-start; }
.onboarding .panel:not(:has(.complete)) { padding-bottom:24px; }
.onboarding .panel > header,.onboarding .panel > p,.onboarding form,.onboarding .skip { flex-shrink:0; }
.onboarding .panel > .onboardingFooter { position:static;align-self:flex-start;flex-shrink:0;margin-top:auto;padding-top:32px; }
}
@@ -53,6 +53,18 @@ test('[P1] B01 invitation layout stays usable at Windows scaled and narrow sizes
expect(inputBox!.height).toBe(48);
expect(roleBox!.height).toBe(inputBox!.height);
expect(roleBox!.y).toBe(inputBox!.y);
const expectFooterAtBottom = async () => {
const footer = host.form.locator('section > footer');
const footerBox = await footer.boundingBox();
expect(footerBox).not.toBeNull();
const bottomGap = page.viewportSize()!.height - footerBox!.y - footerBox!.height;
// scrollTop rounds to whole pixels while text geometry can be fractional.
expect(bottomGap).toBeGreaterThanOrEqual(23);
expect(bottomGap).toBeLessThanOrEqual(41);
const skipBox = await host.form.getByRole('button', { name: '暂时跳过' }).boundingBox();
expect(footerBox!.y).toBeGreaterThanOrEqual(skipBox!.y + skipBox!.height);
};
await expectFooterAtBottom();
await captureRegistration(page, 'B01-scaled');
await role.click();
await expect(page.getByRole('option', { name: /管理员/ })).toBeVisible();
@@ -66,6 +78,7 @@ test('[P1] B01 invitation layout stays usable at Windows scaled and narrow sizes
const lastBox = await host.form.locator('input[type=email]').last().boundingBox();
return lastBox!.y + lastBox!.height <= rowsBox!.y + rowsBox!.height;
}).toBe(true);
await expectFooterAtBottom();
await captureRegistration(page, 'B01-scaled-rows');
await page.setViewportSize({ width: 390, height: 844 });
await expect(host.form.locator('img')).toBeHidden();
@@ -73,6 +86,13 @@ test('[P1] B01 invitation layout stays usable at Windows scaled and narrow sizes
await host.form.getByRole('button', { name: '暂时跳过' }).scrollIntoViewIfNeeded();
await expect(host.form.getByRole('button', { name: '暂时跳过' })).toBeInViewport();
await captureRegistration(page, 'B01-scaled-narrow');
await page.setViewportSize({ width: 390, height: 600 });
await expect(host.form.locator('section')).toHaveCSS('padding-bottom', '24px');
await host.form.locator('section > footer').scrollIntoViewIfNeeded();
await host.form.locator('section').evaluate(panel => { panel.scrollTop = panel.scrollHeight; });
await expectFooterAtBottom();
await expect(host.form.locator('section > footer select')).toBeInViewport();
await captureRegistration(page, 'B01-short-narrow-footer');
});
test('[P1] B04 multiple rows retain independent roles and can be removed', async ({ page }) => {