])
})
+ test.each(['false', '0'])('should remove `hidden` with value `%s`', value => {
+ const { ast } = compileWithStringify(
+ `<div>
+ ${repeat(
+ `<span :hidden="${value}"></span>`,
+ StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
+ )}
+ </div>`,
+ )
+ expect(ast.cached).toMatchObject([
+ cachedArrayStaticNodeMatcher(
+ repeat(`<span></span>`, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT),
+ StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
+ ),
+ ])
+ })
+
test('should stringify svg', () => {
const svg = `<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">`
const repeated = `<rect width="50" height="50" fill="#C4C4C4"></rect>`
} from '@vue/compiler-core'
import {
escapeHtml,
+ includeBooleanAttr,
isArray,
isBooleanAttr,
isKnownHtmlAttr,
}
// #6568
if (
- isBooleanAttr((p.arg as SimpleExpressionNode).content) &&
+ (isBooleanAttr((p.arg as SimpleExpressionNode).content) ||
+ (p.arg as SimpleExpressionNode).content === 'hidden') &&
exp.content === 'false'
) {
continue
let evaluated = evaluateConstant(exp)
if (evaluated != null) {
const arg = p.arg && (p.arg as SimpleExpressionNode).content
+ if (
+ arg === 'hidden' &&
+ typeof evaluated === 'number' &&
+ !includeBooleanAttr(evaluated)
+ ) {
+ continue
+ }
if (arg === 'class') {
evaluated = normalizeClass(evaluated)
} else if (arg === 'style') {
`)
})
+ test('v-bind:arg (hidden)', () => {
+ expect(
+ getCompiledString(
+ `<div><span :hidden="false"></span><span :hidden="'until-found'"></span></div>`,
+ ),
+ ).toMatchInlineSnapshot(`
+ "\`<div><span\${
+ _ssrRenderDynamicAttr("hidden", false)
+ }></span><span\${
+ _ssrRenderDynamicAttr("hidden", 'until-found')
+ }></span></div>\`"
+ `)
+ })
+
test('v-bind:[arg]', () => {
expect(getCompiledString(`<div v-bind:[key]="value"></div>`))
.toMatchInlineSnapshot(`
false /* no newline */,
),
)
+ } else if (attrName === 'hidden') {
+ openTag.push(
+ createCallExpression(
+ context.helper(SSR_RENDER_DYNAMIC_ATTR),
+ [key, value],
+ ),
+ )
} else if (isSSRSafeAttrName(attrName)) {
openTag.push(
createCallExpression(context.helper(SSR_RENDER_ATTR), [
expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
})
+ test('hidden enumerated 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></div>`, () =>
+ h('div', { hidden: 'hidden' }),
+ )
+ expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+
+ mountWithHydration(`<div hidden="anything"></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="UNTIL-FOUND"></div>`, () =>
+ h('div', { hidden: 'until-found' }),
+ )
+ expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+ })
+
+ test('hidden numeric values', () => {
+ mountWithHydration(`<div></div>`, () => h('div', { hidden: 0 }))
+ expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+
+ mountWithHydration(`<div></div>`, () => h('div', { hidden: NaN }))
+ expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+
+ mountWithHydration(`<div hidden></div>`, () => h('div', { hidden: 1 }))
+ expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+
+ mountWithHydration(`<div hidden="0"></div>`, () =>
+ h('div', { hidden: '0' }),
+ )
+ expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+ })
+
+ test('hidden state mismatch', () => {
+ mountWithHydration(`<div hidden="until-found"></div>`, () =>
+ h('div', { hidden: true }),
+ )
+ expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(1)
+
+ mountWithHydration(`<div hidden></div>`, () => h('div', { hidden: 0 }))
+ expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(2)
+ })
+
test('client value is null or undefined', () => {
mountWithHydration(`<div></div>`, () =>
h('div', { draggable: undefined }),
(el instanceof SVGElement && isKnownSvgAttr(key)) ||
(el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key)))
) {
- if (isBooleanAttr(key)) {
+ if (key === 'hidden') {
+ actual = normalizeHiddenValue(el.getAttribute(key))
+ expected = normalizeHiddenValue(clientValue)
+ } else if (isBooleanAttr(key)) {
actual = el.hasAttribute(key)
expected = includeBooleanAttr(clientValue)
} else if (clientValue == null) {
return false
}
+function normalizeHiddenValue(value: unknown): false | '' | 'until-found' {
+ if (!isRenderableAttrValue(value)) {
+ return false
+ }
+ if (isString(value)) {
+ // Attribute values from the DOM are strings, while numeric client values
+ // follow the `hidden` property setter, where 0 and NaN remove the attribute.
+ return value.toLowerCase() === 'until-found' ? 'until-found' : ''
+ }
+ return includeBooleanAttr(value) ? '' : false
+}
+
function toClassSet(str: string): Set<string> {
return new Set(str.trim().split(/\s+/))
}
).toBe(` checked disabled`) // boolean attr w/ false should be ignored
})
+ test('hidden enumerated 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`)
+ expect(ssrRenderAttrs({ hidden: 0 })).toBe(``)
+ expect(ssrRenderAttrs({ hidden: NaN })).toBe(``)
+ expect(ssrRenderAttrs({ hidden: 1 })).toBe(` hidden`)
+ expect(ssrRenderAttrs({ hidden: '0' })).toBe(` hidden="0"`)
+ })
+
test('ignore falsy values', () => {
expect(
ssrRenderAttrs({
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) ||
+ (attrKey === 'hidden' &&
+ (typeof value === 'boolean' || typeof value === 'number'))
+ ) {
return includeBooleanAttr(value) ? ` ${attrKey}` : ``
} else if (isSSRSafeAttrName(attrKey)) {
return value === '' ? ` ${attrKey}` : ` ${attrKey}="${escapeHtml(value)}"`
*/
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`,
)