Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/return-query-selector-all-node-list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@remote-dom/polyfill': patch
---

Return a NodeList-compatible collection from `querySelectorAll()`.
4 changes: 2 additions & 2 deletions packages/polyfill/source/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,15 @@ export class Element extends ParentNode {
return findElementsByTagName(this, qualifiedName);
}

get firstElementChild() {
get firstElementChild(): Element | null {
return this.children[0] ?? null;
}

getElementsByClassName(classNames: string) {
return findElementsByClassName(this, classNames);
}

get lastElementChild() {
get lastElementChild(): Element | null {
return this.children[this.children.length - 1] ?? null;
}

Expand Down
6 changes: 4 additions & 2 deletions packages/polyfill/source/NodeList.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export class NodeList extends Array {
item(index: number) {
import type {Node} from './Node.ts';

export class NodeList<Item extends Node = Node> extends Array<Item> {
item(index: number): Item | null {
return this[index] ?? null;
}
}
13 changes: 7 additions & 6 deletions packages/polyfill/source/ParentNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import {
IS_CONNECTED,
} from './constants.ts';
import type {Node} from './Node.ts';
import type {Element} from './Element.ts';
import {ChildNode, toNode} from './ChildNode.ts';
import {NodeList} from './NodeList.ts';
import {querySelectorAll, querySelector} from './selectors.ts';
import {selfAndDescendants} from './shared.ts';
import {isElementNode, selfAndDescendants} from './shared.ts';
import {
childListObserversActive,
mutationNodeList,
Expand Down Expand Up @@ -46,8 +47,8 @@ interface PreparedInsertionRoot {
}

export class ParentNode extends ChildNode {
readonly childNodes = new NodeList();
readonly children = new NodeList();
readonly childNodes = new NodeList<Node>();
readonly children = new NodeList<Element>();

appendChild<T extends Node>(child: T) {
return performWithCustomElementReactions(() => {
Expand Down Expand Up @@ -286,7 +287,7 @@ export class ParentNode extends ChildNode {
const childNodesIndex = this.childNodes.indexOf(child);
this.childNodes.splice(childNodesIndex, 1);

if (child.nodeType === NODE_TYPE_ELEMENT) {
if (isElementNode(child)) {
this.children.splice(this.children.indexOf(child), 1);
}

Expand Down Expand Up @@ -319,7 +320,7 @@ export class ParentNode extends ChildNode {
}
}

const isElement = child.nodeType === NODE_TYPE_ELEMENT;
const isElement = isElementNode(child);
child[PARENT] = this;
child[OWNER_DOCUMENT] = this[OWNER_DOCUMENT];

Expand All @@ -330,7 +331,7 @@ export class ParentNode extends ChildNode {

if (isElement) {
let reference: Node | null = before;
while (reference && reference.nodeType !== NODE_TYPE_ELEMENT) {
while (reference && !isElementNode(reference)) {
reference = reference[NEXT];
}
if (reference) {
Expand Down
8 changes: 6 additions & 2 deletions packages/polyfill/source/getElementsByClassName.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import type {ParentNode} from './ParentNode.ts';
import type {Element} from './Element.ts';
import {NodeList} from './NodeList.ts';
import {descendants, isElementNode} from './shared.ts';

export function getElementsByClassName(node: ParentNode, classNames: string) {
export function getElementsByClassName(
node: ParentNode,
classNames: string,
): NodeList<Element> {
const names = [...new Set(String(classNames).split(/[\t\n\f\r ]+/))].filter(
Boolean,
);
const matches = new NodeList();
const matches = new NodeList<Element>();

if (names.length === 0) return matches;

Expand Down
5 changes: 3 additions & 2 deletions packages/polyfill/source/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
asciiLowercase,
} from './constants.ts';
import {isElementNode} from './shared.ts';
import {NodeList} from './NodeList.ts';

import type {Node} from './Node.ts';
import type {Element} from './Element.ts';
Expand Down Expand Up @@ -111,12 +112,12 @@ export function querySelector(
export function querySelectorAll(
within: ParentNode,
selector: string | Matcher[],
): Element[] {
): NodeList<Element> {
const parts: Part[] =
typeof selector === 'string'
? parseSelector(selector)
: [{combinator: COMBINATOR_INNER, matchers: selector}];
const results: Element[] = [];
const results = new NodeList<Element>();

const child = within[CHILD];
if (child && parts[0]!.matchers.length) {
Expand Down
46 changes: 45 additions & 1 deletion packages/polyfill/source/tests/selectors.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {Window} from '../index.ts';
import {NodeList} from '../NodeList.ts';
import type {Element as PolyfillElement} from '../Element.ts';
import {
MATCHER_CLASS,
MATCHER_ELEMENT,
Expand All @@ -17,7 +19,7 @@ const MatcherType = {
Class: MATCHER_CLASS,
} as const;

import {describe, it, expect, beforeEach} from 'vitest';
import {describe, it, expect, expectTypeOf, beforeEach} from 'vitest';

describe('selector parsing and matching', () => {
beforeEach(() => {
Expand Down Expand Up @@ -226,6 +228,16 @@ describe('selector parsing and matching', () => {
container.querySelector('.highlight')!.setAttribute('data-label', 'a)b');
});

it('types instance query results as elements', () => {
const matches = new Window().document
.createElement('div')
.querySelectorAll('p');

expectTypeOf(matches).toEqualTypeOf<NodeList<PolyfillElement>>();
expectTypeOf(matches[0]!).toEqualTypeOf<PolyfillElement>();
expectTypeOf(matches.item(0)).toEqualTypeOf<PolyfillElement | null>();
});

it('selects HTML element names case-insensitively', () => {
const articles = container.querySelectorAll('ARTICLE');
expect(articles).toHaveLength(1);
Expand All @@ -235,6 +247,28 @@ describe('selector parsing and matching', () => {
expect(paragraphs).toHaveLength(3);
});

it('returns a static NodeList-compatible collection', () => {
const matches = container.querySelectorAll('.text');
const text = [...matches].map((element) => element.textContent?.trim());
const visited: string[] = [];

matches.forEach((element) => visited.push(element.textContent?.trim()!));
container
.appendChild(document.createElement('p'))
.setAttribute('class', 'text');

expect(container.querySelectorAll('.text')).toHaveLength(4);
expect(matches).toBeInstanceOf(NodeList);
expect(matches).toHaveLength(3);
expect(matches.item(0)).toBe(matches[0]);
expect(matches.item(-1)).toBeNull();
expect(matches.item(matches.length)).toBeNull();
expect(
[...matches].map((element) => element.textContent?.trim()),
).toEqual(text);
expect(visited).toEqual(text);
});

it('does not fold stored createElementNS HTML names', () => {
const uppercase = document.createElementNS(
'http://www.w3.org/1999/xhtml',
Expand Down Expand Up @@ -550,6 +584,16 @@ describe('selector parsing and matching', () => {
expect(main?.tagName.toLowerCase()).toBe('article');
});

it('types standalone query results as elements', () => {
const matches = querySelectorAll(asPolyfill(container), [
{type: MatcherType.Element, name: 'p'},
]);

expectTypeOf(matches).toEqualTypeOf<NodeList<PolyfillElement>>();
expectTypeOf(matches[0]!).toEqualTypeOf<PolyfillElement>();
expectTypeOf(matches.item(0)).toEqualTypeOf<PolyfillElement | null>();
});

it('selects by element matcher without parsing', () => {
const paragraphs = querySelectorAll(asPolyfill(container), [
{type: MatcherType.Element, name: 'p'},
Expand Down
Loading