diff --git a/src/api/attributes.spec.ts b/src/api/attributes.spec.ts index 3e965b3dc2..13e7dfd2d0 100644 --- a/src/api/attributes.spec.ts +++ b/src/api/attributes.spec.ts @@ -44,6 +44,26 @@ describe('$(...)', () => { expect(attr).toBe('autofocus'); }); + it('(valid key) : should get "hidden" when attribute has no value', () => { + const attr = $('').attr('hidden'); + expect(attr).toBe('hidden'); + }); + + it('(valid key) : should get "hidden" when attribute is set to "hidden"', () => { + const attr = $('').attr('hidden'); + expect(attr).toBe('hidden'); + }); + + it('(valid key) : should get "hidden" for other boolean hidden values', () => { + const attr = $('').attr('hidden'); + expect(attr).toBe('hidden'); + }); + + it('(valid key) : should preserve non-boolean hidden attribute values', () => { + const attr = $('').attr('hidden'); + expect(attr).toBe('until-found'); + }); + it('(key, value) : should set one attr', () => { const $pear = $('.pear').attr('id', 'pear'); expect($('#pear')).toHaveLength(1); @@ -349,6 +369,51 @@ describe('$(...)', () => { expect($(undefined).prop('src')).toBeUndefined(); }); + it('("hidden") : should support new "until-found" value while maintaining boolean values', () => { + const $ = load( + ` + + + + + +
+ `, + ); + + expect($('#1').prop('hidden')).toBe(true); + expect($('#2').prop('hidden')).toBe(true); + expect($('#3').prop('hidden')).toBe(true); + expect($('#4').prop('hidden')).toBe(true); + expect($('#5').prop('hidden')).toBe('until-found'); + expect($('#6').prop('hidden')).toBe(false); + + expect($(undefined).prop('hidden')).toBeUndefined(); + }); + + it('("hidden") : should sync hidden property and attribute when setting values', () => { + const $ = load('
'); + const div = $('#1'); + + expect(div.prop('hidden')).toBe(false); + expect(div.attr('hidden')).toBe(undefined); + div.prop('hidden', true); + expect(div.prop('hidden')).toBe(true); + expect(div.attr('hidden')).toBe('hidden'); + div.prop('hidden', 'hidden'); + expect(div.prop('hidden')).toBe(true); + expect(div.attr('hidden')).toBe('hidden'); + div.prop('hidden', 'bananas'); + expect(div.prop('hidden')).toBe(true); + expect(div.attr('hidden')).toBe('hidden'); + div.prop('hidden', 'until-found'); + expect(div.prop('hidden')).toBe('until-found'); + expect(div.attr('hidden')).toBe('until-found'); + div.prop('hidden', false); + expect(div.prop('hidden')).toBe(false); + expect(div.attr('hidden')).toBe(undefined); + }); + it('("outerHTML") : should render properly', () => { const outerHtml = '
'; const $a = $(outerHtml); diff --git a/src/api/attributes.ts b/src/api/attributes.ts index 24425c8b7d..56a7535d65 100644 --- a/src/api/attributes.ts +++ b/src/api/attributes.ts @@ -20,7 +20,7 @@ const dataAttrPrefix = 'data-'; // Attributes that are booleans const rboolean = - /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i; + /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|loop|multiple|open|readonly|required|scoped|selected)$/i; // Matches strings that look like JSON objects or arrays const rbrace = /^{[^]*}$|^\[[^]*]$/; @@ -62,6 +62,12 @@ function getAttr( } if (hasOwn(elem.attribs, name)) { + if (!xmlMode && name === 'hidden') { + const value = elem.attribs[name]; + + return value === 'until-found' ? 'until-found' : 'hidden'; + } + // Get the (decoded) attribute return !xmlMode && rboolean.test(name) ? name : elem.attribs[name]; } @@ -239,12 +245,22 @@ function getProp( name: string, xmlMode?: boolean, ): string | undefined | boolean | Element[keyof Element] { - return name in el - ? // @ts-expect-error TS doesn't like us accessing the value directly here. - (el[name] as string | undefined) - : !xmlMode && rboolean.test(name) - ? getAttr(el, name, false) !== undefined - : getAttr(el, name, xmlMode); + if (name in el) { + // @ts-expect-error TS doesn't like us accessing the value directly here. + return el[name] as string | undefined; + } + + if (!xmlMode && name === 'hidden') { + const value = getAttr(el, name, false); + + return value === 'until-found' ? 'until-found' : value !== undefined; + } + + if (!xmlMode && rboolean.test(name)) { + return getAttr(el, name, false) !== undefined; + } + + return getAttr(el, name, xmlMode); } /** @@ -260,17 +276,24 @@ function setProp(el: Element, name: string, value: unknown, xmlMode?: boolean) { if (name in el) { // @ts-expect-error Overriding value el[name] = value; - } else { + return; + } + + if (!xmlMode && name === 'hidden') { setAttr( el, name, - !xmlMode && rboolean.test(name) - ? value - ? '' - : null - : `${value as string}`, + value === 'until-found' ? 'until-found' : value ? '' : null, ); + return; + } + + if (!xmlMode && rboolean.test(name)) { + setAttr(el, name, value ? '' : null); + return; } + + setAttr(el, name, `${value as string}`); } interface StyleProp {