From 2db19550784999517a83b05b55080898193d295a Mon Sep 17 00:00:00 2001 From: Zyie <24736175+Zyie@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:30:12 +0100 Subject: [PATCH] fix: dynamic removal of layout elements fixes functionality to dynamically remove elements from the layout and recalculate the layout. Modifies the root invalidation to allow it to start searching for the root from a specific container. This ensures the correct root is found after removing a child. --- src/core/Layout.ts | 20 ++++--- tests/__tests__/index.test.tsx | 2 + ...-dynamic-changes-delete--delete-webkit.png | Bin 0 -> 6168 bytes .../dynamicChanges/dynamicDelete.stories.tsx | 55 ++++++++++++++++++ 4 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 tests/snapshots/yoga-dynamic-changes-delete--delete-webkit.png create mode 100644 tests/stories/custom/dynamicChanges/dynamicDelete.stories.tsx diff --git a/src/core/Layout.ts b/src/core/Layout.ts index f939729f..98de44c8 100644 --- a/src/core/Layout.ts +++ b/src/core/Layout.ts @@ -223,9 +223,14 @@ export class Layout { } } - /** Marks the root layout as needing recalculation */ - public invalidateRoot(): void { - const root = this.getRoot(); + /** + * Marks the root layout as needing recalculation + * @param start - Optional container to start searching for the root from + */ + public invalidateRoot(start?: Container): void { + const root = this.getRoot(start); + + if (root.destroyed) return; root._layout!._isDirty = true; root._onUpdate(); @@ -244,11 +249,12 @@ export class Layout { /** * Finds the root container by traversing up the layout tree + * @param start - Optional container to start searching for the root from * @returns The root container */ - public getRoot(): Container { + public getRoot(start?: Container): Container { // find the root node by traversing up the yoga tree - let root: Container = this.target as Container; + let root: Container = start || (this.target as Container); while (root.parent?._layout || (root.parent as OverflowContainer)?.isOverflowContainer) { root = root.parent; @@ -278,11 +284,11 @@ export class Layout { /** * @ignore */ - public _onChildRemoved(): void { + public _onChildRemoved(parent?: Container): void { if (!this.hasParent) return; this.hasParent = false; - this.invalidateRoot(); + this.invalidateRoot(parent); onChildRemoved(this); } diff --git a/tests/__tests__/index.test.tsx b/tests/__tests__/index.test.tsx index 384a0f3c..5bf4789f 100644 --- a/tests/__tests__/index.test.tsx +++ b/tests/__tests__/index.test.tsx @@ -46,6 +46,7 @@ import * as objectFitLayoutViewStories from '../stories/custom/objectFit/objectF import * as objectFitStories from '../stories/custom/objectFit/objectFit.stories'; import * as isLeafStories from '../stories/custom/leaf/isLeaf.stories'; import * as dynamicTextStories from '../stories/custom/dynamicChanges/dynamicText.stories'; +import * as dynamicDeleteStories from '../stories/custom/dynamicChanges/dynamicDelete.stories'; import * as layoutContainerBackgroundStories from '../stories/custom/components/layoutContainerBackground.stories'; import * as layoutContainerStories from '../stories/custom/components/layoutContainer.stories'; import * as applySizeStories from '../stories/custom/applySize/applySize.stories'; @@ -93,6 +94,7 @@ allStories.objectFitLayoutViewStories = composeStories(objectFitLayoutViewStorie allStories.objectFitStories = composeStories(objectFitStories); allStories.isLeafStories = composeStories(isLeafStories); allStories.dynamicTextStories = composeStories(dynamicTextStories); +allStories.dynamicDeleteStories = composeStories(dynamicDeleteStories); allStories.layoutContainerBackgroundStories = composeStories(layoutContainerBackgroundStories); allStories.layoutContainerStories = composeStories(layoutContainerStories); allStories.applySizeStories = composeStories(applySizeStories); diff --git a/tests/snapshots/yoga-dynamic-changes-delete--delete-webkit.png b/tests/snapshots/yoga-dynamic-changes-delete--delete-webkit.png new file mode 100644 index 0000000000000000000000000000000000000000..a9317d70483a6b3425ca0f3d23a082224e56b131 GIT binary patch literal 6168 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 L>ZzC9z&!pRv0wXu literal 0 HcmV?d00001 diff --git a/tests/stories/custom/dynamicChanges/dynamicDelete.stories.tsx b/tests/stories/custom/dynamicChanges/dynamicDelete.stories.tsx new file mode 100644 index 00000000..9a5be1eb --- /dev/null +++ b/tests/stories/custom/dynamicChanges/dynamicDelete.stories.tsx @@ -0,0 +1,55 @@ +import { type Text as PixiText } from 'pixi.js'; +import React, { type FC } from 'react'; +import { ReactStory } from '../../yoga/utils/reactStory'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const LayoutComponent: FC = ({}) => { + const refs: Record> = {}; + + for (let i = 0; i < 4; i++) { + refs[`r${i + 1}`] = React.createRef(); + } + + setTimeout(() => { + Object.values(refs)[2]!.current?.removeFromParent(); + }, 100); + + return ( + + + + + + + + + ); +}; + +// 👇 This default export determines where your story goes in the story list +const meta: Meta = { + title: 'Yoga/Dynamic Changes/Delete', + component: LayoutComponent, + render: (args) => ( + + + + ), +}; + +export default meta; +type Story = StoryObj; + +export const Delete: Story = { + tags: ['delay:200'], + args: {}, +};