Skip to content

Commit

Permalink
fix: fix for shadydom polyfill (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
nolanlawson authored Jun 20, 2020
1 parent a2f16c6 commit 508df4d
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 99 deletions.
66 changes: 51 additions & 15 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,33 +86,69 @@ function shouldIgnoreEvent (activeElement, forwardDirection) {
return true
}

function getNextNode (root, targetElement, forwardDirection): HTMLElement {
var filter: NodeFilter = {
acceptNode: function (node: HTMLElement) {
var accept = (node === targetElement || node.shadowRoot || isFocusable(node))
return accept ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
function getNextCandidateNodeForShadowDomPolyfill (root, targetElement, forwardDirection, filter): HTMLElement {
// When the shadydom polyfill is running, we can't use TreeWalker on ShadowRoots because
// they aren't real Nodes. So we do this workaround where we run TreeWalker on the
// children instead.
var nodes = Array.prototype.slice.call(root.querySelectorAll('*'))
var idx = nodes.indexOf(targetElement)
if (forwardDirection) {
nodes = nodes.slice(idx + 1)
} else {
if (idx === -1) {
idx = nodes.length
}
nodes = nodes.slice(0, idx)
nodes.reverse()
}
var walker: TreeWalker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filter)
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i]
if (node instanceof HTMLElement && filter.acceptNode(node) === NodeFilter.FILTER_ACCEPT) {
return node
}
}
return undefined
}

function getNextCandidateNode (root, targetElement, forwardDirection, filter): HTMLElement {
var walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filter)
if (targetElement) {
walker.currentNode = targetElement
}

var nextNode: HTMLElement

if (forwardDirection) {
nextNode = walker.nextNode() as HTMLElement
return walker.nextNode() as HTMLElement
} else if (targetElement) {
nextNode = walker.previousNode() as HTMLElement
} else { // iterating backwards through shadow root, use last child
nextNode = walker.lastChild() as HTMLElement
return walker.previousNode() as HTMLElement
}
// iterating backwards through shadow root, use last child
return walker.lastChild() as HTMLElement
}

function isShadowDomPolyfill () {
return typeof ShadowRoot !== 'undefined' &&
// ShadowRoot.polyfill is just a hack for our unit tests
('polyfill' in ShadowRoot || !ShadowRoot.toString().includes('[native code]'))
}

