Skip to content

Commit

Permalink
fix: [#1463] Element.contentEditable should be synced with the "conte…
Browse files Browse the repository at this point in the history
…nteditable" attribute (#1757)
  • Loading branch information
karpiuMG authored Mar 6, 2025
1 parent b1620c5 commit 1417bad
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 14 deletions.
2 changes: 0 additions & 2 deletions packages/happy-dom/src/PropertySymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,6 @@ export const attributesProxy = Symbol('attributesProxy');
export const namespaceURI = Symbol('namespaceURI');
export const accessKey = Symbol('accessKey');
export const accessKeyLabel = Symbol('accessKeyLabel');
export const contentEditable = Symbol('contentEditable');
export const isContentEditable = Symbol('isContentEditable');
export const offsetHeight = Symbol('offsetHeight');
export const offsetWidth = Symbol('offsetWidth');
export const offsetLeft = Symbol('offsetLeft');
Expand Down
42 changes: 35 additions & 7 deletions packages/happy-dom/src/nodes/html-element/HTMLElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ export default class HTMLElement extends Element {

// Internal properties
public [PropertySymbol.accessKey] = '';
public [PropertySymbol.contentEditable] = 'inherit';
public [PropertySymbol.isContentEditable] = false;
public [PropertySymbol.offsetHeight] = 0;
public [PropertySymbol.offsetWidth] = 0;
public [PropertySymbol.offsetLeft] = 0;
Expand Down Expand Up @@ -500,7 +498,16 @@ export default class HTMLElement extends Element {
* @returns Content editable.
*/
public get contentEditable(): string {
return this[PropertySymbol.contentEditable];
const contentEditable = String(this.getAttribute('contentEditable')).toLowerCase();

switch (contentEditable) {
case 'false':
case 'true':
case 'plaintext-only':
return contentEditable;
default:
return 'inherit';
}
}

/**
Expand All @@ -509,7 +516,20 @@ export default class HTMLElement extends Element {
* @param contentEditable Content editable.
*/
public set contentEditable(contentEditable: string) {
this[PropertySymbol.contentEditable] = contentEditable;
contentEditable = String(contentEditable).toLowerCase();

switch (contentEditable) {
case 'false':
case 'true':
case 'plaintext-only':
case 'inherit':
this.setAttribute('contentEditable', contentEditable);
break;
default:
throw new this[PropertySymbol.window].SyntaxError(
`Failed to set the 'contentEditable' property on 'HTMLElement': The value provided ('${contentEditable}') is not one of 'true', 'false', 'plaintext-only', or 'inherit'.`
);
}
}

/**
Expand All @@ -518,7 +538,17 @@ export default class HTMLElement extends Element {
* @returns Is content editable.
*/
public get isContentEditable(): boolean {
return this[PropertySymbol.isContentEditable];
const contentEditable = this.contentEditable;

if (contentEditable === 'true' || contentEditable === 'plaintext-only') {
return true;
}

if (contentEditable === 'inherit') {
return (<HTMLElement>this[PropertySymbol.parentNode])?.isContentEditable ?? false;
}

return false;
}

/**
Expand Down Expand Up @@ -944,8 +974,6 @@ export default class HTMLElement extends Element {
const clone = <HTMLElement>super[PropertySymbol.cloneNode](deep);

clone[PropertySymbol.accessKey] = this[PropertySymbol.accessKey];
clone[PropertySymbol.contentEditable] = this[PropertySymbol.contentEditable];
clone[PropertySymbol.isContentEditable] = this[PropertySymbol.isContentEditable];

return clone;
}
Expand Down
88 changes: 83 additions & 5 deletions packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,18 +134,96 @@ describe('HTMLElement', () => {
});
}

describe('contentEditable', () => {
it('Returns "inherit".', () => {
const div = <HTMLElement>document.createElement('div');
describe('get contentEditable()', () => {
it('Returns "inherit" by default.', () => {
const div = document.createElement('div');
expect(div.contentEditable).toBe('inherit');
});

it('Returns the "contenteditable" attribute value.', () => {
for (const value of [
'true',
'false',
'plaintext-only',
'inherit',
'TRUE',
'FALSE',
'PLAINTEXT-ONLY',
'INHERIT'
]) {
const div = document.createElement('div');
div.setAttribute('contenteditable', value);
expect(div.contentEditable).toBe(value.toLowerCase());
}
});

it('Returns "inherit" when the "contenteditable" attribute is set to an invalid value.', () => {
const div = document.createElement('div');
div.setAttribute('contenteditable', 'invalid');
expect(div.contentEditable).toBe('inherit');
});
});

describe('isContentEditable', () => {
it('Returns "false".', () => {
describe('set contentEditable()', () => {
it('Sets the "contenteditable" attribute.', () => {
const div = document.createElement('div');
div.contentEditable = 'true';
expect(div.getAttribute('contenteditable')).toBe('true');
div.contentEditable = 'false';
expect(div.getAttribute('contenteditable')).toBe('false');
div.contentEditable = 'plaintext-only';
expect(div.getAttribute('contenteditable')).toBe('plaintext-only');
div.contentEditable = 'inherit';
expect(div.getAttribute('contenteditable')).toBe('inherit');
div.contentEditable = <string>(<unknown>true);
expect(div.getAttribute('contenteditable')).toBe('true');
div.contentEditable = <string>(<unknown>false);
expect(div.getAttribute('contenteditable')).toBe('false');
div.contentEditable = 'TRUE';
expect(div.getAttribute('contenteditable')).toBe('true');
div.contentEditable = 'FALSE';
expect(div.getAttribute('contenteditable')).toBe('false');
div.contentEditable = 'PLAINTEXT-ONLY';
expect(div.getAttribute('contenteditable')).toBe('plaintext-only');
div.contentEditable = 'INHERIT';
expect(div.getAttribute('contenteditable')).toBe('inherit');
});

it('Throws an error when an invalid value is provided.', () => {
const div = document.createElement('div');
expect(() => {
div.contentEditable = 'invalid';
}).toThrowError(
new SyntaxError(
`Failed to set the 'contentEditable' property on 'HTMLElement': The value provided ('invalid') is not one of 'true', 'false', 'plaintext-only', or 'inherit'.`
)
);
});
});

describe('get isContentEditable()', () => {
it('Returns "false" by default.', () => {
const div = <HTMLElement>document.createElement('div');
expect(div.isContentEditable).toBe(false);
});

it('Returns "true" when the "contenteditable" attribute is set to "true" or "plaintext-only".', () => {
const div = <HTMLElement>document.createElement('div');
div.setAttribute('contenteditable', 'true');
expect(div.isContentEditable).toBe(true);
div.setAttribute('contenteditable', 'plaintext-only');
expect(div.isContentEditable).toBe(true);
div.setAttribute('contenteditable', 'false');
expect(div.isContentEditable).toBe(false);
});

it('Returns "true" when parent element is content editable and value is "inherit".', () => {
const parent = <HTMLElement>document.createElement('div');
const div = <HTMLElement>document.createElement('div');
parent.setAttribute('contenteditable', 'true');
parent.appendChild(div);
expect(div.isContentEditable).toBe(true);
});
});

describe('get tabIndex()', () => {
Expand Down

0 comments on commit 1417bad

Please sign in to comment.