diff --git a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts index 79e6fc9c6e8..2fd39c55022 100644 --- a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts +++ b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts @@ -389,6 +389,23 @@ describe('stringify static html', () => { ]) }) + test('should remove overloaded boolean attribute for `false`', () => { + const { ast } = compileWithStringify( + `
+ ${repeat( + ``, + StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, + )} +
`, + ) + expect(ast.cached).toMatchObject([ + cachedArrayStaticNodeMatcher( + repeat(``, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT), + StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, + ), + ]) + }) + test('should stringify svg', () => { const svg = `` const repeated = `` diff --git a/packages/compiler-dom/src/transforms/stringifyStatic.ts b/packages/compiler-dom/src/transforms/stringifyStatic.ts index cd8f1a9d184..83f57018338 100644 --- a/packages/compiler-dom/src/transforms/stringifyStatic.ts +++ b/packages/compiler-dom/src/transforms/stringifyStatic.ts @@ -26,6 +26,7 @@ import { isKnownHtmlAttr, isKnownMathMLAttr, isKnownSvgAttr, + isOverloadedBooleanAttr, isString, isSymbol, isVoidTag, @@ -341,7 +342,8 @@ function stringifyElement( } // #6568 if ( - isBooleanAttr((p.arg as SimpleExpressionNode).content) && + (isBooleanAttr((p.arg as SimpleExpressionNode).content) || + isOverloadedBooleanAttr((p.arg as SimpleExpressionNode).content)) && exp.content === 'false' ) { continue diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index 4a9e0fac2b6..441ebd3d986 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -2166,6 +2166,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 12813b598b5..3920fafa651 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, @@ -842,7 +844,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(`