From 1d1ae00d8733c3284dca65197418da1d5e631de6 Mon Sep 17 00:00:00 2001 From: chirokas <157580465+chirokas@users.noreply.github.com> Date: Mon, 31 Mar 2025 12:22:44 +0000 Subject: [PATCH] fix(ssr): render `hidden` correctly --- .../runtime-core/__tests__/hydration.spec.ts | 18 ++++++++++++++++++ packages/runtime-core/src/hydration.ts | 7 ++++++- packages/runtime-dom/src/jsx.ts | 2 +- .../__tests__/ssrRenderAttrs.spec.ts | 16 ++++++++++++++++ .../src/helpers/ssrRenderAttrs.ts | 10 +++++++++- packages/shared/src/domAttrConfig.ts | 15 ++++++++++++++- 6 files changed, 64 insertions(+), 4 deletions(-) diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index 56011d06359..a457c8662ce 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -2086,6 +2086,24 @@ describe('SSR hydration', () => { expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() }) + test('combined boolean/string attribute', () => { + mountWithHydration(`
`, () => h('div', { hidden: false })) + expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() + + mountWithHydration(``, () => h('div', { hidden: true })) + expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() + + mountWithHydration(``, () => + h('div', { hidden: 'until-found' }), + ) + expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() + + mountWithHydration(``, () => + h('div', { hidden: true }), + ) + expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() + }) + test('client value is null or undefined', () => { mountWithHydration(`
`, () => h('div', { draggable: undefined }), diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index a94ff356810..d43e773a0fc 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -21,9 +21,11 @@ import { getEscapedCssVarName, includeBooleanAttr, isBooleanAttr, + isBooleanAttrValue, isKnownHtmlAttr, isKnownSvgAttr, isOn, + isOverloadedBooleanAttr, isRenderableAttrValue, isReservedProp, isString, @@ -835,7 +837,10 @@ function propHasMismatch( (el instanceof SVGElement && isKnownSvgAttr(key)) || (el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key))) ) { - if (isBooleanAttr(key)) { + if ( + isBooleanAttr(key) || + (isOverloadedBooleanAttr(key) && isBooleanAttrValue(clientValue)) + ) { actual = el.hasAttribute(key) expected = includeBooleanAttr(clientValue) } else if (clientValue == null) { diff --git a/packages/runtime-dom/src/jsx.ts b/packages/runtime-dom/src/jsx.ts index 5292441cde9..0240695cdda 100644 --- a/packages/runtime-dom/src/jsx.ts +++ b/packages/runtime-dom/src/jsx.ts @@ -264,7 +264,7 @@ export interface HTMLAttributes extends AriaAttributes, EventHandlers { contextmenu?: string dir?: string draggable?: Booleanish - hidden?: Booleanish | '' | 'hidden' | 'until-found' + hidden?: boolean | '' | 'hidden' | 'until-found' id?: string inert?: Booleanish lang?: string diff --git a/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts b/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts index 9f33866e5a8..960f913212e 100644 --- a/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts +++ b/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts @@ -55,6 +55,15 @@ describe('ssr: renderAttrs', () => { ).toBe(` checked disabled`) // boolean attr w/ false should be ignored }) + test('combined boolean/string attribute', () => { + expect(ssrRenderAttrs({ hidden: true })).toBe(` hidden`) + expect(ssrRenderAttrs({ disabled: true, hidden: false })).toBe(` disabled`) + expect(ssrRenderAttrs({ hidden: 'until-found' })).toBe( + ` hidden="until-found"`, + ) + expect(ssrRenderAttrs({ hidden: '' })).toBe(` hidden`) + }) + test('ignore falsy values', () => { expect( ssrRenderAttrs({ @@ -122,6 +131,13 @@ describe('ssr: renderAttr', () => { ` foo="${escapeHtml(`