Skip to content

Commit 57c988c

Browse files
authored
Merge branch 'main' into fix-providable-bugs
2 parents 4e25629 + 9f1fff8 commit 57c988c

5 files changed

Lines changed: 159 additions & 6 deletions

File tree

src/ability.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const createAbility = <TExtend, TClass extends CustomElementClass>(
99
const markers = abilityMarkers.get(Class)
1010
if (markers?.has(decorate as Decorator)) return Class as unknown as TExtend
1111
const NewClass = decorate(Class) as TExtend
12+
Object.defineProperty(NewClass, 'name', {value: Class.name})
1213
const newMarkers = new Set(markers)
1314
newMarkers.add(decorate as Decorator)
1415
abilityMarkers.set(NewClass as unknown as CustomElementClass, newMarkers)

src/controllable.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,6 @@ const internalsCalled = new WeakSet()
2020
export const controllable = createAbility(
2121
<T extends CustomElementClass>(Class: T): T & ControllableClass =>
2222
class extends Class {
23-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
24-
// @ts-ignore TypeScript doesn't like assigning static name
25-
static get name() {
26-
return Class.name
27-
}
28-
2923
// TS mandates Constructors that get mixins have `...args: any[]`
3024
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3125
constructor(...args: any[]) {

src/tag-observer.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
type Parse = (str: string) => string[]
2+
type Found = (el: Element, controller: Element | ShadowRoot, tag: string, ...parsed: string[]) => void
3+
4+
function closestShadowPiercing(el: Element, tagName: string): Element | null {
5+
const closest: Element | null = el.closest(tagName)
6+
if (!closest) {
7+
const shadow = el.getRootNode()
8+
if (!(shadow instanceof ShadowRoot)) return null
9+
return shadow.host.closest(tagName)
10+
}
11+
return closest
12+
}
13+
14+
export const tags = (el: Element, tag: string, parse: Parse) =>
15+
(el.getAttribute(tag) || '')
16+
.trim()
17+
.split(/\s+/g)
18+
.map((tagPart: string) => parse(tagPart))
19+
20+
const registry = new Map<string, [Parse, Found]>()
21+
const observer = new MutationObserver((mutations: MutationRecord[]) => {
22+
for (const mutation of mutations) {
23+
if (mutation.type === 'attributes') {
24+
const tag = mutation.attributeName!
25+
const el = mutation.target
26+
27+
if (el instanceof Element && registry.has(tag)) {
28+
const [parse, found] = registry.get(tag)!
29+
for (const [tagName, ...meta] of tags(el, tag, parse)) {
30+
const controller = closestShadowPiercing(el, tagName)
31+
if (controller) found(el, controller, tag, ...meta)
32+
}
33+
}
34+
} else if (mutation.addedNodes.length) {
35+
for (const node of mutation.addedNodes) {
36+
if (node instanceof Element) add(node)
37+
}
38+
}
39+
}
40+
})
41+
42+
export const register = (tag: string, parse: Parse, found: Found) => {
43+
if (registry.has(tag)) throw new Error('duplicate tag')
44+
registry.set(tag, [parse, found])
45+
}
46+
47+
export const add = (root: Element | ShadowRoot) => {
48+
for (const [tag, [parse, found]] of registry) {
49+
for (const el of root.querySelectorAll(`[${tag}]`)) {
50+
for (const [tagName, ...meta] of tags(el, tag, parse)) {
51+
const controller = closestShadowPiercing(el, tagName)
52+
if (controller) found(el, controller, tag, ...meta)
53+
}
54+
}
55+
if (root instanceof Element && root.hasAttribute(tag)) {
56+
for (const [tagName, ...meta] of tags(root, tag, parse)) {
57+
const controller = closestShadowPiercing(root, tagName)
58+
if (controller) found(root, controller, tag, ...meta)
59+
}
60+
}
61+
}
62+
observer.observe(root instanceof Element ? root.ownerDocument : root, {
63+
childList: true,
64+
subtree: true,
65+
attributeFilter: Array.from(registry.keys())
66+
})
67+
}

test/ability.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ describe('ability', () => {
6767
expect(DElement).to.have.property('prototype').instanceof(Element)
6868
})
6969

70+
it('retains original class name', () => {
71+
const DElement = fakeable(Element)
72+
const D2Element = otherfakeable(Element)
73+
expect(DElement).to.have.property('name', 'Element')
74+
expect(D2Element).to.have.property('name', 'Element')
75+
})
76+
7077
it('can be used in decorator position', async () => {
7178
@fakeable
7279
class DElement extends HTMLElement {}

test/tag-observer.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import {expect, fixture, html} from '@open-wc/testing'
2+
import {fake, match} from 'sinon'
3+
import {register, add} from '../src/tag-observer.js'
4+
5+
describe('tag observer', () => {
6+
let instance: HTMLElement
7+
beforeEach(async () => {
8+
instance = await fixture(html`<section>
9+
<div data-tagtest="section.a.b.c section.d.e.f doesntexist.g.h.i"></div>
10+
</section>`)
11+
})
12+
13+
it('can register new tag observers', () => {
14+
register('foo', fake(), fake())
15+
})
16+
17+
it('throws an error when registering a duplicate', () => {
18+
register('duplicate', fake(), fake())
19+
expect(() => register('duplicate', fake(), fake())).to.throw()
20+
})
21+
22+
describe('registered behaviour', () => {
23+
const testParse = fake(v => v.split('.'))
24+
const testFound = fake()
25+
register('data-tagtest', testParse, testFound)
26+
beforeEach(() => {
27+
add(instance)
28+
})
29+
30+
it('uses parse to extract tagged element values', () => {
31+
expect(testParse).to.be.calledWithExactly('section.a.b.c')
32+
expect(testParse).to.be.calledWithExactly('section.d.e.f')
33+
expect(testParse).to.be.calledWithExactly('doesntexist.g.h.i')
34+
})
35+
36+
it('calls found with el and args based from testParse', () => {
37+
const div = instance.querySelector('div')!
38+
expect(testFound).to.be.calledWithExactly(div, instance, 'data-tagtest', 'a', 'b', 'c')
39+
expect(testFound).to.be.calledWithExactly(div, instance, 'data-tagtest', 'd', 'e', 'f')
40+
expect(testFound).to.not.be.calledWithMatch(match.any, match.any, 'data-tagtest', 'g', 'h', 'i')
41+
})
42+
43+
it('calls found if added to a node that has tags on itself', () => {
44+
const div = document.createElement('div')
45+
div.setAttribute('data-tagtest', 'div.j.k.l')
46+
add(div)
47+
expect(testParse).to.be.calledWithExactly('div.j.k.l')
48+
expect(testFound).to.be.calledWithExactly(div, div, 'data-tagtest', 'j', 'k', 'l')
49+
})
50+
51+
it('pierces shadowdom boundaries to find nearest controller', () => {
52+
const div = document.createElement('div')
53+
const shadow = div.attachShadow({mode: 'open'})
54+
const span = document.createElement('span')
55+
span.setAttribute('data-tagtest', 'div.m.n.o')
56+
shadow.append(span)
57+
add(span)
58+
expect(testParse).to.be.calledWithExactly('div.m.n.o')
59+
expect(testFound).to.be.calledWithExactly(span, div, 'data-tagtest', 'm', 'n', 'o')
60+
})
61+
62+
it('queries inside shadowdom, and pierces to find nearest controller', () => {
63+
const div = document.createElement('div')
64+
const shadow = div.attachShadow({mode: 'open'})
65+
const span = document.createElement('span')
66+
span.setAttribute('data-tagtest', 'div.p.q.r')
67+
shadow.append(span)
68+
add(shadow)
69+
expect(testParse).to.be.calledWithExactly('div.p.q.r')
70+
expect(testFound).to.be.calledWithExactly(span, div, 'data-tagtest', 'p', 'q', 'r')
71+
})
72+
73+
describe('mutations', () => {
74+
it('calls parse+found on attributes that change', async () => {
75+
instance.setAttribute('data-tagtest', 'section.s.t.u not.v.w.x')
76+
await Promise.resolve()
77+
expect(testParse).to.be.calledWithExactly('section.s.t.u')
78+
expect(testParse).to.be.calledWithExactly('not.v.w.x')
79+
expect(testFound).to.be.calledWithExactly(instance, instance, 'data-tagtest', 's', 't', 'u')
80+
expect(testFound).to.not.be.calledWithMatch(match.any, match.any, 'data-tagtest', 'v', 'w', 'x')
81+
})
82+
})
83+
})
84+
})

0 commit comments

Comments
 (0)