Browse Source

fix(@vben-core/layout-ui): preserve content sizing across footer modes (#8304)

pull/8310/head
dream-weave 1 month ago
committed by GitHub
parent
commit
bb35b13eb0
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      packages/@core/ui-kit/layout-ui/src/components/layout-footer.vue
  2. 1
      packages/@core/ui-kit/layout-ui/src/vben-layout.vue
  3. 1
      playground/__tests__/e2e/common/layout.ts
  4. 207
      playground/__tests__/e2e/layout-system.spec.ts

1
packages/@core/ui-kit/layout-ui/src/components/layout-footer.vue

@ -38,6 +38,7 @@ const style = computed((): CSSProperties => {
<template>
<footer
:style="style"
data-layout-region="footer"
class="bottom-0 w-full shrink-0 bg-background-deep transition-all duration-200"
>
<slot></slot>

1
packages/@core/ui-kit/layout-ui/src/vben-layout.vue

@ -373,6 +373,7 @@ const layoutScrollStyle = computed((): CSSProperties => {
const contentStyle = computed((): CSSProperties => {
const { footerEnable, footerFixed, footerHeight } = props;
return {
minHeight: footerEnable && !footerFixed ? undefined : 0,
paddingBottom: `${footerEnable && footerFixed ? footerHeight : 0}px`,
};
});

1
playground/__tests__/e2e/common/layout.ts

@ -89,6 +89,7 @@ async function updateLayoutPreferences(page: Page, updates: PreferenceUpdates) {
async function getLayoutMetrics(page: Page): Promise<LayoutMetrics> {
return page.evaluate(() => {
const selectors = {
footer: '[data-layout-region="footer"]',
header: '[data-layout-region="header"]',
layout: '[data-layout-region="layout"]',
main: '[data-layout-region="main"]',

207
playground/__tests__/e2e/layout-system.spec.ts

@ -47,7 +47,7 @@ async function configureDesktopRolePage(page: Page) {
await page.setViewportSize({ height: 900, width: 1440 });
await updateLayoutPreferences(page, {
app: { layout: 'sidebar-nav' },
footer: { enable: false },
footer: { enable: false, fixed: false },
header: { enable: true, hidden: false, mode: 'fixed' },
sidebar: {
collapsed: false,
@ -60,6 +60,14 @@ async function configureDesktopRolePage(page: Page) {
});
}
async function navigateToLayoutPage(page: Page, path: string) {
await page.goto(path);
await page
.locator('[data-layout-region="layout"]')
.waitFor({ state: 'visible' });
await page.waitForFunction(() => Boolean(window.__VBEN_LAYOUT_TEST__));
}
async function getSidebarTransitionDuration(
page: Page,
property: 'clip-path' | 'transform',
@ -146,6 +154,203 @@ test('keeps the layout background continuous while content scrolls', async ({
expect(result.scrollBottom).toBeGreaterThan(0);
});
test('preserves content sizing across footer modes in short viewports', async ({
layoutPage,
}) => {
await configureDesktopRolePage(layoutPage);
await layoutPage.setViewportSize({ height: 600, width: 1365 });
await waitForLayoutSettled(layoutPage);
try {
for (const mode of [
{ enable: false, fixed: false, name: 'disabled' },
{ enable: true, fixed: true, name: 'fixed' },
{ enable: true, fixed: false, name: 'non-fixed' },
] as const) {
await test.step(mode.name, async () => {
if (mode.name === 'non-fixed') {
await navigateToLayoutPage(layoutPage, '/dashboard/analytics');
await layoutPage
.locator('#__vben_main_content .page-route-container > *')
.waitFor({ state: 'visible' });
}
await updateLayoutPreferences(layoutPage, {
footer: { enable: mode.enable, fixed: mode.fixed },
});
const metrics = await getLayoutMetrics(layoutPage);
const sizing = await layoutPage.evaluate(() => {
const footer = document.querySelector(
'[data-layout-region="footer"]',
);
const main = document.querySelector('#__vben_main_content');
const pageContent = document.querySelector(
'[data-layout-region="page-content"]',
);
const scroll = document.querySelector('#__vben_layout_scroll');
const routeContainer = document.querySelector(
'#__vben_main_content .page-route-container',
);
const routeContent = routeContainer?.firstElementChild;
if (
!(main instanceof HTMLElement) ||
!(scroll instanceof HTMLElement)
) {
throw new Error('Layout content regions are missing');
}
const footerRect = footer?.getBoundingClientRect();
return {
footerPosition:
footer instanceof HTMLElement
? getComputedStyle(footer).position
: undefined,
footerTop: footerRect?.top,
layoutClientHeight: scroll.clientHeight,
layoutScrollHeight: scroll.scrollHeight,
mainMinHeight: getComputedStyle(main).minHeight,
mainPaddingBottom: getComputedStyle(main).paddingBottom,
pageContentBottom:
pageContent instanceof HTMLElement
? pageContent.getBoundingClientRect().bottom
: 0,
routeContentBottom: routeContent?.getBoundingClientRect().bottom,
};
});
expectNoViewportOverflow(metrics);
expect(sizing.mainMinHeight).toBe(
mode.name === 'non-fixed' ? 'auto' : '0px',
);
expect(Number.parseFloat(sizing.mainPaddingBottom)).toBe(
mode.name === 'fixed' ? requireRegion(metrics, 'footer').height : 0,
);
if (mode.name === 'disabled') {
expect(metrics.regions.footer).toBeNull();
expect(sizing.layoutScrollHeight).toBeLessThanOrEqual(
sizing.layoutClientHeight + 1,
);
} else if (mode.name === 'fixed') {
const footer = requireRegion(metrics, 'footer');
expect(sizing.footerPosition).toBe('fixed');
expect(Math.abs(footer.bottom - 600)).toBeLessThanOrEqual(1);
expect(sizing.pageContentBottom).toBeLessThanOrEqual(footer.top + 1);
expect(sizing.layoutScrollHeight).toBeLessThanOrEqual(
sizing.layoutClientHeight + 1,
);
} else {
expect(sizing.footerPosition).toBe('static');
expect(sizing.layoutScrollHeight).toBeGreaterThan(
sizing.layoutClientHeight,
);
expect(sizing.footerTop).toBeGreaterThanOrEqual(
(sizing.routeContentBottom ?? 0) - 1,
);
}
await layoutPage.evaluate(() => {
document.querySelector('#__vben_layout_scroll')?.scrollTo({ top: 0 });
});
});
}
} finally {
await updateLayoutPreferences(layoutPage, {
footer: { enable: false, fixed: false },
});
await layoutPage.setViewportSize({ height: 900, width: 1440 });
await navigateToLayoutPage(layoutPage, '/system/role');
}
});
test('keeps appendToMain drawers within the available content height', async ({
layoutPage,
}) => {
await configureDesktopRolePage(layoutPage);
await layoutPage.setViewportSize({ height: 600, width: 1365 });
await navigateToLayoutPage(layoutPage, '/examples/drawer');
try {
const card = layoutPage
.getByText('在内容区域打开', { exact: true })
.locator('xpath=ancestor::*[.//button][1]');
await card
.getByRole('button', { exact: true, name: '右侧打开' })
.dispatchEvent('click');
const main = layoutPage.locator('#__vben_main_content');
const drawer = main.getByRole('dialog');
await expect(drawer).toBeVisible();
await drawer.evaluate((element) => {
for (const animation of element.getAnimations({ subtree: true })) {
if (Number.isFinite(animation.effect?.getComputedTiming().endTime)) {
animation.finish();
}
}
});
await waitForLayoutSettled(layoutPage);
const metrics = await getLayoutMetrics(layoutPage);
const geometry = await drawer.evaluate((element) => {
const main = document.querySelector('#__vben_main_content');
const scroll = document.querySelector('#__vben_layout_scroll');
const drawerBody = [...element.querySelectorAll('div')].find(
(child) => getComputedStyle(child).overflowY === 'auto',
);
if (
!(drawerBody instanceof HTMLElement) ||
!(main instanceof HTMLElement) ||
!(scroll instanceof HTMLElement)
) {
throw new Error('Drawer layout regions are missing');
}
const spacer = document.createElement('div');
spacer.dataset.layoutTestSpacer = 'drawer';
spacer.style.height = '1200px';
drawerBody.append(spacer);
const drawerRect = element.getBoundingClientRect();
const mainRect = main.getBoundingClientRect();
return {
drawerBodyClientHeight: drawerBody.clientHeight,
drawerBodyScrollHeight: drawerBody.scrollHeight,
drawerBottom: drawerRect.bottom,
drawerHeight: drawerRect.height,
drawerTop: drawerRect.top,
layoutClientHeight: scroll.clientHeight,
layoutScrollHeight: scroll.scrollHeight,
mainBottom: mainRect.bottom,
mainHeight: mainRect.height,
mainTop: mainRect.top,
parentId: element.parentElement?.id,
};
});
expectNoViewportOverflow(metrics);
expect(geometry.parentId).toBe('__vben_main_content');
expect(Math.abs(geometry.mainTop - 88)).toBeLessThanOrEqual(1);
expect(Math.abs(geometry.drawerTop - geometry.mainTop)).toBeLessThanOrEqual(
1,
);
expect(
Math.abs(geometry.drawerBottom - geometry.mainBottom),
).toBeLessThanOrEqual(1);
expect(
Math.abs(geometry.drawerHeight - geometry.mainHeight),
).toBeLessThanOrEqual(1);
expect(geometry.drawerBodyScrollHeight).toBeGreaterThan(
geometry.drawerBodyClientHeight,
);
expect(geometry.layoutScrollHeight).toBeLessThanOrEqual(
geometry.layoutClientHeight + 1,
);
} finally {
await layoutPage.setViewportSize({ height: 900, width: 1440 });
await navigateToLayoutPage(layoutPage, '/system/role');
}
});
test('uses the same deliberate duration for both sidebar controls', async ({
layoutPage,
}) => {

Loading…
Cancel
Save