Browse Source

fix(@vben-core/layout-ui): add overlay viewport height fallback

pull/8252/head
Dream 1 week ago
parent
commit
522a83b2fb
  1. 6
      internal/tailwind-config/src/theme.css
  2. 2
      packages/@core/base/shared/src/constants/globals.ts
  3. 139
      packages/@core/composables/src/__tests__/use-layout-viewport-height.test.ts
  4. 1
      packages/@core/composables/src/index.ts
  5. 71
      packages/@core/composables/src/use-layout-viewport-height.ts
  6. 3
      packages/@core/ui-kit/layout-ui/src/components/layout-content.vue
  7. 2
      packages/@core/ui-kit/layout-ui/src/vben-layout.vue

6
internal/tailwind-config/src/theme.css

@ -310,6 +310,8 @@
}
html {
--vben-viewport-height: 100vh;
@apply bg-background font-sans text-foreground;
scroll-behavior: smooth;
@ -320,6 +322,10 @@
text-rendering: optimizelegibility;
text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
@supports (height: 1dvh) {
--vben-viewport-height: 100dvh;
}
}
#app,

2
packages/@core/base/shared/src/constants/globals.ts

@ -6,6 +6,8 @@ export const CSS_VARIABLE_LAYOUT_CONTENT_WIDTH = `--vben-content-width`;
export const CSS_VARIABLE_LAYOUT_HEADER_HEIGHT = `--vben-header-height`;
/** layout footer 组件的高度 */
export const CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT = `--vben-footer-height`;
/** layout overlay 使用的视口高度,CSS 按 100vh → 100dvh 降级 */
export const CSS_VARIABLE_LAYOUT_VIEWPORT_HEIGHT = `--vben-viewport-height`;
/** 内容区域的组件ID */
export const ELEMENT_ID_MAIN_CONTENT = `__vben_main_content`;

139
packages/@core/composables/src/__tests__/use-layout-viewport-height.test.ts

@ -0,0 +1,139 @@
import type { App } from 'vue';
import { createApp } from 'vue';
import { CSS_VARIABLE_LAYOUT_VIEWPORT_HEIGHT } from '@vben-core/shared/constants';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { useLayoutViewportHeight } from '../use-layout-viewport-height';
let activeApp: App | undefined;
function stubDvhSupport(supported: boolean) {
vi.stubGlobal('CSS', {
supports: vi.fn((property: string, value: string) => {
return supported && property === 'height' && value === '1dvh';
}),
});
}
function mountViewportHeight() {
const host = document.createElement('div');
document.body.append(host);
activeApp = createApp({
setup() {
useLayoutViewportHeight();
return () => null;
},
});
activeApp.mount(host);
}
function getViewportHeightVar() {
return document.documentElement.style.getPropertyValue(
CSS_VARIABLE_LAYOUT_VIEWPORT_HEIGHT,
);
}
async function flushAnimationFrame() {
await new Promise<void>((resolve) => {
requestAnimationFrame(() => resolve());
});
}
afterEach(() => {
activeApp?.unmount();
activeApp = undefined;
document.body.innerHTML = '';
document.documentElement.style.removeProperty(
CSS_VARIABLE_LAYOUT_VIEWPORT_HEIGHT,
);
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
describe('useLayoutViewportHeight', () => {
it('does not write the CSS variable when dvh is supported', () => {
const resizeObserver = vi.fn();
const addEventListener = vi.spyOn(window, 'addEventListener');
vi.stubGlobal('ResizeObserver', resizeObserver);
stubDvhSupport(true);
mountViewportHeight();
expect(getViewportHeightVar()).toBe('');
expect(resizeObserver).not.toHaveBeenCalled();
expect(addEventListener).not.toHaveBeenCalledWith(
'resize',
expect.any(Function),
);
});
it('writes innerHeight when dvh is unsupported', () => {
const resizeObserver = vi.fn();
vi.stubGlobal('ResizeObserver', resizeObserver);
stubDvhSupport(false);
vi.stubGlobal('visualViewport', null);
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800);
mountViewportHeight();
expect(getViewportHeightVar()).toBe('800px');
expect(resizeObserver).not.toHaveBeenCalled();
});
it('prefers visualViewport.height over innerHeight', () => {
stubDvhSupport(false);
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800);
const visualViewport = new EventTarget() as VisualViewport;
Object.defineProperty(visualViewport, 'height', {
configurable: true,
value: 640,
writable: true,
});
vi.stubGlobal('visualViewport', visualViewport);
mountViewportHeight();
expect(getViewportHeightVar()).toBe('640px');
});
it('updates the CSS variable on visualViewport resize and stops after unmount', async () => {
stubDvhSupport(false);
const visualViewport = new EventTarget() as VisualViewport;
Object.defineProperty(visualViewport, 'height', {
configurable: true,
value: 640,
writable: true,
});
vi.stubGlobal('visualViewport', visualViewport);
mountViewportHeight();
expect(getViewportHeightVar()).toBe('640px');
Object.defineProperty(visualViewport, 'height', {
configurable: true,
value: 520,
writable: true,
});
visualViewport.dispatchEvent(new Event('resize'));
await flushAnimationFrame();
expect(getViewportHeightVar()).toBe('520px');
activeApp?.unmount();
activeApp = undefined;
Object.defineProperty(visualViewport, 'height', {
configurable: true,
value: 400,
writable: true,
});
visualViewport.dispatchEvent(new Event('resize'));
await flushAnimationFrame();
expect(getViewportHeightVar()).toBe('520px');
});
});

