Skip to content

Commit

Permalink
feat(mock-doc): assignednodes and assignedElements (#6108)
Browse files Browse the repository at this point in the history
* feat(mock-doc): slot `assignedNodes` and `assignedElements`

* chore: stoopid

* chore: revert

* chore: comments

* chore: format

* chore: improve test

* chore: tidy

---------

Co-authored-by: John Jenkins <john.jenkins@nanoporetech.com>
  • Loading branch information
johnjenkins and John Jenkins authored Jan 21, 2025
1 parent 2df56c4 commit 777aafd
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 4 deletions.
97 changes: 93 additions & 4 deletions src/mock-doc/element.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { cloneAttributes } from './attribute';
import { NODE_TYPES } from './constants';
import { getStyleElementText, MockCSSStyleSheet, setStyleElementText } from './css-style-sheet';
import { createCustomElement } from './custom-element-registry';
import { MockDocumentFragment } from './document-fragment';
import { MockElement, MockHTMLElement } from './node';
import { MockElement, MockHTMLElement, MockNode } from './node';

export function createElement(ownerDocument: any, tagName: string): any {
if (typeof tagName !== 'string' || tagName === '' || !/^[a-z0-9-_:]+$/i.test(tagName)) {
Expand Down Expand Up @@ -41,6 +42,12 @@ export function createElement(ownerDocument: any, tagName: string): any {
case 'script':
return new MockScriptElement(ownerDocument);

case 'slot':
return new MockSlotElement(ownerDocument);

case 'slot-fb':
return new MockHTMLElement(ownerDocument, tagName);

case 'style':
return new MockStyleElement(ownerDocument);

Expand All @@ -52,9 +59,6 @@ export function createElement(ownerDocument: any, tagName: string): any {

case 'ul':
return new MockUListElement(ownerDocument);

case 'slot-fb':
return new MockHTMLElement(ownerDocument, tagName);
}

if (ownerDocument != null && tagName.includes('-')) {
Expand Down Expand Up @@ -514,6 +518,91 @@ export class MockUListElement extends MockHTMLElement {
}
}

export class MockSlotElement extends MockHTMLElement {
constructor(ownerDocument: any) {
super(ownerDocument, 'slot');
}

assignedNodes(opts?: { flatten: boolean }): (MockNode | Node)[] {
let nodesToReturn: (MockNode | Node)[] = [];

const ownerHost = (this.getRootNode() as any).host as MockElement;
if (!ownerHost) return nodesToReturn;

if (ownerHost.childNodes.length) {
// try to find lightDOM nodes matching this slot's name (or lack of)
if ((this as any).name) {
nodesToReturn = ownerHost.childNodes.filter(
(n) =>
n.nodeType === NODE_TYPES.ELEMENT_NODE && (n as MockElement).getAttribute('slot') === (this as any).name,
);
} else {
// find elements that do not have a slot attribute or
// any other type of node
nodesToReturn = ownerHost.childNodes.filter(
(n) =>
(n.nodeType === NODE_TYPES.ELEMENT_NODE && !(n as MockElement).getAttribute('slot')) ||
n.nodeType !== NODE_TYPES.ELEMENT_NODE,
);
}
if (nodesToReturn.length) return nodesToReturn;
}

// no flatten option? Return whatever's in this slot (without nested slots)
if (!opts?.flatten) return this.childNodes.filter((n) => !(n instanceof MockSlotElement));

// flatten option? Return all nodes in this slot (including anything within nested slots)
return this.childNodes.reduce(
(acc, node) => {
if (node instanceof MockSlotElement) {
acc.push(...node.assignedNodes(opts));
} else {
acc.push(node);
}
return acc;
},
[] as (MockNode | Node)[],
);
}

assignedElements(opts?: { flatten: boolean }): (Element | MockHTMLElement)[] {
let elesToReturn: (Element | MockHTMLElement)[] = [];

const ownerHost = (this.getRootNode() as any).host as MockElement;
if (!ownerHost) return elesToReturn;

if (ownerHost.children.length) {
// try to find lightDOM elements matching this slot's name (or lack of)
if ((this as any).name) {
elesToReturn = ownerHost.children.filter((n) => (n as MockElement).getAttribute('slot') == (this as any).name);
} else {
elesToReturn = ownerHost.children.filter((n) => !(n as MockElement).getAttribute('slot'));
}
if (elesToReturn.length) return elesToReturn;
}

// no flatten option? Return whatever elements are in this slot (without nested slots)
if (!opts?.flatten) return this.children.filter((n) => !(n instanceof MockSlotElement));

// flatten option? Return all elements in this slot (including anything within nested slots)
return this.children.reduce(
(acc, node) => {
if (node instanceof MockSlotElement) {
acc.push(...node.assignedElements(opts));
} else {
acc.push(node);
}
return acc;
},
[] as (MockElement | Element)[],
);
}
}

patchPropAttributes(MockSlotElement.prototype, {
name: String,
});

type CanvasContext = '2d' | 'webgl' | 'webgl2' | 'bitmaprenderer';
export class CanvasRenderingContext {
context: CanvasContext;
Expand Down
84 changes: 84 additions & 0 deletions src/mock-doc/test/element.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,4 +521,88 @@ describe('element', () => {
expect(ctx).toBeDefined();
expect(ctx.toDataURL()).toBe('data:,');
});

describe('slot', () => {
it('returns no nodes via `assignedNodes` / `assignedElements` when not within a custom element', () => {
const slot = doc.createElement('slot');
slot.innerHTML = 'invisible <div>Something</div> <!-- not here -->';
expect(slot.assignedNodes().length).toEqual(0);
expect(slot.assignedElements().length).toEqual(0);
});

it('returns correct nodes with `assignedNodes`', () => {
const elm = doc.createElement('cmp-a');
const shadowRoot = elm.attachShadow({ mode: 'open' });
const slot = doc.createElement('slot');
shadowRoot.appendChild(slot);
slot.innerHTML = '<slot name="nested-slot">Fallback content</slot>';

elm.innerHTML = `
text node
<div>light dom</div>
<!-- comment node -->
<div slot="nested-slot">nested slot</div>
`;

expect(slot.assignedNodes().length).toEqual(5);
expect(slot.assignedNodes()[0].textContent.trim()).toEqual('text node');
expect(slot.assignedNodes()[1].textContent.trim()).toEqual('light dom');
expect(slot.assignedNodes()[3].textContent.trim()).toEqual('comment node');
expect(slot.assignedNodes()[4].textContent.trim()).toEqual('');
expect(slot.assignedNodes({ flatten: true }).length).toEqual(5);

elm.innerHTML = '<div slot="nested-slot">nested slot</div>';

expect(slot.assignedNodes().length).toEqual(0);
expect(slot.assignedNodes({ flatten: true }).length).toEqual(1);
expect(slot.assignedNodes({ flatten: true })[0].textContent.trim()).toEqual('nested slot');

elm.innerHTML = 'text node <div slot="nested-slot">nested slot</div>';

expect(slot.assignedNodes().length).toEqual(1);
expect(slot.assignedNodes({ flatten: true }).length).toEqual(1);
expect(slot.assignedNodes()[0].textContent.trim()).toEqual('text node');

elm.innerHTML = '';
expect(slot.assignedNodes().length).toEqual(0);
expect(slot.assignedNodes({ flatten: true }).length).toEqual(1);
expect(slot.assignedNodes({ flatten: true })[0].textContent.trim()).toEqual('Fallback content');
});

it('returns correct elements with `assignedElements`', () => {
const elm = doc.createElement('cmp-a');
const shadowRoot = elm.attachShadow({ mode: 'open' });
const slot = doc.createElement('slot');
shadowRoot.appendChild(slot);
slot.innerHTML = '<slot name="nested-slot"><div>Fallback content</div></slot>';

elm.innerHTML = `
text node
<div>light dom</div>
<!-- comment node -->
<div slot="nested-slot">nested slot</div>
`;

expect(slot.assignedElements().length).toEqual(1);
expect(slot.assignedElements()[0].textContent.trim()).toEqual('light dom');
expect(slot.assignedElements({ flatten: true }).length).toEqual(1);

elm.innerHTML = '<div slot="nested-slot">nested slot</div>';

expect(slot.assignedElements().length).toEqual(0);
expect(slot.assignedElements({ flatten: true }).length).toEqual(1);
expect(slot.assignedElements({ flatten: true })[0].textContent.trim()).toEqual('nested slot');

elm.innerHTML = 'text node <div slot="nested-slot">nested slot</div>';

expect(slot.assignedElements().length).toEqual(0);
expect(slot.assignedElements({ flatten: true }).length).toEqual(1);
expect(slot.assignedElements({ flatten: true })[0].textContent.trim()).toEqual('nested slot');

elm.innerHTML = '';
expect(slot.assignedElements().length).toEqual(0);
expect(slot.assignedElements({ flatten: true }).length).toEqual(1);
expect(slot.assignedElements({ flatten: true })[0].textContent.trim()).toEqual('Fallback content');
});
});
});

0 comments on commit 777aafd

Please sign in to comment.