diff --git a/lib/components/group/Group.tsx b/lib/components/group/Group.tsx index a3693d3bf..b377d5b8e 100644 --- a/lib/components/group/Group.tsx +++ b/lib/components/group/Group.tsx @@ -207,11 +207,12 @@ export function Group({ } const group = getRegisteredGroup(id); - const groupState = getMountedGroupState(id); - if (group && groupState) { - updateMountedGroup(group, { - ...groupState, - derivedPanelConstraints: calculatePanelConstraints(group) + if (group) { + updateMountedGroup({ + group, + partial: { + derivedPanelConstraints: calculatePanelConstraints(group) + } }); } }, diff --git a/lib/global/event-handlers/onDocumentPointerMove.ts b/lib/global/event-handlers/onDocumentPointerMove.ts index 93d24c4c3..62f7bdfc1 100644 --- a/lib/global/event-handlers/onDocumentPointerMove.ts +++ b/lib/global/event-handlers/onDocumentPointerMove.ts @@ -43,8 +43,12 @@ export function onDocumentPointerMove(event: PointerEvent) { return; } const groupState = getMountedGroupState(hitRegion.group.id, true); - updateMountedGroup(hitRegion.group, groupState, { - isUserInteraction: true + updateMountedGroup({ + group: hitRegion.group, + meta: { + isUserInteraction: true + }, + state: groupState }); }); diff --git a/lib/global/event-handlers/onDocumentVisibilityChange.ts b/lib/global/event-handlers/onDocumentVisibilityChange.ts new file mode 100644 index 000000000..03409abb7 --- /dev/null +++ b/lib/global/event-handlers/onDocumentVisibilityChange.ts @@ -0,0 +1,23 @@ +import { getMountedGroups } from "../mutable-state/groups"; +import { resizeObserverCallback } from "./resizeObserverCallback"; + +export function onDocumentVisibilityChange(event: Event) { + const ownerDocument = event.currentTarget as Document; + + switch (document.visibilityState) { + case "visible": { + const mountedGroups = getMountedGroups(); + + for (const [group, state] of mountedGroups) { + if (group.element.ownerDocument === ownerDocument) { + for (const target of state.pendingResizeEventsForElements) { + resizeObserverCallback({ + group, + target + }); + } + } + } + } + } +} diff --git a/lib/global/event-handlers/resizeObserverCallback.ts b/lib/global/event-handlers/resizeObserverCallback.ts new file mode 100644 index 000000000..be43a350e --- /dev/null +++ b/lib/global/event-handlers/resizeObserverCallback.ts @@ -0,0 +1,101 @@ +import type { RegisteredGroup } from "../../components/group/types"; +import { calculateAvailableGroupSize } from "../dom/calculateAvailableGroupSize"; +import { calculatePanelConstraints } from "../dom/calculatePanelConstraints"; +import { + getMountedGroupState, + updateMountedGroup +} from "../mutable-state/groups"; +import { calculateDefaultLayout } from "../utils/calculateDefaultLayout"; +import { layoutsEqual } from "../utils/layoutsEqual"; +import { notifyPanelOnResize } from "../utils/notifyPanelOnResize"; +import { objectsEqual } from "../utils/objectsEqual"; +import { preserveFixedPanelSizes } from "../utils/preserveFixedPanelSizes"; +import { validatePanelGroupLayout } from "../utils/validatePanelGroupLayout"; + +export function resizeObserverCallback({ + group, + target +}: { + group: RegisteredGroup; + target: Element; +}) { + const ownerDocument = target.ownerDocument; + + const groupState = getMountedGroupState(group.id); + if (!groupState) { + return; + } + + const pendingResizeEventsForElements = new Set( + groupState.pendingResizeEventsForElements + ); + + switch (ownerDocument.visibilityState) { + case "hidden": { + // Ignore ResizeObserver updates when hidden; see #731 + pendingResizeEventsForElements.add(target); + + updateMountedGroup({ + group, + partial: { pendingResizeEventsForElements } + }); + return; + } + case "visible": { + pendingResizeEventsForElements.delete(target); + } + } + + if (target === group.element) { + const groupSize = calculateAvailableGroupSize({ group }); + if (groupSize === 0) { + // Can't calculate anything meaningful if the group has a width/height of 0 + // (This could indicate that it's within a hidden subtree) + return; + } + + // Update non-percentage based constraints + const nextDerivedPanelConstraints = calculatePanelConstraints(group); + + // Revalidate layout in case constraints have changed or group size changed + const prevLayout = groupState.defaultLayoutDeferred + ? calculateDefaultLayout(nextDerivedPanelConstraints) + : groupState.layout; + const unsafeLayout = preserveFixedPanelSizes({ + group, + nextGroupSize: groupSize, + prevGroupSize: groupState.groupSize, + prevLayout + }); + const nextLayout = validatePanelGroupLayout({ + layout: unsafeLayout, + panelConstraints: nextDerivedPanelConstraints + }); + + if ( + !groupState.defaultLayoutDeferred && + layoutsEqual(groupState.layout, nextLayout) && + objectsEqual( + groupState.derivedPanelConstraints, + nextDerivedPanelConstraints + ) && + groupState.groupSize === groupSize + ) { + return; + } + + updateMountedGroup({ + group, + state: { + defaultLayoutDeferred: false, + derivedPanelConstraints: nextDerivedPanelConstraints, + groupSize, + layout: nextLayout, + pendingResizeEventsForElements, + separatorToPanels: groupState.separatorToPanels + } + }); + } else { + notifyPanelOnResize(group, target as HTMLElement); + } +} diff --git a/lib/global/mountGroup.ts b/lib/global/mountGroup.ts index cf7c53e4b..1c5eee13b 100644 --- a/lib/global/mountGroup.ts +++ b/lib/global/mountGroup.ts @@ -11,17 +11,11 @@ import { onDocumentPointerLeave } from "./event-handlers/onDocumentPointerLeave" import { onDocumentPointerMove } from "./event-handlers/onDocumentPointerMove"; import { onDocumentPointerOut } from "./event-handlers/onDocumentPointerOut"; import { onDocumentPointerUp } from "./event-handlers/onDocumentPointerUp"; -import { - deleteMutableGroup, - getMountedGroupState, - updateMountedGroup -} from "./mutable-state/groups"; +import { onDocumentVisibilityChange } from "./event-handlers/onDocumentVisibilityChange"; +import { resizeObserverCallback } from "./event-handlers/resizeObserverCallback"; +import { deleteMutableGroup, updateMountedGroup } from "./mutable-state/groups"; import type { SeparatorToPanelsMap } from "./mutable-state/types"; import { calculateDefaultLayout } from "./utils/calculateDefaultLayout"; -import { layoutsEqual } from "./utils/layoutsEqual"; -import { notifyPanelOnResize } from "./utils/notifyPanelOnResize"; -import { objectsEqual } from "./utils/objectsEqual"; -import { preserveFixedPanelSizes } from "./utils/preserveFixedPanelSizes"; import { validateLayoutKeys } from "./utils/validateLayoutKeys"; import { validatePanelGroupLayout } from "./utils/validatePanelGroupLayout"; @@ -43,64 +37,17 @@ export function mountGroup(group: RegisteredGroup) { // Add Panels with onResize callbacks to ResizeObserver // Add Group to ResizeObserver also in order to sync % based constraints const resizeObserver = new ResizeObserver((entries) => { + if (!isMounted) { + return; + } + for (const entry of entries) { - const { borderBoxSize, target } = entry; - if (target === group.element) { - if (isMounted) { - const groupSize = calculateAvailableGroupSize({ group }); - if (groupSize === 0) { - // Can't calculate anything meaningful if the group has a width/height of 0 - // (This could indicate that it's within a hidden subtree) - return; - } - - const groupState = getMountedGroupState(group.id); - if (!groupState) { - // Not mounted yet - return; - } - - // Update non-percentage based constraints - const nextDerivedPanelConstraints = calculatePanelConstraints(group); - - // Revalidate layout in case constraints have changed or group size changed - const prevLayout = groupState.defaultLayoutDeferred - ? calculateDefaultLayout(nextDerivedPanelConstraints) - : groupState.layout; - const unsafeLayout = preserveFixedPanelSizes({ - group, - nextGroupSize: groupSize, - prevGroupSize: groupState.groupSize, - prevLayout - }); - const nextLayout = validatePanelGroupLayout({ - layout: unsafeLayout, - panelConstraints: nextDerivedPanelConstraints - }); - - if ( - !groupState.defaultLayoutDeferred && - layoutsEqual(groupState.layout, nextLayout) && - objectsEqual( - groupState.derivedPanelConstraints, - nextDerivedPanelConstraints - ) && - groupState.groupSize === groupSize - ) { - return; - } - - updateMountedGroup(group, { - defaultLayoutDeferred: false, - derivedPanelConstraints: nextDerivedPanelConstraints, - groupSize, - layout: nextLayout, - separatorToPanels: groupState.separatorToPanels - }); - } - } else { - notifyPanelOnResize(group, target as HTMLElement, borderBoxSize); - } + const { target } = entry; + + resizeObserverCallback({ + group, + target + }); } }); @@ -159,12 +106,16 @@ export function mountGroup(group: RegisteredGroup) { } }); - updateMountedGroup(group, { - defaultLayoutDeferred: groupSize === 0, - derivedPanelConstraints, - groupSize, - layout: defaultLayoutSafe, - separatorToPanels + updateMountedGroup({ + group, + partial: { + defaultLayoutDeferred: groupSize === 0, + derivedPanelConstraints, + groupSize, + pendingResizeEventsForElements: new Set(), + layout: defaultLayoutSafe, + separatorToPanels + } }); group.separators.forEach((separator) => { @@ -187,6 +138,10 @@ export function mountGroup(group: RegisteredGroup) { ownerDocument.addEventListener("pointermove", onDocumentPointerMove); ownerDocument.addEventListener("pointerout", onDocumentPointerOut); ownerDocument.addEventListener("pointerup", onDocumentPointerUp, true); + ownerDocument.addEventListener( + "visibilitychange", + onDocumentVisibilityChange + ); } return function unmountGroup() { @@ -224,6 +179,10 @@ export function mountGroup(group: RegisteredGroup) { ownerDocument.removeEventListener("pointermove", onDocumentPointerMove); ownerDocument.removeEventListener("pointerout", onDocumentPointerOut); ownerDocument.removeEventListener("pointerup", onDocumentPointerUp, true); + ownerDocument.removeEventListener( + "onvisibilitychange", + onDocumentVisibilityChange + ); } resizeObserver.disconnect(); diff --git a/lib/global/mutable-state/groups.ts b/lib/global/mutable-state/groups.ts index 37de938e7..957ae65aa 100644 --- a/lib/global/mutable-state/groups.ts +++ b/lib/global/mutable-state/groups.ts @@ -7,6 +7,7 @@ type State = { defaultLayoutDeferred: boolean; derivedPanelConstraints: PanelConstraints[]; groupSize: number; + pendingResizeEventsForElements: Set; layout: Layout; separatorToPanels: SeparatorToPanelsMap; }; @@ -90,13 +91,42 @@ export function subscribeToMountedGroup( }); } -export function updateMountedGroup( - group: RegisteredGroup, - next: State, - meta?: { isUserInteraction?: boolean } -) { +export function updateMountedGroup({ + group, + meta, + partial, + state +}: { + group: RegisteredGroup; + meta?: { isUserInteraction?: boolean }; +} & ( + | { + partial?: never; + state: State; + } + | { + partial: Partial; + state?: never; + } +)) { const prev = map.get(group); + let next: State; + if (state) { + next = state; + } else { + next = { + defaultLayoutDeferred: false, + derivedPanelConstraints: [], + groupSize: 0, + pendingResizeEventsForElements: new Set(), + layout: {}, + separatorToPanels: new Map(), + ...prev, + ...partial + }; + } + map = new Map(map); map.set(group, next); diff --git a/lib/global/utils/adjustLayoutForSeparator.ts b/lib/global/utils/adjustLayoutForSeparator.ts index 24a6a2170..7b235e424 100644 --- a/lib/global/utils/adjustLayoutForSeparator.ts +++ b/lib/global/utils/adjustLayoutForSeparator.ts @@ -43,20 +43,20 @@ export function adjustLayoutForSeparator( }); if (!layoutsEqual(prevLayout, nextLayout)) { - updateMountedGroup( + updateMountedGroup({ group, - { + // Keyboard resizes (arrow keys, Home/End, Enter collapse/expand) originate + // from a real DOM event on the separator, so they are user interactions + // just like pointer drags. This function is only reached from + // onDocumentKeyDown. See #716. + meta: { isUserInteraction: true }, + partial: { defaultLayoutDeferred: groupState.defaultLayoutDeferred, derivedPanelConstraints: groupState.derivedPanelConstraints, groupSize: groupState.groupSize, layout: nextLayout, separatorToPanels: groupState.separatorToPanels - }, - // Keyboard resizes (arrow keys, Home/End, Enter collapse/expand) originate - // from a real DOM event on the separator, so they are user interactions - // just like pointer drags. This function is only reached from - // onDocumentKeyDown. See #716. - { isUserInteraction: true } - ); + } + }); } } diff --git a/lib/global/utils/completeActivePointerResize.ts b/lib/global/utils/completeActivePointerResize.ts index 38bde1e06..859b6cc68 100644 --- a/lib/global/utils/completeActivePointerResize.ts +++ b/lib/global/utils/completeActivePointerResize.ts @@ -38,8 +38,12 @@ export function completeActivePointerResize(document: Document) { return; } const groupState = getMountedGroupState(hitRegion.group.id, true); - updateMountedGroup(hitRegion.group, groupState, { - isUserInteraction: true + updateMountedGroup({ + group: hitRegion.group, + meta: { + isUserInteraction: true + }, + state: groupState }); }); } diff --git a/lib/global/utils/getImperativeGroupMethods.ts b/lib/global/utils/getImperativeGroupMethods.ts index ea38d5f29..b65ab466a 100644 --- a/lib/global/utils/getImperativeGroupMethods.ts +++ b/lib/global/utils/getImperativeGroupMethods.ts @@ -60,12 +60,15 @@ export function getImperativeGroupMethods({ } if (!layoutsEqual(prevLayout, nextLayout)) { - updateMountedGroup(group, { - defaultLayoutDeferred, - derivedPanelConstraints, - groupSize, - layout: nextLayout, - separatorToPanels + updateMountedGroup({ + group, + partial: { + defaultLayoutDeferred, + derivedPanelConstraints, + groupSize, + layout: nextLayout, + separatorToPanels + } }); } diff --git a/lib/global/utils/getImperativePanelMethods.ts b/lib/global/utils/getImperativePanelMethods.ts index 7b9eca04d..da03c3e56 100644 --- a/lib/global/utils/getImperativePanelMethods.ts +++ b/lib/global/utils/getImperativePanelMethods.ts @@ -161,12 +161,15 @@ export function getImperativePanelMethods({ panelConstraints: derivedPanelConstraints }); if (!layoutsEqual(prevLayout, nextLayout)) { - updateMountedGroup(group, { - defaultLayoutDeferred, - derivedPanelConstraints, - groupSize, - layout: nextLayout, - separatorToPanels + updateMountedGroup({ + group, + partial: { + defaultLayoutDeferred, + derivedPanelConstraints, + groupSize, + layout: nextLayout, + separatorToPanels + } }); } }; diff --git a/lib/global/utils/notifyPanelOnResize.ts b/lib/global/utils/notifyPanelOnResize.ts index 714a24081..c488cb2d1 100644 --- a/lib/global/utils/notifyPanelOnResize.ts +++ b/lib/global/utils/notifyPanelOnResize.ts @@ -4,14 +4,8 @@ import { formatLayoutNumber } from "./formatLayoutNumber"; export function notifyPanelOnResize( group: RegisteredGroup, - element: HTMLElement, - borderBoxSize: readonly ResizeObserverSize[] + element: HTMLElement ) { - const resizeObserverSize = borderBoxSize[0]; - if (!resizeObserverSize) { - return; - } - const panel = group.panels.find((current) => current.element === element); if (!panel || !panel.onResize) { return; diff --git a/lib/global/utils/updateActiveHitRegion.ts b/lib/global/utils/updateActiveHitRegion.ts index dcc2ca5a0..2103cf734 100644 --- a/lib/global/utils/updateActiveHitRegion.ts +++ b/lib/global/utils/updateActiveHitRegion.ts @@ -110,12 +110,15 @@ export function updateActiveHitRegions({ } } } else { - updateMountedGroup(current.group, { - defaultLayoutDeferred, - derivedPanelConstraints: derivedPanelConstraints, - groupSize: mountedGroupSize, - layout: nextLayout, - separatorToPanels + updateMountedGroup({ + group: current.group, + partial: { + defaultLayoutDeferred, + derivedPanelConstraints: derivedPanelConstraints, + groupSize: mountedGroupSize, + layout: nextLayout, + separatorToPanels + } }); } }