1
packages/@core/composables/src/index.ts

@ -1,5 +1,6 @@
export * from './use-is-mobile';
export * from './use-layout-style';
export * from './use-layout-viewport-height';
export * from './use-namespace';
export * from './use-priority-value';
export * from './use-scroll-lock';

71
packages/@core/composables/src/use-layout-viewport-height.ts

@ -0,0 +1,71 @@
import { onMounted, onUnmounted } from 'vue';
import { CSS_VARIABLE_LAYOUT_VIEWPORT_HEIGHT } from '@vben-core/shared/constants';
import { useCssVar, useEventListener } from '@vueuse/core';
function supportsDynamicViewportHeight() {
return (
globalThis.CSS !== undefined &&
typeof globalThis.CSS.supports === 'function' &&
globalThis.CSS.supports('height', '1dvh')
);
}
function readViewportHeight() {
return Math.round(window.visualViewport?.height ?? window.innerHeight);
}
/**
* dvh --vben-viewport-height
* dvh CSS 100vh 100dvh useCssVar px
*/
export function useLayoutViewportHeight() {
if (typeof window === 'undefined' || supportsDynamicViewportHeight()) {
return;
}
const viewportHeight = useCssVar(
CSS_VARIABLE_LAYOUT_VIEWPORT_HEIGHT,
document.documentElement,
{ observe: false },
);
let frameId = 0;
function applyViewportHeight() {
viewportHeight.value = `${readViewportHeight()}px`;
}
function scheduleApplyViewportHeight() {
if (frameId) {
return;
}
frameId = window.requestAnimationFrame(() => {
frameId = 0;
applyViewportHeight();
});
}
applyViewportHeight();
onMounted(applyViewportHeight);
useEventListener(window, 'resize', scheduleApplyViewportHeight);
if (window.visualViewport) {
useEventListener(
window.visualViewport,
'resize',
scheduleApplyViewportHeight,
);
}
onUnmounted(() => {
if (!frameId) {
return;
}
window.cancelAnimationFrame(frameId);
frameId = 0;
});
}

3
packages/@core/ui-kit/layout-ui/src/components/layout-content.vue

@ -25,7 +25,7 @@ const props = withDefaults(defineProps<Props>(), {});
const overlayViewportStyle: CSSProperties = {
height:
'calc(100dvh - var(--vben-header-height, 0px) - var(--vben-footer-height, 0px))',
'calc(var(--vben-viewport-height) - var(--vben-header-height, 0px) - var(--vben-footer-height, 0px))',
};
const style = computed((): CSSProperties => {
@ -65,6 +65,7 @@ const style = computed((): CSSProperties => {
>
<div
:style="overlayViewportStyle"
data-layout-region="overlay-viewport"
class="pointer-events-none relative min-h-0 w-full"
>
<slot name="overlay"></slot>

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

@ -9,6 +9,7 @@ import {
SCROLL_FIXED_CLASS,
useLayoutFooterStyle,
useLayoutHeaderStyle,
useLayoutViewportHeight,
} from '@vben-core/composables';
import { IconifyIcon } from '@vben-core/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui';
@ -106,6 +107,7 @@ const {
onScroll: handleLayoutScroll,
});
useLayoutViewportHeight();
const { setLayoutHeaderHeight } = useLayoutHeaderStyle();
const { setLayoutFooterHeight } = useLayoutFooterStyle();

Loading…
Cancel
Save