function getNextNode (root, targetElement, forwardDirection): HTMLElement {
var filter: NodeFilter = {
acceptNode: function (node: HTMLElement) {
return (node === targetElement || node.shadowRoot || isFocusable(node))
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_SKIP
}
}

// TODO: remove this when we don't need to support the Shadow DOM polyfill
var nextNode = isShadowDomPolyfill() && root instanceof ShadowRoot
? getNextCandidateNodeForShadowDomPolyfill(root, targetElement, forwardDirection, filter)
: getNextCandidateNode(root, targetElement, forwardDirection, filter)

if (nextNode && nextNode.shadowRoot) { // push into the shadow DOM
return getNextNode(nextNode.shadowRoot, null, forwardDirection)
}
if (!nextNode && root.host) { // pop out of the shadow DOM
return getNextNode(root.host.getRootNode(), root.host, forwardDirection)
if (!nextNode && (root as ShadowRoot).host) { // pop out of the shadow DOM
return getNextNode((root as ShadowRoot).host.getRootNode(), (root as ShadowRoot).host, forwardDirection)
}
return nextNode
}
Expand Down
189 changes: 105 additions & 84 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,11 +341,12 @@ describe('test suite', () => {
})

describe('shadow dom', () => {
it('works with shadow dom', () => {

before(() => {
class Component extends HTMLElement {
constructor() {
constructor () {
super()
this.attachShadow({ mode: 'open'})
this.attachShadow({ mode: 'open' })
this.shadowRoot.innerHTML = `
<button class="inside-shadow-button-1">
<input type=text class="inside-shadow-text" value='hi'>
Expand All @@ -354,48 +355,11 @@ describe('test suite', () => {
this.classList.add('my-component')
}
}
customElements.define('my-component', Component)

document.body.innerHTML = `
<button class="button-1">one</button>
<my-component></my-component>
<button class="button-2">two</button>
`
typeRight()
assertActiveClass(['button-1'])
typeRight()
assertActiveClass(['my-component'])
assertShadowActiveClass(['inside-shadow-button-1'])
typeRight()
assertShadowActiveClass(['inside-shadow-text'])
typeRight()
assertShadowActiveClass(['inside-shadow-text'])
typeRight()
assertShadowActiveClass(['inside-shadow-text'])
typeRight()
assertShadowActiveClass(['inside-shadow-button-2'])
typeRight()
assertActiveClass(['button-2'])
typeLeft()
assertActiveClass(['my-component'])
assertShadowActiveClass(['inside-shadow-button-2'])
typeLeft()
assertShadowActiveClass(['inside-shadow-text'])
typeLeft()
assertShadowActiveClass(['inside-shadow-text'])
typeLeft()
assertShadowActiveClass(['inside-shadow-text'])
typeLeft()
assertShadowActiveClass(['inside-shadow-button-1'])
typeLeft()
assertActiveClass(['button-1'])
})

it('works with shadow dom inside shadow dom', () => {
class Component2 extends HTMLElement {
constructor() {
constructor () {
super()
this.attachShadow({ mode: 'open'})
this.attachShadow({ mode: 'open' })
this.shadowRoot.innerHTML = `
<span>not focusable</span>
<my-component-3></my-component-3>
Expand All @@ -406,9 +370,9 @@ describe('test suite', () => {
}

class Component3 extends HTMLElement {
constructor() {
constructor () {
super()
this.attachShadow({ mode: 'open'})
this.attachShadow({ mode: 'open' })
this.shadowRoot.innerHTML = `
<span>not focusable</span>
<button class="inside-shadow-button">button</button>
Expand All @@ -418,33 +382,10 @@ describe('test suite', () => {
}
}

customElements.define('my-component-2', Component2)
customElements.define('my-component-3', Component3)

document.body.innerHTML = `
<button class="button-1">one</button>
<my-component-2></my-component-2>
<button class="button-2">two</button>
`
typeRight()
assertActiveClass(['button-1'])
typeRight()
assertActiveClass(['my-component-2'])
assertShadowActiveClass(['inside-shadow-button'])
typeRight()
assertActiveClass(['button-2'])
typeLeft()
assertActiveClass(['my-component-2'])
assertShadowActiveClass(['inside-shadow-button'])
typeLeft()
assertActiveClass(['button-1'])
})

it('works with shadow dom inside shadow dom 2', () => {
class Component4 extends HTMLElement {
constructor() {
constructor () {
super()
this.attachShadow({ mode: 'open'})
this.attachShadow({ mode: 'open' })
this.shadowRoot.innerHTML = `
<my-component-5></my-component-5>
`
Expand All @@ -453,36 +394,116 @@ describe('test suite', () => {
}

class Component5 extends HTMLElement {
constructor() {
constructor () {
super()
this.attachShadow({ mode: 'open'})
this.attachShadow({ mode: 'open' })
this.shadowRoot.innerHTML = `
<button class="inside-shadow-button">button</button>
`
this.classList.add('my-component-5')
}
}

customElements.define('my-component', Component)
customElements.define('my-component-2', Component2)
customElements.define('my-component-3', Component3)
customElements.define('my-component-4', Component4)
customElements.define('my-component-5', Component5)
})

document.body.innerHTML = `
const polyFillTypes = [false, true]
polyFillTypes.forEach(isPolyfill => {
describe(`polyfilled: ${isPolyfill}`, () => {

before(() => {
if (isPolyfill) {
// hack to opt our code into the polyfill mode (not really polyfilled)
ShadowRoot.polyfill = true
}
})

after(() => {
if (isPolyfill) {
delete ShadowRoot.polyfill
}
})

it('works with shadow dom', () => {
document.body.innerHTML = `
<button class="button-1">one</button>
<my-component></my-component>
<button class="button-2">two</button>
`
typeRight()
assertActiveClass(['button-1'])
typeRight()
assertActiveClass(['my-component'])
assertShadowActiveClass(['inside-shadow-button-1'])
typeRight()
assertShadowActiveClass(['inside-shadow-text'])
typeRight()
assertShadowActiveClass(['inside-shadow-text'])
typeRight()
assertShadowActiveClass(['inside-shadow-text'])
typeRight()
assertShadowActiveClass(['inside-shadow-button-2'])
typeRight()
assertActiveClass(['button-2'])
typeLeft()
assertActiveClass(['my-component'])
assertShadowActiveClass(['inside-shadow-button-2'])
typeLeft()
assertShadowActiveClass(['inside-shadow-text'])
typeLeft()
assertShadowActiveClass(['inside-shadow-text'])
typeLeft()
assertShadowActiveClass(['inside-shadow-text'])
typeLeft()
assertShadowActiveClass(['inside-shadow-button-1'])
typeLeft()
assertActiveClass(['button-1'])
})

it('works with shadow dom inside shadow dom', () => {
document.body.innerHTML = `
<button class="button-1">one</button>
<my-component-2></my-component-2>
<button class="button-2">two</button>
`
typeRight()
assertActiveClass(['button-1'])
typeRight()
assertActiveClass(['my-component-2'])
assertShadowActiveClass(['inside-shadow-button'])
typeRight()
assertActiveClass(['button-2'])
typeLeft()
assertActiveClass(['my-component-2'])
assertShadowActiveClass(['inside-shadow-button'])
typeLeft()
assertActiveClass(['button-1'])
})

it('works with shadow dom inside shadow dom 2', () => {
document.body.innerHTML = `
<button class="button-1">one</button>
<my-component-4></my-component-4>
<button class="button-2">two</button>
`
typeRight()
assertActiveClass(['button-1'])
typeRight()
assertActiveClass(['my-component-4'])
assertShadowActiveClass(['inside-shadow-button'])
typeRight()
assertActiveClass(['button-2'])
typeLeft()
assertActiveClass(['my-component-4'])
assertShadowActiveClass(['inside-shadow-button'])
typeLeft()
assertActiveClass(['button-1'])
typeRight()
assertActiveClass(['button-1'])
typeRight()
assertActiveClass(['my-component-4'])
assertShadowActiveClass(['inside-shadow-button'])
typeRight()
assertActiveClass(['button-2'])
typeLeft()
assertActiveClass(['my-component-4'])
assertShadowActiveClass(['inside-shadow-button'])
typeLeft()
assertActiveClass(['button-1'])
})
})
})
})
})

0 comments on commit 508df4d

Please sign in to comment.