From 79784962e1b85d4a81495fef0ab5e62404b0d3c5 Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Thu, 6 Aug 2026 12:06:28 -0400 Subject: [PATCH] fix(flex): give justifyContent free space to distribute again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A flex row with no explicit width seeds w = 0 and only the flexStart branch of the layout resizes the container to fit its children. Every other justify mode computes against containerSize, so with a shrink-to-fit width they position children out of zero free space — producing negative offsets against a zero-width container rather than simply falling back to flexStart. This regressed in e7310f8, which switched non-fixed flex rows from filling the parent to seeding 0 to avoid a resize flash. Restore the previous default when justifyContent needs free space: that case never triggers the shrink-to-fit resize, so it can't flash. An explicit flexBoundary 'contain' still wins — that combination is a genuine contradiction, so honor the boundary and warn in dev. Replaces the #39 warning, which tested kebab-case strings against a camelCase union and so only ever fired for 'center'. Testing !== 'flexStart' avoids the mismatch entirely and drops the `as string` cast that hid it from the compiler. Co-Authored-By: Claude Opus 5 --- src/core/elementNode.ts | 24 ++++---- tests/flex-justify-width.test.tsx | 98 +++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 tests/flex-justify-width.test.tsx diff --git a/src/core/elementNode.ts b/src/core/elementNode.ts index 2a54d1b..e4f8505 100644 --- a/src/core/elementNode.ts +++ b/src/core/elementNode.ts @@ -1663,18 +1663,20 @@ export class ElementNode { flexDirection === 'row' || flexDirection === 'row-reverse'; flexFitsWidth = isFlexRow && node.flexBoundary !== 'fixed'; - if (isDev && flexFitsWidth) { - const needsWidthForJustify = [ - 'center', - 'flex-end', - 'space-between', - 'space-around', - 'space-evenly', - ].includes(node.justifyContent as string); - - if (needsWidthForJustify) { + // Every justify mode except flexStart positions children out of the + // container's free space. Shrinking to fit leaves none, so fall back + // to filling the parent unless the developer explicitly asked to + // contain — in which case honor it and warn about the contradiction. + if ( + flexFitsWidth && + node.justifyContent !== undefined && + node.justifyContent !== 'flexStart' + ) { + if (node.flexBoundary === undefined) { + flexFitsWidth = false; + } else if (isDev) { console.warn( - `justifyContent '${node.justifyContent}' requires an explicit width on the flex container; without one it shrinks to fit its children and has no free space to distribute: `, + `justifyContent '${node.justifyContent}' has no free space to distribute on a flexBoundary 'contain' container without an explicit width: `, this, ); } diff --git a/tests/flex-justify-width.test.tsx b/tests/flex-justify-width.test.tsx new file mode 100644 index 0000000..db916a1 --- /dev/null +++ b/tests/flex-justify-width.test.tsx @@ -0,0 +1,98 @@ +import * as v from 'vitest' +import * as lng from '@solidtv/solid' + +import {renderer, waitForUpdate} from './setup.js' + +const PARENT = 600 +const CHILD = 100 + +v.test('flex row without width fills parent when justifyContent needs free space', async () => { + let center!: lng.ElementNode + let centerOne!: lng.ElementNode + let between!: lng.ElementNode + let betweenTwo!: lng.ElementNode + + const dispose = renderer.render(() => ( + + + + + + + + + + + )) + + await waitForUpdate() + + // Container fills the parent instead of shrinking to its children... + v.assert.equal(center.width, PARENT) + // ...so there is free space to center into: (600 - 200) / 2 + v.assert.equal(centerOne.x, (PARENT - CHILD * 2) / 2) + + // spaceBetween pushes the last child to the far edge rather than to a + // negative offset against a zero-width container. + v.assert.equal(between.width, PARENT) + v.assert.equal(betweenTwo.x, PARENT - CHILD) + + dispose() +}) + +v.test('flex row without width still shrinks to fit for flexStart and no justifyContent', async () => { + let implicit!: lng.ElementNode + let start!: lng.ElementNode + + const dispose = renderer.render(() => ( + + + + + + + + + + + )) + + await waitForUpdate() + + v.assert.equal(implicit.width, CHILD * 2) + v.assert.equal(start.width, CHILD * 2) + + dispose() +}) + +v.test('an explicit flexBoundary contain wins over justifyContent and warns', async () => { + const warn = v.vi.spyOn(console, 'warn').mockImplementation(() => {}) + let contained!: lng.ElementNode + + const dispose = renderer.render(() => ( + + + + + + + )) + + await waitForUpdate() + + // The explicit contain is honored — the container is not widened to the parent. + v.assert.notEqual(contained.width, PARENT) + v.assert.isTrue( + warn.mock.calls.some((args) => String(args[0]).includes('justifyContent')), + 'expected a dev warning about the contradiction', + ) + + warn.mockRestore() + dispose() +})