mirror of https://github.com/vuejs/core.git
				
				
				
			fix(ssr): render `hidden` correctly
This commit is contained in:
		
							parent
							
								
									f6e84af30a
								
							
						
					
					
						commit
						1d1ae00d87
					
				|  | @ -2086,6 +2086,24 @@ describe('SSR hydration', () => { | |||
|       expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() | ||||
|     }) | ||||
| 
 | ||||
|     test('combined boolean/string attribute', () => { | ||||
|       mountWithHydration(`<div></div>`, () => h('div', { hidden: false })) | ||||
|       expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() | ||||
| 
 | ||||
|       mountWithHydration(`<div hidden></div>`, () => h('div', { hidden: true })) | ||||
|       expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() | ||||
| 
 | ||||
|       mountWithHydration(`<div hidden="until-found"></div>`, () => | ||||
|         h('div', { hidden: 'until-found' }), | ||||
|       ) | ||||
|       expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() | ||||
| 
 | ||||
|       mountWithHydration(`<div hidden=""></div>`, () => | ||||
|         h('div', { hidden: true }), | ||||
|       ) | ||||
|       expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() | ||||
|     }) | ||||
| 
 | ||||
|     test('client value is null or undefined', () => { | ||||
|       mountWithHydration(`<div></div>`, () => | ||||
|         h('div', { draggable: undefined }), | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -264,7 +264,7 @@ export interface HTMLAttributes extends AriaAttributes, EventHandlers<Events> { | |||
|   contextmenu?: string | ||||
|   dir?: string | ||||
|   draggable?: Booleanish | ||||
|   hidden?: Booleanish | '' | 'hidden' | 'until-found' | ||||
|   hidden?: boolean | '' | 'hidden' | 'until-found' | ||||
|   id?: string | ||||
|   inert?: Booleanish | ||||
|   lang?: string | ||||
|  |  | |||
|  | @ -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(`<script>`)}"`, | ||||
|     ) | ||||
|   }) | ||||
| 
 | ||||
|   test('combined boolean/string attribute', () => { | ||||
|     expect(ssrRenderAttr('hidden', true)).toBe(` hidden`) | ||||
|     expect(ssrRenderAttr('hidden', false)).toBe('') | ||||
|     expect(ssrRenderAttr('hidden', 'until-found')).toBe(` hidden="until-found"`) | ||||
|     expect(ssrRenderAttr('hidden', '')).toBe(` hidden`) | ||||
|   }) | ||||
| }) | ||||
| 
 | ||||
| describe('ssr: renderClass', () => { | ||||
|  |  | |||
|  | @ -1,5 +1,7 @@ | |||
| import { | ||||
|   escapeHtml, | ||||
|   isBooleanAttrValue, | ||||
|   isOverloadedBooleanAttr, | ||||
|   isRenderableAttrValue, | ||||
|   isSVGTag, | ||||
|   stringifyStyle, | ||||
|  | @ -61,7 +63,10 @@ export function ssrRenderDynamicAttr( | |||
|     tag && (tag.indexOf('-') > 0 || isSVGTag(tag)) | ||||
|       ? key // preserve raw name on custom elements and svg
 | ||||
|       : propsToAttrMap[key] || key.toLowerCase() | ||||
|   if (isBooleanAttr(attrKey)) { | ||||
|   if ( | ||||
|     isBooleanAttr(attrKey) || | ||||
|     (isOverloadedBooleanAttr(attrKey) && isBooleanAttrValue(value)) | ||||
|   ) { | ||||
|     return includeBooleanAttr(value) ? ` ${attrKey}` : `` | ||||
|   } else if (isSSRSafeAttrName(attrKey)) { | ||||
|     return value === '' ? ` ${attrKey}` : ` ${attrKey}="${escapeHtml(value)}"` | ||||
|  | @ -79,6 +84,9 @@ export function ssrRenderAttr(key: string, value: unknown): string { | |||
|   if (!isRenderableAttrValue(value)) { | ||||
|     return `` | ||||
|   } | ||||
|   if (isOverloadedBooleanAttr(key) && isBooleanAttrValue(value)) { | ||||
|     return includeBooleanAttr(value) ? ` ${key}` : `` | ||||
|   } | ||||
|   return ` ${key}="${escapeHtml(value)}"` | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ export const isSpecialBooleanAttr: (key: string) => boolean = | |||
|  */ | ||||
| export const isBooleanAttr: (key: string) => boolean = /*@__PURE__*/ makeMap( | ||||
|   specialBooleanAttrs + | ||||
|     `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,` + | ||||
|     `,async,autofocus,autoplay,controls,default,defer,disabled,` + | ||||
|     `inert,loop,open,required,reversed,scoped,seamless,` + | ||||
|     `checked,muted,multiple,selected`, | ||||
| ) | ||||
|  | @ -152,3 +152,16 @@ export function isRenderableAttrValue(value: unknown): boolean { | |||
|   const type = typeof value | ||||
|   return type === 'string' || type === 'number' || type === 'boolean' | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * An attribute that can be used as a flag as well as with a value. | ||||
|  * When `true`, it should be present (set either to an empty string or its name). | ||||
|  * When `false`, it should be omitted. | ||||
|  * For any other value, should be present with that value. | ||||
|  */ | ||||
| export const isOverloadedBooleanAttr: (key: string) => boolean = | ||||
|   /*@__PURE__*/ makeMap('hidden') | ||||
| 
 | ||||
| export function isBooleanAttrValue(value: unknown): boolean { | ||||
|   return typeof value === 'boolean' || value === '' | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue