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() |       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', () => { |     test('client value is null or undefined', () => { | ||||||
|       mountWithHydration(`<div></div>`, () => |       mountWithHydration(`<div></div>`, () => | ||||||
|         h('div', { draggable: undefined }), |         h('div', { draggable: undefined }), | ||||||
|  |  | ||||||
|  | @ -21,9 +21,11 @@ import { | ||||||
|   getEscapedCssVarName, |   getEscapedCssVarName, | ||||||
|   includeBooleanAttr, |   includeBooleanAttr, | ||||||
|   isBooleanAttr, |   isBooleanAttr, | ||||||
|  |   isBooleanAttrValue, | ||||||
|   isKnownHtmlAttr, |   isKnownHtmlAttr, | ||||||
|   isKnownSvgAttr, |   isKnownSvgAttr, | ||||||
|   isOn, |   isOn, | ||||||
|  |   isOverloadedBooleanAttr, | ||||||
|   isRenderableAttrValue, |   isRenderableAttrValue, | ||||||
|   isReservedProp, |   isReservedProp, | ||||||
|   isString, |   isString, | ||||||
|  | @ -835,7 +837,10 @@ function propHasMismatch( | ||||||
|     (el instanceof SVGElement && isKnownSvgAttr(key)) || |     (el instanceof SVGElement && isKnownSvgAttr(key)) || | ||||||
|     (el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key))) |     (el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key))) | ||||||
|   ) { |   ) { | ||||||
|     if (isBooleanAttr(key)) { |     if ( | ||||||
|  |       isBooleanAttr(key) || | ||||||
|  |       (isOverloadedBooleanAttr(key) && isBooleanAttrValue(clientValue)) | ||||||
|  |     ) { | ||||||
|       actual = el.hasAttribute(key) |       actual = el.hasAttribute(key) | ||||||
|       expected = includeBooleanAttr(clientValue) |       expected = includeBooleanAttr(clientValue) | ||||||
|     } else if (clientValue == null) { |     } else if (clientValue == null) { | ||||||
|  |  | ||||||
|  | @ -264,7 +264,7 @@ export interface HTMLAttributes extends AriaAttributes, EventHandlers<Events> { | ||||||
|   contextmenu?: string |   contextmenu?: string | ||||||
|   dir?: string |   dir?: string | ||||||
|   draggable?: Booleanish |   draggable?: Booleanish | ||||||
|   hidden?: Booleanish | '' | 'hidden' | 'until-found' |   hidden?: boolean | '' | 'hidden' | 'until-found' | ||||||
|   id?: string |   id?: string | ||||||
|   inert?: Booleanish |   inert?: Booleanish | ||||||
|   lang?: string |   lang?: string | ||||||
|  |  | ||||||
|  | @ -55,6 +55,15 @@ describe('ssr: renderAttrs', () => { | ||||||
|     ).toBe(` checked disabled`) // boolean attr w/ false should be ignored
 |     ).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', () => { |   test('ignore falsy values', () => { | ||||||
|     expect( |     expect( | ||||||
|       ssrRenderAttrs({ |       ssrRenderAttrs({ | ||||||
|  | @ -122,6 +131,13 @@ describe('ssr: renderAttr', () => { | ||||||
|       ` foo="${escapeHtml(`<script>`)}"`, |       ` 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', () => { | describe('ssr: renderClass', () => { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| import { | import { | ||||||
|   escapeHtml, |   escapeHtml, | ||||||
|  |   isBooleanAttrValue, | ||||||
|  |   isOverloadedBooleanAttr, | ||||||
|   isRenderableAttrValue, |   isRenderableAttrValue, | ||||||
|   isSVGTag, |   isSVGTag, | ||||||
|   stringifyStyle, |   stringifyStyle, | ||||||
|  | @ -61,7 +63,10 @@ export function ssrRenderDynamicAttr( | ||||||
|     tag && (tag.indexOf('-') > 0 || isSVGTag(tag)) |     tag && (tag.indexOf('-') > 0 || isSVGTag(tag)) | ||||||
|       ? key // preserve raw name on custom elements and svg
 |       ? key // preserve raw name on custom elements and svg
 | ||||||
|       : propsToAttrMap[key] || key.toLowerCase() |       : propsToAttrMap[key] || key.toLowerCase() | ||||||
|   if (isBooleanAttr(attrKey)) { |   if ( | ||||||
|  |     isBooleanAttr(attrKey) || | ||||||
|  |     (isOverloadedBooleanAttr(attrKey) && isBooleanAttrValue(value)) | ||||||
|  |   ) { | ||||||
|     return includeBooleanAttr(value) ? ` ${attrKey}` : `` |     return includeBooleanAttr(value) ? ` ${attrKey}` : `` | ||||||
|   } else if (isSSRSafeAttrName(attrKey)) { |   } else if (isSSRSafeAttrName(attrKey)) { | ||||||
|     return value === '' ? ` ${attrKey}` : ` ${attrKey}="${escapeHtml(value)}"` |     return value === '' ? ` ${attrKey}` : ` ${attrKey}="${escapeHtml(value)}"` | ||||||
|  | @ -79,6 +84,9 @@ export function ssrRenderAttr(key: string, value: unknown): string { | ||||||
|   if (!isRenderableAttrValue(value)) { |   if (!isRenderableAttrValue(value)) { | ||||||
|     return `` |     return `` | ||||||
|   } |   } | ||||||
|  |   if (isOverloadedBooleanAttr(key) && isBooleanAttrValue(value)) { | ||||||
|  |     return includeBooleanAttr(value) ? ` ${key}` : `` | ||||||
|  |   } | ||||||
|   return ` ${key}="${escapeHtml(value)}"` |   return ` ${key}="${escapeHtml(value)}"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -20,7 +20,7 @@ export const isSpecialBooleanAttr: (key: string) => boolean = | ||||||
|  */ |  */ | ||||||
| export const isBooleanAttr: (key: string) => boolean = /*@__PURE__*/ makeMap( | export const isBooleanAttr: (key: string) => boolean = /*@__PURE__*/ makeMap( | ||||||
|   specialBooleanAttrs + |   specialBooleanAttrs + | ||||||
|     `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,` + |     `,async,autofocus,autoplay,controls,default,defer,disabled,` + | ||||||
|     `inert,loop,open,required,reversed,scoped,seamless,` + |     `inert,loop,open,required,reversed,scoped,seamless,` + | ||||||
|     `checked,muted,multiple,selected`, |     `checked,muted,multiple,selected`, | ||||||
| ) | ) | ||||||
|  | @ -152,3 +152,16 @@ export function isRenderableAttrValue(value: unknown): boolean { | ||||||
|   const type = typeof value |   const type = typeof value | ||||||
|   return type === 'string' || type === 'number' || type === 'boolean' |   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