Skip to content

Commit

Permalink
chore: [#1332] Continues on implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
capricorn86 committed Apr 9, 2024
1 parent 5f29bdd commit 614bd9d
Show file tree
Hide file tree
Showing 14 changed files with 245 additions and 15 deletions.
1 change: 1 addition & 0 deletions packages/happy-dom/src/PropertySymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,4 @@ export const tracks = Symbol('tracks');
export const constraints = Symbol('constraints');
export const capabilities = Symbol('capabilities');
export const settings = Symbol('settings');
export const dataListNode = Symbol('dataListNode');
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,22 @@ import HTMLElement from '../html-element/HTMLElement.js';
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDataElement
*/
export default class HTMLDataElement extends HTMLElement {}
export default class HTMLDataElement extends HTMLElement {
/**
* Returns value.
*
* @returns Value.
*/
public get value(): string {
return this.getAttribute('value') || '';
}

/**
* Sets value.
*
* @param value Value.
*/
public set value(value: string) {
this.setAttribute('value', value);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
import HTMLElement from '../html-element/HTMLElement.js';
import * as PropertySymbol from '../../PropertySymbol.js';
import HTMLCollection from '../element/HTMLCollection.js';
import HTMLOptionElement from '../html-option-element/HTMLOptionElement.js';
import Node from '../node/Node.js';

/**
* HTMLDataListElement
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDataListElement
*/
export default class HTMLDataListElement extends HTMLElement {}
export default class HTMLDataListElement extends HTMLElement {
public [PropertySymbol.options] = new HTMLCollection<HTMLOptionElement>();
public [PropertySymbol.dataListNode]: Node = this;

/**
* Returns options.
*
* @returns Options.
*/
public get options(): HTMLCollection<HTMLOptionElement> {
return this[PropertySymbol.options];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import NamedNodeMap from '../../named-node-map/NamedNodeMap.js';
import HTMLInputElementNamedNodeMap from './HTMLInputElementNamedNodeMap.js';
import PointerEvent from '../../event/events/PointerEvent.js';
import { URL } from 'url';
import HTMLDataListElement from '../html-data-list-element/HTMLDataListElement.js';
import Document from '../document/Document.js';
import ShadowRoot from '../shadow-root/ShadowRoot.js';

/**
* HTML Input Element.
Expand Down Expand Up @@ -1102,6 +1105,21 @@ export default class HTMLInputElement extends HTMLElement {
return HTMLLabelElementUtility.getAssociatedLabelElements(this);
}

/**
* Returns associated datalist element.
*
* @returns Data list element.
*/
public get list(): HTMLDataListElement | null {
const id = this.getAttribute('list');
if (!id) {
return null;
}
const rootNode =
<Document | ShadowRoot>this[PropertySymbol.rootNode] || this[PropertySymbol.ownerDocument];
return <HTMLDataListElement | null>rootNode.querySelector(`datalist#${id}`);
}

/**
* Sets validation message.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ export default class HTMLLabelElementUtility {
const id = element.id;
let labels: NodeList<HTMLLabelElement>;
if (id) {
const rootNode = <Document | ShadowRoot>element.getRootNode();
const rootNode =
<Document | ShadowRoot>element[PropertySymbol.rootNode] ||
element[PropertySymbol.ownerDocument];
labels = <NodeList<HTMLLabelElement>>rootNode.querySelectorAll(`label[for="${id}"]`);
} else {
labels = new NodeList<HTMLLabelElement>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import NamedNodeMap from '../../named-node-map/NamedNodeMap.js';
import * as PropertySymbol from '../../PropertySymbol.js';
import HTMLDataListElement from '../html-data-list-element/HTMLDataListElement.js';
import HTMLElement from '../html-element/HTMLElement.js';
import HTMLFormElement from '../html-form-element/HTMLFormElement.js';
import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js';
Expand Down Expand Up @@ -127,15 +128,39 @@ export default class HTMLOptionElement extends HTMLElement {
*/
public override [PropertySymbol.connectToNode](parentNode: Node = null): void {
const oldSelectNode = <HTMLSelectElement>this[PropertySymbol.selectNode];
const oldDataListNode = <HTMLDataListElement>this[PropertySymbol.dataListNode];

super[PropertySymbol.connectToNode](parentNode);

if (oldSelectNode !== this[PropertySymbol.selectNode]) {
const selectNode = <HTMLSelectElement>this[PropertySymbol.selectNode];

if (oldSelectNode !== selectNode) {
if (oldSelectNode) {
oldSelectNode[PropertySymbol.updateOptionItems]();
}
if (this[PropertySymbol.selectNode]) {
(<HTMLSelectElement>this[PropertySymbol.selectNode])[PropertySymbol.updateOptionItems]();
if (selectNode) {
selectNode[PropertySymbol.updateOptionItems]();
}
}

const dataListNode = <HTMLDataListElement>this[PropertySymbol.dataListNode];

if (oldDataListNode !== dataListNode) {
const name = this.getAttribute('name');
const id = this.id;
if (oldDataListNode) {
const index = oldDataListNode[PropertySymbol.options].indexOf(this);
if (index !== -1) {
oldDataListNode[PropertySymbol.options].splice(index, 1);
}

oldDataListNode[PropertySymbol.options][PropertySymbol.removeNamedItem](this, name);
oldDataListNode[PropertySymbol.options][PropertySymbol.removeNamedItem](this, id);
}
if (dataListNode) {
dataListNode[PropertySymbol.options].push(this);
dataListNode[PropertySymbol.options][PropertySymbol.appendNamedItem](this, name);
dataListNode[PropertySymbol.options][PropertySymbol.appendNamedItem](this, id);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default class HTMLSlotElement extends HTMLElement {
* @returns Nodes.
*/
public assignedNodes(options?: { flatten?: boolean }): Node[] {
const host = (<ShadowRoot>this.getRootNode())?.host;
const host = (<ShadowRoot>this[PropertySymbol.rootNode])?.host;

// TODO: Add support for options.flatten. We need to find an example of how it is expected to work before it can be implemented.

Expand Down
9 changes: 9 additions & 0 deletions packages/happy-dom/src/nodes/node/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export default class Node extends EventTarget {
public [PropertySymbol.nodeType]: NodeTypeEnum;
public [PropertySymbol.rootNode]: Node = null;
public [PropertySymbol.formNode]: Node = null;
public [PropertySymbol.dataListNode]: Node = null;
public [PropertySymbol.selectNode]: Node = null;
public [PropertySymbol.textAreaNode]: Node = null;
public [PropertySymbol.observers]: MutationListener[] = [];
Expand Down Expand Up @@ -514,6 +515,7 @@ export default class Node extends EventTarget {
public [PropertySymbol.connectToNode](parentNode: Node = null): void {
const isConnected = !!parentNode && parentNode[PropertySymbol.isConnected];
const formNode = (<Node>this)[PropertySymbol.formNode];
const dataListNode = (<Node>this)[PropertySymbol.dataListNode];
const selectNode = (<Node>this)[PropertySymbol.selectNode];
const textAreaNode = (<Node>this)[PropertySymbol.textAreaNode];

Expand All @@ -528,6 +530,12 @@ export default class Node extends EventTarget {
: null;
}

if (this['tagName'] !== 'DATALIST') {
(<Node>this)[PropertySymbol.dataListNode] = parentNode
? (<Node>parentNode)[PropertySymbol.dataListNode]
: null;
}

if (this['tagName'] !== 'SELECT') {
(<Node>this)[PropertySymbol.selectNode] = parentNode
? (<Node>parentNode)[PropertySymbol.selectNode]
Expand Down Expand Up @@ -567,6 +575,7 @@ export default class Node extends EventTarget {
}
} else if (
formNode !== this[PropertySymbol.formNode] ||
dataListNode !== this[PropertySymbol.dataListNode] ||
selectNode !== this[PropertySymbol.selectNode] ||
textAreaNode !== this[PropertySymbol.textAreaNode]
) {
Expand Down
17 changes: 9 additions & 8 deletions packages/happy-dom/test/nodes/element/HTMLCollection.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Window from '../../../src/window/Window.js';
import Document from '../../../src/nodes/document/Document.js';
import { beforeEach, afterEach, describe, it, expect } from 'vitest';
import { beforeEach, describe, it, expect } from 'vitest';
import HTMLElement from '../../../src/nodes/html-element/HTMLElement.js';

describe('HTMLCollection', () => {
let window: Window;
Expand Down Expand Up @@ -76,10 +77,10 @@ describe('HTMLCollection', () => {
it('Supports attributes only consisting of numbers.', () => {
const div = document.createElement('div');
div.innerHTML = `<div name="container1" class="container1"></div><div name="container2" class="container2"></div><div name="0" class="container3"></div><div name="1" class="container4"></div>`;
const container1 = div.querySelector('.container1');
const container2 = div.querySelector('.container2');
const container3 = div.querySelector('.container3');
const container4 = div.querySelector('.container4');
const container1 = <HTMLElement>div.querySelector('.container1');
const container2 = <HTMLElement>div.querySelector('.container2');
const container3 = <HTMLElement>div.querySelector('.container3');
const container4 = <HTMLElement>div.querySelector('.container4');

expect(div.children.length).toBe(4);
expect(div.children[0] === container1).toBe(true);
Expand Down Expand Up @@ -118,9 +119,9 @@ describe('HTMLCollection', () => {
it('Supports attributes that has the same name as properties and methods of the HTMLCollection class.', () => {
const div = document.createElement('div');
div.innerHTML = `<div name="length" class="container1"></div><div name="namedItem" class="container2"></div><div name="push" class="container3"></div>`;
const container1 = div.querySelector('.container1');
const container2 = div.querySelector('.container2');
const container3 = div.querySelector('.container3');
const container1 = <HTMLElement>div.querySelector('.container1');
const container2 = <HTMLElement>div.querySelector('.container2');
const container3 = <HTMLElement>div.querySelector('.container3');

expect(div.children.length).toBe(3);
expect(div.children[0] === container1).toBe(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,19 @@ describe('HTMLDataElement', () => {
expect(element instanceof HTMLDataElement).toBe(true);
});
});

describe('get value()', () => {
it('Should return value', () => {
expect(element.value).toBe('');
element.setAttribute('value', 'test');
expect(element.value).toBe('test');
});
});

describe('set value()', () => {
it('Should set value', () => {
element.value = 'test';
expect(element.getAttribute('value')).toBe('test');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import HTMLDataListElement from '../../../src/nodes/html-data-list-element/HTMLD
import Window from '../../../src/window/Window.js';
import Document from '../../../src/nodes/document/Document.js';
import { beforeEach, describe, it, expect } from 'vitest';
import HTMLCollection from '../../../src/nodes/element/HTMLCollection.js';

describe('HTMLDataListElement', () => {
let window: Window;
Expand All @@ -19,4 +20,60 @@ describe('HTMLDataListElement', () => {
expect(element instanceof HTMLDataListElement).toBe(true);
});
});

describe('get options()', () => {
it('Should return options', () => {
expect(element.options).toBeInstanceOf(HTMLCollection);
expect(element.options.length).toBe(0);

const option1 = document.createElement('option');
const option2 = document.createElement('option');
const option3 = document.createElement('option');

option3.setAttribute('id', 'option3_id');
option3.setAttribute('name', 'option3_name');

element.appendChild(option1);
element.appendChild(option2);
element.appendChild(option3);

expect(element.options.length).toBe(3);

expect(element.options[0]).toBe(option1);
expect(element.options[1]).toBe(option2);
expect(element.options[2]).toBe(option3);

expect(element.options['option3_id']).toBe(option3);
expect(element.options['option3_name']).toBe(option3);

element.removeChild(option2);

expect(element.options.length).toBe(2);

expect(element.options[0]).toBe(option1);
expect(element.options[1]).toBe(option3);

expect(element.options['option3_id']).toBe(option3);
expect(element.options['option3_name']).toBe(option3);

element.removeChild(option3);

expect(element.options.length).toBe(1);

expect(element.options[0]).toBe(option1);

expect(element.options['option3_id']).toBe(undefined);
expect(element.options['option3_name']).toBe(undefined);

element.appendChild(option3);

expect(element.options.length).toBe(2);

expect(element.options[0]).toBe(option1);
expect(element.options[1]).toBe(option3);

expect(element.options['option3_id']).toBe(option3);
expect(element.options['option3_name']).toBe(option3);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,51 @@ describe('HTMLInputElement', () => {
});
});

describe('get list()', () => {
it('Returns null if the attribute "list" is not set.', () => {
expect(element.list).toBe(null);
});

it('Returns null if no associated list element matches the attribuge "list".', () => {
element.setAttribute('list', 'datalist');
expect(element.list).toBe(null);
});

it('Returns the associated datalist element.', () => {
const datalist = document.createElement('datalist');
datalist.id = 'list_id';
document.body.appendChild(datalist);
element.setAttribute('list', 'list_id');
expect(element.list).toBe(datalist);
});

it('Finds datalist inside a shadowroot.', () => {
/* eslint-disable-next-line jsdoc/require-jsdoc */
class MyComponent extends window.HTMLElement {
/* eslint-disable-next-line jsdoc/require-jsdoc */
constructor() {
super();
this.attachShadow({ mode: 'open' });
if (this.shadowRoot) {
this.shadowRoot.innerHTML = `
<datalist id="list_id">
<option value="1">
<option value="2">
</datalist>
<input list="list_id">
`;
}
}
}
window.customElements.define('my-component', MyComponent);
const component = document.createElement('my-component');
document.body.appendChild(component);
const input = component.shadowRoot?.querySelector('input');
const list = component.shadowRoot?.querySelector('datalist');
expect(input?.list === list).toBe(true);
});
});

describe('set selectionEnd()', () => {
it('Sets the value to the length of the property "value" if it is out of range.', () => {
element.setAttribute('value', 'TEST_VALUE');
Expand Down
5 changes: 5 additions & 0 deletions packages/happy-dom/test/nodes/node/Node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,11 @@ describe('Node', () => {
it('Returns Document when called on Document', () => {
expect(document.getRootNode() === document).toBe(true);
});

it('Returns self when element is not connected to DOM', () => {
const element = document.createElement('div');
expect(element.getRootNode() === element).toBe(true);
});
});

describe('cloneNode()', () => {
Expand Down

0 comments on commit 614bd9d

Please sign in to comment.