diff --git a/src/js/finder.js b/src/js/finder.js index 172fbc5..5e6500a 100644 --- a/src/js/finder.js +++ b/src/js/finder.js @@ -9,7 +9,8 @@ import { } from './parser.js'; import { isContentEditable, isCustomElement, isFocusVisible, isFocusable, - isInShadowTree, isVisible, resolveContent, sortNodes, traverseNode + isFocusableArea, isInShadowTree, isVisible, resolveContent, sortNodes, + traverseNode } from './utility.js'; /* constants */ @@ -1064,16 +1065,14 @@ export class Finder { break; } case 'focus': { - if (node === this.#document.activeElement && - (node.hasAttribute('autofocus') || node.tabIndex >= 0) && + if (node === this.#document.activeElement && isFocusableArea(node) && isFocusable(node)) { matched.add(node); } break; } case 'focus-visible': { - if (node === this.#document.activeElement && - (node.hasAttribute('autofocus') || node.tabIndex >= 0)) { + if (node === this.#document.activeElement && isFocusableArea(node)) { let bool; if (isFocusVisible(node)) { bool = true; @@ -1096,7 +1095,7 @@ export class Finder { case 'focus-within': { let bool; let current = this.#document.activeElement; - if (current.hasAttribute('autofocus') || current.tabIndex >= 0) { + if (isFocusableArea(current)) { while (current) { if (current === node) { bool = true; diff --git a/src/js/utility.js b/src/js/utility.js index 69f95e7..a983151 100644 --- a/src/js/utility.js +++ b/src/js/utility.js @@ -416,6 +416,98 @@ export const isFocusVisible = node => { return !!res; }; +/** + * is focusable area + * @param {object} node - Element node + * @returns {boolean} - result + */ +export const isFocusableArea = node => { + if (node?.nodeType === ELEMENT_NODE) { + if (!node.isConnected) { + return false; + } + const window = node.ownerDocument.defaultView; + if (node instanceof window.HTMLElement) { + if (!Number.isNaN(parseInt(node.getAttribute('tabindex')))) { + return true; + } + if (isContentEditable(node)) { + return true; + } + const { localName, parentNode } = node; + switch (localName) { + case 'a': { + if (node.href || node.hasAttribute('href')) { + return true; + } + return false; + } + case 'iframe': { + return true; + } + case 'input': { + if (node.disabled || node.hasAttribute('disabled') || + node.hidden || node.hasAttribute('hidden')) { + return false; + } + return true; + } + case 'summary': { + if (parentNode.localName === 'details') { + let child = parentNode.firstElementChild; + while (child) { + if (child.localName === 'summary') { + return node === child; + } + child = child.nextElementSibling; + } + } + return false; + } + default: { + const keys = new Set(['button', 'select', 'textarea']); + if (keys.has(localName) && + !(node.disabled || node.hasAttribute('disabled'))) { + return true; + } + return false; + } + } + } else if (node instanceof window.SVGElement) { + if (!Number.isNaN(parseInt(node.getAttributeNS(null, 'tabindex')))) { + const keys = new Set([ + 'clipPath', 'defs', 'desc', 'linearGradient', 'marker', 'mask', + 'metadata', 'pattern', 'radialGradient', 'script', 'style', 'symbol', + 'title' + ]); + const ns = 'http://www.w3.org/2000/svg'; + let bool; + let refNode = node; + while (refNode.namespaceURI === ns) { + bool = keys.has(refNode.localName); + if (bool) { + break; + } + if (refNode?.parentNode?.namespaceURI === ns) { + refNode = refNode.parentNode; + } else { + break; + } + } + if (bool) { + return false; + } + return true; + } + if (node.localName === 'a' && + (node.href || node.hasAttributeNS(null, 'href'))) { + return true; + } + } + } + return false; +}; + /** * is focusable * NOTE: workaround for jsdom issue: https://github.com/jsdom/jsdom/issues/3464 diff --git a/test/utility.test.js b/test/utility.test.js index a304617..4151580 100644 --- a/test/utility.test.js +++ b/test/utility.test.js @@ -1154,6 +1154,221 @@ describe('utility functions', () => { }); }); + describe('is focasable area', () => { + const func = util.isFocusableArea; + + it('should get false', () => { + const res = func(); + assert.isFalse(res, 'result'); + }); + + it('should get false', () => { + const res = func(document); + assert.isFalse(res, 'result'); + }); + + it('should get false', () => { + const res = func(document.body); + assert.isFalse(res, 'result'); + }); + + it('should get false', () => { + const node = document.createElement('div'); + const res = func(node); + assert.isFalse(res, 'result'); + }); + + it('should get false', () => { + const node = document.createElement('div'); + document.body.appendChild(node); + const res = func(node); + assert.isFalse(res, 'result'); + }); + + it('should get true', () => { + const node = document.createElement('div'); + node.tabIndex = -1; + document.body.appendChild(node); + const res = func(node); + assert.isTrue(res, 'result'); + }); + + it('should get true', () => { + const node = document.createElement('div'); + node.setAttribute('contenteditable', ''); + document.body.appendChild(node); + const res = func(node); + assert.isTrue(res, 'result'); + }); + + it('should get false', () => { + const node = document.createElement('a'); + document.body.appendChild(node); + const res = func(node); + assert.isFalse(res, 'result'); + }); + + it('should get true', () => { + const node = document.createElement('a'); + node.href = 'about:blank'; + document.body.appendChild(node); + const res = func(node); + assert.isTrue(res, 'result'); + }); + + it('should get true', () => { + const node = document.createElement('iframe'); + document.body.appendChild(node); + const res = func(node); + assert.isTrue(res, 'result'); + }); + + it('should get true', () => { + const node = document.createElement('input'); + document.body.appendChild(node); + const res = func(node); + assert.isTrue(res, 'result'); + }); + + it('should get false', () => { + const node = document.createElement('input'); + node.disabled = true; + document.body.appendChild(node); + const res = func(node); + assert.isFalse(res, 'result'); + }); + + it('should get false', () => { + const node = document.createElement('input'); + node.setAttribute('disabled', ''); + document.body.appendChild(node); + const res = func(node); + assert.isFalse(res, 'result'); + }); + + it('should get false', () => { + const node = document.createElement('input'); + node.hidden = true; + document.body.appendChild(node); + const res = func(node); + assert.isFalse(res, 'result'); + }); + + it('should get false', () => { + const node = document.createElement('input'); + node.setAttribute('hidden', ''); + document.body.appendChild(node); + const res = func(node); + assert.isFalse(res, 'result'); + }); + + it('should get false', () => { + const node = document.createElement('summary'); + document.body.appendChild(node); + const res = func(node); + assert.isFalse(res, 'result'); + }); + + it('should get true', () => { + const parent = document.createElement('details'); + const node = document.createElement('summary'); + parent.appendChild(node); + document.body.appendChild(parent); + const res = func(node); + assert.isTrue(res, 'result'); + }); + + it('should get false', () => { + const parent = document.createElement('details'); + const nodeBefore = document.createElement('summary'); + const node = document.createElement('summary'); + parent.appendChild(nodeBefore); + parent.appendChild(node); + document.body.appendChild(parent); + const res = func(node); + assert.isFalse(res, 'result'); + }); + + it('should get true', () => { + const parent = document.createElement('details'); + const nodeBefore = document.createElement('div'); + const node = document.createElement('summary'); + parent.appendChild(nodeBefore); + parent.appendChild(node); + document.body.appendChild(parent); + const res = func(node); + assert.isTrue(res, 'result'); + }); + + it('should get true', () => { + const node = document.createElement('button'); + document.body.appendChild(node); + const res = func(node); + assert.isTrue(res, 'result'); + }); + + it('should get false', () => { + const node = document.createElement('button'); + node.disabled = true; + document.body.appendChild(node); + const res = func(node); + assert.isFalse(res, 'result'); + }); + + it('should get false', () => { + const node = + document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + document.body.appendChild(node); + const res = func(node); + assert.isFalse(res, 'result'); + }); + + it('should get true', () => { + const node = + document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + node.tabIndex = -1; + document.body.appendChild(node); + const res = func(node); + assert.isTrue(res, 'result'); + }); + + it('should get true', () => { + const parent = + document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + const node = + document.createElementNS('http://www.w3.org/2000/svg', 'text'); + node.tabIndex = -1; + parent.appendChild(node) + document.body.appendChild(parent); + const res = func(node); + assert.isTrue(res, 'result'); + }); + + it('should get false', () => { + const parent = + document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + const node = + document.createElementNS('http://www.w3.org/2000/svg', 'mask'); + node.tabIndex = -1; + parent.appendChild(node) + document.body.appendChild(parent); + const res = func(node); + assert.isFalse(res, 'result'); + }); + + it('should get true', () => { + const parent = + document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + const node = + document.createElementNS('http://www.w3.org/2000/svg', 'a'); + node.setAttribute('href', 'about:blank'); + parent.appendChild(node) + document.body.appendChild(parent); + const res = func(node); + assert.isTrue(res, 'result'); + }); + }); + describe('is focusable', () => { const func = util.isFocusable;