mirror of
https://github.com/zoriya/v10.git
synced 2026-08-14 18:04:49 +00:00
perf(core): batch menu viewport measurements (#1823)
This commit is contained in:
@@ -18,7 +18,7 @@ export interface MenuRootViewAttrs {
|
||||
'data-menu-view': '';
|
||||
}
|
||||
|
||||
interface MenuViewSize {
|
||||
interface ViewSize {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
@@ -29,29 +29,29 @@ interface InlineStyleSnapshotEntry {
|
||||
priority: string;
|
||||
}
|
||||
|
||||
interface PendingMenuViewTransition {
|
||||
interface PendingViewTransition {
|
||||
entering: HTMLElement;
|
||||
availableWidth: number | null;
|
||||
fromSize: MenuViewSize;
|
||||
toSize: MenuViewSize;
|
||||
fromSize: ViewSize;
|
||||
toSize: ViewSize;
|
||||
}
|
||||
|
||||
interface MenuViewportTransitionState {
|
||||
pending: PendingMenuViewTransition | null;
|
||||
interface ViewportTransitionState {
|
||||
pending: PendingViewTransition | null;
|
||||
phaseKeys: WeakMap<HTMLElement, string>;
|
||||
}
|
||||
|
||||
const DEFAULT_MENU_VIEWPORT_MIN_WIDTH = 160;
|
||||
const MENU_VIEW_ATTR = 'data-menu-view';
|
||||
const MENU_VIEW_STATE_ATTR = 'data-menu-view-state';
|
||||
const MENU_VIEW_ACTIVE_STATE = 'active';
|
||||
const MENU_VIEW_INACTIVE_STATE = 'inactive';
|
||||
const MENU_ROOT_VIEW_ATTR = 'data-menu-root-view';
|
||||
const MENU_VIEWPORT_ATTR = 'data-menu-viewport';
|
||||
const MENU_VIEW_LAYOUT_ATTRS = ['data-availability'];
|
||||
const MENU_WIDTH_VAR = '--media-menu-width';
|
||||
const MENU_HEIGHT_VAR = '--media-menu-height';
|
||||
const MENU_VIEW_MEASURE_STYLE_PROPERTIES = [
|
||||
const DEFAULT_MIN_WIDTH = 160;
|
||||
const VIEW_ATTR = 'data-menu-view';
|
||||
const VIEW_STATE_ATTR = 'data-menu-view-state';
|
||||
const VIEW_ACTIVE_STATE = 'active';
|
||||
const VIEW_INACTIVE_STATE = 'inactive';
|
||||
const ROOT_VIEW_ATTR = 'data-menu-root-view';
|
||||
const VIEWPORT_ATTR = 'data-menu-viewport';
|
||||
const VIEW_LAYOUT_ATTRS = ['data-availability'];
|
||||
const WIDTH_VAR = '--media-menu-width';
|
||||
const HEIGHT_VAR = '--media-menu-height';
|
||||
const VIEW_MEASURE_STYLE_PROPERTIES = [
|
||||
'position',
|
||||
'top',
|
||||
'right',
|
||||
@@ -63,7 +63,7 @@ const MENU_VIEW_MEASURE_STYLE_PROPERTIES = [
|
||||
'max-width',
|
||||
];
|
||||
|
||||
const viewportTransitionStates = new WeakMap<HTMLElement, MenuViewportTransitionState>();
|
||||
const viewportTransitionStates = new WeakMap<HTMLElement, ViewportTransitionState>();
|
||||
|
||||
export function getMenuViewportAttrs(): MenuViewportAttrs {
|
||||
return {
|
||||
@@ -78,7 +78,7 @@ export function getMenuRootViewAttrs(): MenuRootViewAttrs {
|
||||
};
|
||||
}
|
||||
|
||||
function getViewportTransitionState(content: HTMLElement): MenuViewportTransitionState {
|
||||
function getViewportTransitionState(content: HTMLElement): ViewportTransitionState {
|
||||
let state = viewportTransitionStates.get(content);
|
||||
|
||||
if (!state) {
|
||||
@@ -95,7 +95,7 @@ function getViewportTransitionState(content: HTMLElement): MenuViewportTransitio
|
||||
export function getMenuViewportElement(content: HTMLElement | null): HTMLElement | null {
|
||||
if (!content) return null;
|
||||
|
||||
return content.querySelector<HTMLElement>(`:scope > [${MENU_VIEWPORT_ATTR}]`) ?? content;
|
||||
return content.querySelector<HTMLElement>(`:scope > [${VIEWPORT_ATTR}]`) ?? content;
|
||||
}
|
||||
|
||||
function getViewportElement(content: HTMLElement, view?: HTMLElement | null): HTMLElement {
|
||||
@@ -113,16 +113,16 @@ function getViewportElement(content: HTMLElement, view?: HTMLElement | null): HT
|
||||
}
|
||||
|
||||
function getRootViewElement(viewport: HTMLElement): HTMLElement | null {
|
||||
return viewport.querySelector<HTMLElement>(`:scope > [${MENU_ROOT_VIEW_ATTR}]`);
|
||||
return viewport.querySelector<HTMLElement>(`:scope > [${ROOT_VIEW_ATTR}]`);
|
||||
}
|
||||
|
||||
function getActiveMenuViewElement(viewport: HTMLElement): HTMLElement | null {
|
||||
function getActiveViewElement(viewport: HTMLElement): HTMLElement | null {
|
||||
return (
|
||||
Array.from(viewport.children).find(
|
||||
(child): child is HTMLElement =>
|
||||
child instanceof HTMLElement &&
|
||||
child.hasAttribute(MENU_VIEW_ATTR) &&
|
||||
!child.hasAttribute(MENU_ROOT_VIEW_ATTR) &&
|
||||
child.hasAttribute(VIEW_ATTR) &&
|
||||
!child.hasAttribute(ROOT_VIEW_ATTR) &&
|
||||
!child.hidden &&
|
||||
!child.hasAttribute(TransitionDataAttrs.transitionEnding)
|
||||
) ?? null
|
||||
@@ -130,7 +130,7 @@ function getActiveMenuViewElement(viewport: HTMLElement): HTMLElement | null {
|
||||
}
|
||||
|
||||
function resolveMinWidth(options: MenuViewportTransitionOptions | undefined): number {
|
||||
return options?.minWidth ?? DEFAULT_MENU_VIEWPORT_MIN_WIDTH;
|
||||
return options?.minWidth ?? DEFAULT_MIN_WIDTH;
|
||||
}
|
||||
|
||||
function resolveAvailableWidth(
|
||||
@@ -147,7 +147,7 @@ function resolveAvailableWidth(
|
||||
}
|
||||
|
||||
function snapshotInlineStyle(element: HTMLElement): InlineStyleSnapshotEntry[] {
|
||||
return MENU_VIEW_MEASURE_STYLE_PROPERTIES.map((property) => ({
|
||||
return VIEW_MEASURE_STYLE_PROPERTIES.map((property) => ({
|
||||
property,
|
||||
value: element.style.getPropertyValue(property),
|
||||
priority: element.style.getPropertyPriority(property),
|
||||
@@ -164,89 +164,132 @@ function restoreInlineStyle(element: HTMLElement, snapshot: InlineStyleSnapshotE
|
||||
}
|
||||
}
|
||||
|
||||
function measureMenuView(
|
||||
function measureViews(
|
||||
content: HTMLElement,
|
||||
views: readonly [HTMLElement],
|
||||
minWidth: number,
|
||||
options?: MenuViewportTransitionOptions
|
||||
): [ViewSize];
|
||||
function measureViews(
|
||||
content: HTMLElement,
|
||||
views: readonly [HTMLElement, HTMLElement],
|
||||
minWidth: number,
|
||||
options?: MenuViewportTransitionOptions
|
||||
): [ViewSize, ViewSize];
|
||||
function measureViews(
|
||||
content: HTMLElement,
|
||||
views: readonly HTMLElement[],
|
||||
minWidth: number,
|
||||
options?: MenuViewportTransitionOptions
|
||||
): ViewSize[] {
|
||||
const snapshots = views.map((view) => ({
|
||||
view,
|
||||
snapshot: snapshotInlineStyle(view),
|
||||
}));
|
||||
const availableWidth = resolveAvailableWidth(content, options);
|
||||
|
||||
try {
|
||||
for (const { view } of snapshots) {
|
||||
view.style.setProperty('position', 'absolute');
|
||||
view.style.setProperty('top', '0px');
|
||||
view.style.setProperty('right', 'auto');
|
||||
view.style.setProperty('bottom', 'auto');
|
||||
view.style.setProperty('left', '0px');
|
||||
view.style.setProperty('width', 'max-content');
|
||||
view.style.setProperty('height', 'auto');
|
||||
view.style.setProperty('min-width', `${minWidth}px`);
|
||||
view.style.setProperty('max-width', 'none');
|
||||
}
|
||||
|
||||
const sizes = snapshots.map(({ view }) => {
|
||||
const rect = view.getBoundingClientRect();
|
||||
const naturalWidth = Math.ceil(Math.max(minWidth, rect.width, view.scrollWidth));
|
||||
const width = Math.ceil(
|
||||
availableWidth ? Math.max(minWidth, Math.min(naturalWidth, availableWidth)) : naturalWidth
|
||||
);
|
||||
|
||||
return { view, rect, naturalWidth, width };
|
||||
});
|
||||
const constrained = sizes.filter((size) => size.width !== size.naturalWidth);
|
||||
|
||||
for (const { view, width } of constrained) {
|
||||
view.style.setProperty('width', `${width}px`);
|
||||
view.style.setProperty('max-width', `${width}px`);
|
||||
}
|
||||
|
||||
for (const size of constrained) {
|
||||
size.rect = size.view.getBoundingClientRect();
|
||||
}
|
||||
|
||||
return sizes.map((size) => ({
|
||||
width: size.width,
|
||||
height: Math.ceil(Math.max(size.rect.height, size.view.scrollHeight)),
|
||||
}));
|
||||
} finally {
|
||||
for (const { view, snapshot } of snapshots) restoreInlineStyle(view, snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
function measureView(
|
||||
content: HTMLElement,
|
||||
view: HTMLElement,
|
||||
minWidth: number,
|
||||
options?: MenuViewportTransitionOptions
|
||||
): MenuViewSize {
|
||||
const snapshot = snapshotInlineStyle(view);
|
||||
const availableWidth = resolveAvailableWidth(content, options);
|
||||
|
||||
try {
|
||||
view.style.setProperty('position', 'absolute');
|
||||
view.style.setProperty('top', '0px');
|
||||
view.style.setProperty('right', 'auto');
|
||||
view.style.setProperty('bottom', 'auto');
|
||||
view.style.setProperty('left', '0px');
|
||||
view.style.setProperty('width', 'max-content');
|
||||
view.style.setProperty('height', 'auto');
|
||||
view.style.setProperty('min-width', `${minWidth}px`);
|
||||
view.style.setProperty('max-width', 'none');
|
||||
forceLayout(view);
|
||||
|
||||
let rect = view.getBoundingClientRect();
|
||||
const naturalWidth = Math.ceil(Math.max(minWidth, rect.width, view.scrollWidth));
|
||||
const width = Math.ceil(availableWidth ? Math.max(minWidth, Math.min(naturalWidth, availableWidth)) : naturalWidth);
|
||||
|
||||
if (width !== naturalWidth) {
|
||||
view.style.setProperty('width', `${width}px`);
|
||||
view.style.setProperty('max-width', `${width}px`);
|
||||
forceLayout(view);
|
||||
rect = view.getBoundingClientRect();
|
||||
}
|
||||
|
||||
return {
|
||||
width,
|
||||
height: Math.ceil(Math.max(rect.height, view.scrollHeight)),
|
||||
};
|
||||
} finally {
|
||||
restoreInlineStyle(view, snapshot);
|
||||
forceLayout(view);
|
||||
}
|
||||
): ViewSize {
|
||||
return measureViews(content, [view], minWidth, options)[0];
|
||||
}
|
||||
|
||||
function setViewportSize(content: HTMLElement, size: MenuViewSize): void {
|
||||
content.style.setProperty(MENU_WIDTH_VAR, `${size.width}px`);
|
||||
content.style.setProperty(MENU_HEIGHT_VAR, `${size.height}px`);
|
||||
function setViewportSize(content: HTMLElement, size: ViewSize): void {
|
||||
content.style.setProperty(WIDTH_VAR, `${size.width}px`);
|
||||
content.style.setProperty(HEIGHT_VAR, `${size.height}px`);
|
||||
}
|
||||
|
||||
function setMenuViewState(
|
||||
view: HTMLElement,
|
||||
state: typeof MENU_VIEW_ACTIVE_STATE | typeof MENU_VIEW_INACTIVE_STATE
|
||||
): void {
|
||||
view.setAttribute(MENU_VIEW_STATE_ATTR, state);
|
||||
function setViewState(view: HTMLElement, state: typeof VIEW_ACTIVE_STATE | typeof VIEW_INACTIVE_STATE): void {
|
||||
view.setAttribute(VIEW_STATE_ATTR, state);
|
||||
|
||||
if (state === MENU_VIEW_ACTIVE_STATE) {
|
||||
if (state === VIEW_ACTIVE_STATE) {
|
||||
view.setAttribute('data-open', '');
|
||||
} else {
|
||||
view.removeAttribute('data-open');
|
||||
}
|
||||
}
|
||||
|
||||
function prepareEnteringMenuView(
|
||||
function prepareEnteringView(
|
||||
content: HTMLElement,
|
||||
rootView: HTMLElement,
|
||||
entering: HTMLElement,
|
||||
state: MenuViewportTransitionState,
|
||||
minWidth: number,
|
||||
availableWidth: number | null,
|
||||
options?: MenuViewportTransitionOptions
|
||||
): PendingViewTransition {
|
||||
const [fromSize, toSize] = measureViews(content, [rootView, entering], minWidth, options);
|
||||
|
||||
return { entering, availableWidth, fromSize, toSize };
|
||||
}
|
||||
|
||||
function prepareEnteringTransition(
|
||||
content: HTMLElement,
|
||||
rootView: HTMLElement,
|
||||
entering: HTMLElement,
|
||||
state: ViewportTransitionState,
|
||||
options?: MenuViewportTransitionOptions
|
||||
): void {
|
||||
const minWidth = resolveMinWidth(options);
|
||||
const availableWidth = resolveAvailableWidth(content, options);
|
||||
const fromSize = measureMenuView(content, rootView, minWidth, options);
|
||||
const toSize = measureMenuView(content, entering, minWidth, options);
|
||||
|
||||
state.pending = { entering, availableWidth, fromSize, toSize };
|
||||
setMenuViewState(rootView, MENU_VIEW_ACTIVE_STATE);
|
||||
setViewportSize(content, fromSize);
|
||||
const pending = prepareEnteringView(content, rootView, entering, minWidth, availableWidth, options);
|
||||
|
||||
state.pending = pending;
|
||||
setViewState(rootView, VIEW_ACTIVE_STATE);
|
||||
setViewportSize(content, pending.fromSize);
|
||||
forceLayout(content);
|
||||
}
|
||||
|
||||
function startEnteringMenuView(
|
||||
function startEnteringView(
|
||||
content: HTMLElement,
|
||||
rootView: HTMLElement,
|
||||
entering: HTMLElement,
|
||||
state: MenuViewportTransitionState,
|
||||
state: ViewportTransitionState,
|
||||
options?: MenuViewportTransitionOptions
|
||||
): void {
|
||||
const minWidth = resolveMinWidth(options);
|
||||
@@ -254,39 +297,33 @@ function startEnteringMenuView(
|
||||
const current =
|
||||
state.pending?.entering === entering && state.pending.availableWidth === availableWidth
|
||||
? state.pending
|
||||
: {
|
||||
entering,
|
||||
availableWidth,
|
||||
fromSize: measureMenuView(content, rootView, minWidth, options),
|
||||
toSize: measureMenuView(content, entering, minWidth, options),
|
||||
};
|
||||
: prepareEnteringView(content, rootView, entering, minWidth, availableWidth, options);
|
||||
|
||||
state.pending = null;
|
||||
|
||||
setViewportSize(content, current.fromSize);
|
||||
forceLayout(rootView);
|
||||
setMenuViewState(rootView, MENU_VIEW_INACTIVE_STATE);
|
||||
setViewState(rootView, VIEW_INACTIVE_STATE);
|
||||
forceLayout(rootView);
|
||||
setViewportSize(content, current.toSize);
|
||||
}
|
||||
|
||||
function startExitingMenuView(
|
||||
function startExitingView(
|
||||
content: HTMLElement,
|
||||
rootView: HTMLElement,
|
||||
exiting: HTMLElement,
|
||||
transitionState: MenuViewportTransitionState,
|
||||
transitionState: ViewportTransitionState,
|
||||
options?: MenuViewportTransitionOptions
|
||||
): void {
|
||||
transitionState.pending = null;
|
||||
|
||||
const minWidth = resolveMinWidth(options);
|
||||
const fromSize = measureMenuView(content, exiting, minWidth, options);
|
||||
const toSize = measureMenuView(content, rootView, minWidth, options);
|
||||
const [fromSize, toSize] = measureViews(content, [exiting, rootView], minWidth, options);
|
||||
|
||||
setViewportSize(content, fromSize);
|
||||
setMenuViewState(rootView, MENU_VIEW_INACTIVE_STATE);
|
||||
setViewState(rootView, VIEW_INACTIVE_STATE);
|
||||
forceLayout(rootView);
|
||||
setMenuViewState(rootView, MENU_VIEW_ACTIVE_STATE);
|
||||
setViewState(rootView, VIEW_ACTIVE_STATE);
|
||||
forceLayout(rootView);
|
||||
setViewportSize(content, toSize);
|
||||
}
|
||||
@@ -303,11 +340,11 @@ export function syncMenuViewRoot(
|
||||
|
||||
if (!rootView) return;
|
||||
|
||||
const activeView = getActiveMenuViewElement(viewport);
|
||||
const activeView = getActiveViewElement(viewport);
|
||||
|
||||
if (activeView) {
|
||||
if (rootView.getAttribute(MENU_VIEW_STATE_ATTR) === MENU_VIEW_INACTIVE_STATE) {
|
||||
const size = measureMenuView(content, activeView, resolveMinWidth(options), options);
|
||||
if (rootView.getAttribute(VIEW_STATE_ATTR) === VIEW_INACTIVE_STATE) {
|
||||
const size = measureView(content, activeView, resolveMinWidth(options), options);
|
||||
setViewportSize(content, size);
|
||||
}
|
||||
|
||||
@@ -316,9 +353,9 @@ export function syncMenuViewRoot(
|
||||
|
||||
if (hasActiveChildView) return;
|
||||
|
||||
const size = measureMenuView(content, rootView, resolveMinWidth(options), options);
|
||||
const size = measureView(content, rootView, resolveMinWidth(options), options);
|
||||
|
||||
setMenuViewState(rootView, MENU_VIEW_ACTIVE_STATE);
|
||||
setViewState(rootView, VIEW_ACTIVE_STATE);
|
||||
setViewportSize(content, size);
|
||||
}
|
||||
|
||||
@@ -353,7 +390,7 @@ export function observeMenuViewContent(content: HTMLElement, onChange: () => voi
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeOldValue: true,
|
||||
attributeFilter: MENU_VIEW_LAYOUT_ATTRS,
|
||||
attributeFilter: VIEW_LAYOUT_ATTRS,
|
||||
});
|
||||
|
||||
return () => {
|
||||
@@ -377,29 +414,31 @@ export function syncMenuViewTransition(
|
||||
|
||||
const state = getViewportTransitionState(content);
|
||||
const phaseKey = `${viewState.phase}:${viewState.direction}`;
|
||||
const previousPhaseKey = state.phaseKeys.get(view);
|
||||
|
||||
const shouldResyncActiveView =
|
||||
viewState.phase === 'active' && rootView.getAttribute(MENU_VIEW_STATE_ATTR) !== MENU_VIEW_INACTIVE_STATE;
|
||||
viewState.phase === 'active' && rootView.getAttribute(VIEW_STATE_ATTR) !== VIEW_INACTIVE_STATE;
|
||||
|
||||
if (state.phaseKeys.get(view) === phaseKey && !shouldResyncActiveView) return;
|
||||
if (previousPhaseKey === phaseKey && !shouldResyncActiveView) return;
|
||||
|
||||
state.phaseKeys.set(view, phaseKey);
|
||||
|
||||
if (viewState.phase === 'hidden') {
|
||||
state.phaseKeys.delete(view);
|
||||
syncMenuViewRoot(content, getActiveMenuViewElement(viewport) !== null, options);
|
||||
if (!previousPhaseKey || previousPhaseKey.startsWith('hidden:')) return;
|
||||
|
||||
syncMenuViewRoot(content, getActiveViewElement(viewport) !== null, options);
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewState.phase === 'entering') {
|
||||
prepareEnteringMenuView(content, rootView, view, state, options);
|
||||
prepareEnteringTransition(content, rootView, view, state, options);
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewState.phase === 'active') {
|
||||
startEnteringMenuView(content, rootView, view, state, options);
|
||||
startEnteringView(content, rootView, view, state, options);
|
||||
return;
|
||||
}
|
||||
|
||||
startExitingMenuView(content, rootView, view, state, options);
|
||||
startExitingView(content, rootView, view, state, options);
|
||||
}
|
||||
|
||||
@@ -221,6 +221,29 @@ describe('menu-viewport-transition', () => {
|
||||
expect(content.style.getPropertyValue('--media-menu-width')).toBe('160px');
|
||||
});
|
||||
|
||||
it('does not measure the root view for initially hidden child views', () => {
|
||||
const content = addElement();
|
||||
const rootView = document.createElement('div');
|
||||
const menuViews = Array.from({ length: 13 }, () => document.createElement('div'));
|
||||
|
||||
applyAttrs(rootView, getMenuRootViewAttrs());
|
||||
rootView.getBoundingClientRect = vi.fn(() => createRect(160, 100));
|
||||
for (const menuView of menuViews) menuView.setAttribute('data-menu-view', '');
|
||||
content.append(rootView, ...menuViews);
|
||||
|
||||
for (const menuView of menuViews) {
|
||||
syncMenuViewTransition(content, menuView, {
|
||||
phase: 'hidden',
|
||||
direction: 'forward',
|
||||
triggerId: null,
|
||||
});
|
||||
}
|
||||
|
||||
expect(rootView.getBoundingClientRect).not.toHaveBeenCalled();
|
||||
expect(content.style.getPropertyValue('--media-menu-width')).toBe('');
|
||||
expect(content.style.getPropertyValue('--media-menu-height')).toBe('');
|
||||
});
|
||||
|
||||
it('measures the root view height at the available menu width', () => {
|
||||
const content = addElement();
|
||||
const rootView = document.createElement('div');
|
||||
@@ -242,6 +265,61 @@ describe('menu-viewport-transition', () => {
|
||||
|
||||
expect(content.style.getPropertyValue('--media-menu-width')).toBe('180px');
|
||||
expect(content.style.getPropertyValue('--media-menu-height')).toBe('128px');
|
||||
expect(rootView.getBoundingClientRect).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('avoids duplicate reads when measuring a natural menu view size', () => {
|
||||
const content = addElement();
|
||||
const rootView = document.createElement('div');
|
||||
|
||||
applyAttrs(rootView, getMenuRootViewAttrs());
|
||||
content.append(rootView);
|
||||
|
||||
mockMenuViewSize(rootView, {
|
||||
currentWidth: 160,
|
||||
currentHeight: 80,
|
||||
naturalWidth: 160,
|
||||
naturalHeight: 80,
|
||||
});
|
||||
|
||||
syncMenuViewRoot(content, false);
|
||||
|
||||
expect(rootView.getBoundingClientRect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('batches entering menu view measurements', () => {
|
||||
const content = addElement();
|
||||
const rootView = document.createElement('div');
|
||||
const menuView = document.createElement('div');
|
||||
|
||||
applyAttrs(rootView, getMenuRootViewAttrs());
|
||||
menuView.setAttribute('data-menu-view', '');
|
||||
content.style.setProperty('--media-popover-available-width', '180px');
|
||||
content.append(rootView, menuView);
|
||||
|
||||
mockMenuViewSize(rootView, {
|
||||
currentWidth: 160,
|
||||
currentHeight: 100,
|
||||
naturalWidth: 160,
|
||||
naturalHeight: 100,
|
||||
});
|
||||
mockMenuViewSize(menuView, {
|
||||
currentWidth: 160,
|
||||
currentHeight: 100,
|
||||
naturalWidth: 260,
|
||||
naturalHeight: 100,
|
||||
constrainedWidth: 180,
|
||||
constrainedHeight: 148,
|
||||
});
|
||||
|
||||
syncMenuViewTransition(content, menuView, {
|
||||
phase: 'entering',
|
||||
direction: 'forward',
|
||||
triggerId: 'trigger-1',
|
||||
});
|
||||
|
||||
expect(rootView.getBoundingClientRect).toHaveBeenCalledTimes(1);
|
||||
expect(menuView.getBoundingClientRect).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('measures an entering submenu height at the available menu width', () => {
|
||||
@@ -439,6 +517,48 @@ describe('menu-viewport-transition', () => {
|
||||
expect(content.style.getPropertyValue('--media-menu-height')).toBe('109px');
|
||||
expect(rootView.style.getPropertyValue('width')).toBe('');
|
||||
expect(rootView.style.getPropertyValue('height')).toBe('');
|
||||
expect(menuView.getBoundingClientRect).toHaveBeenCalledTimes(1);
|
||||
expect(rootView.getBoundingClientRect).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('resyncs the root view when a visible child view becomes hidden', () => {
|
||||
const content = addElement();
|
||||
const rootView = document.createElement('div');
|
||||
const menuView = document.createElement('div');
|
||||
|
||||
applyAttrs(rootView, getMenuRootViewAttrs());
|
||||
menuView.setAttribute('data-menu-view', '');
|
||||
content.append(rootView, menuView);
|
||||
|
||||
mockMenuViewSize(rootView, {
|
||||
currentWidth: 220,
|
||||
currentHeight: 170,
|
||||
naturalWidth: 160,
|
||||
naturalHeight: 100,
|
||||
});
|
||||
mockMenuViewSize(menuView, {
|
||||
currentWidth: 220,
|
||||
currentHeight: 170,
|
||||
naturalWidth: 220,
|
||||
naturalHeight: 170,
|
||||
});
|
||||
|
||||
syncMenuViewTransition(content, menuView, {
|
||||
phase: 'active',
|
||||
direction: 'forward',
|
||||
triggerId: 'trigger-1',
|
||||
});
|
||||
menuView.hidden = true;
|
||||
|
||||
syncMenuViewTransition(content, menuView, {
|
||||
phase: 'hidden',
|
||||
direction: 'back',
|
||||
triggerId: 'trigger-1',
|
||||
});
|
||||
|
||||
expect(rootView.getAttribute('data-menu-view-state')).toBe('active');
|
||||
expect(content.style.getPropertyValue('--media-menu-width')).toBe('160px');
|
||||
expect(content.style.getPropertyValue('--media-menu-height')).toBe('100px');
|
||||
});
|
||||
|
||||
it('does not restore the root view when a hidden child sibling still has an active view', () => {
|
||||
|
||||
Reference in New Issue
Block a user