From 343dfd2b7faf54b9a77ce32c27eedc80361b7607 Mon Sep 17 00:00:00 2001 From: Jonathan Gotti Date: Thu, 29 Aug 2024 16:21:27 +0200 Subject: [PATCH] fix: [#1439] Fixes the HTMLInputElement.indeterminate, so that it behaves correctly (#1475) * fix: [#1439] Fix setting indeterminate sets an attribute (indeterminate is not an attribute) * fix: [#1439] Set indeterminate to false on click event --------- Co-authored-by: David Ortner --- packages/happy-dom/src/PropertySymbol.ts | 1 + .../html-input-element/HTMLInputElement.ts | 15 +++--- .../HTMLInputElement.test.ts | 46 +++++++++++++++---- 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts index c92dbb57e..f9a91eee5 100644 --- a/packages/happy-dom/src/PropertySymbol.ts +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -31,6 +31,7 @@ export const formNode = Symbol('formNode'); export const internalId = Symbol('internalId'); export const height = Symbol('height'); export const immediatePropagationStopped = Symbol('immediatePropagationStopped'); +export const indeterminate = Symbol('indeterminate'); export const isFirstWrite = Symbol('isFirstWrite'); export const isFirstWriteAfterOpen = Symbol('isFirstWriteAfterOpen'); export const isInPassiveEventListener = Symbol('isInPassiveEventListener'); diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts index 0fb6724cb..0c392bc98 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts @@ -46,6 +46,7 @@ export default class HTMLInputElement extends HTMLElement { public [PropertySymbol.validationMessage] = ''; public [PropertySymbol.validity] = new ValidityState(this); public [PropertySymbol.files]: FileList = new FileList(); + public [PropertySymbol.indeterminate]: boolean = false; public [PropertySymbol.formNode]: HTMLFormElement | null = null; public [PropertySymbol.popoverTargetElement]: HTMLElement | null = null; @@ -683,7 +684,7 @@ export default class HTMLInputElement extends HTMLElement { * @returns Indeterminate. */ public get indeterminate(): boolean { - return this.getAttribute('indeterminate') !== null; + return this[PropertySymbol.indeterminate]; } /** @@ -692,11 +693,7 @@ export default class HTMLInputElement extends HTMLElement { * @param indeterminate Indeterminate. */ public set indeterminate(indeterminate: boolean) { - if (!indeterminate) { - this.removeAttribute('indeterminate'); - } else { - this.setAttribute('indeterminate', ''); - } + this[PropertySymbol.indeterminate] = Boolean(indeterminate); } /** @@ -1372,6 +1369,7 @@ export default class HTMLInputElement extends HTMLElement { } let previousCheckedValue: boolean | null = null; + let previousIndeterminateValue: boolean = this[PropertySymbol.indeterminate]; // The checkbox or radio button has to be checked before the click event is dispatched, so that event listeners can check the checked value. // However, the value has to be restored if preventDefault() is called on the click event. @@ -1380,6 +1378,10 @@ export default class HTMLInputElement extends HTMLElement { if (type === 'checkbox' || type === 'radio') { previousCheckedValue = this.checked; this.#setChecked(type === 'checkbox' ? !previousCheckedValue : true); + if (type === 'checkbox') { + previousIndeterminateValue = this[PropertySymbol.indeterminate]; + this[PropertySymbol.indeterminate] = false; + } } // Dispatches the event @@ -1397,6 +1399,7 @@ export default class HTMLInputElement extends HTMLElement { // Restore checked state if preventDefault() is triggered inside a listener of the click event. if (previousCheckedValue !== null) { this.#setChecked(previousCheckedValue); + this[PropertySymbol.indeterminate] = previousIndeterminateValue; } } else { const type = this.type; diff --git a/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts b/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts index 67825885b..cc87ea96e 100644 --- a/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts +++ b/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts @@ -702,14 +702,7 @@ describe('HTMLInputElement', () => { }); }); - for (const property of [ - 'disabled', - 'autofocus', - 'required', - 'indeterminate', - 'multiple', - 'readOnly' - ]) { + for (const property of ['disabled', 'autofocus', 'required', 'multiple', 'readOnly']) { describe(`get ${property}()`, () => { it('Returns attribute value.', () => { expect(element[property]).toBe(false); @@ -893,6 +886,23 @@ describe('HTMLInputElement', () => { }); }); + describe('get indeterminate()', () => { + it('Returns indeterminate value.', () => { + element.type = 'checkbox'; + expect(element.indeterminate).toBe(false); + expect(element.hasAttribute('indeterminate')).toBe(false); + }); + }); + + describe('set indeterminate()', () => { + it('Sets indeterminate value.', () => { + element.type = 'checkbox'; + element.indeterminate = true; + expect(element.indeterminate).toBe(true); + expect(element.hasAttribute('indeterminate')).toBe(false); + }); + }); + describe('get size()', () => { it('Returns attribute value.', () => { expect(element.size).toBe(20); @@ -1296,6 +1306,26 @@ describe('HTMLInputElement', () => { expect(element.checked).toBe(true); }); + it('Switch "checked" to "true" or "false" and "indeterminate" to "false" if type is "checkbox" and "indeterminate" is "true" and is a "click" event.', () => { + element.type = 'checkbox'; + element.indeterminate = true; + + // "input" and "change" events should only be triggered if connected to DOM + document.body.appendChild(element); + + element.dispatchEvent(new MouseEvent('click')); + + expect(element.checked).toBe(true); + expect(element.indeterminate).toBe(false); + + element.indeterminate = true; + + element.dispatchEvent(new MouseEvent('click')); + + expect(element.checked).toBe(false); + expect(element.indeterminate).toBe(false); + }); + it('Sets "checked" to "true" if type is "radio" and is a "click" event.', () => { let isInputTriggered = false; let isChangeTriggered = false;