Skip to content

Commit

Permalink
Add isFocusableArea function
Browse files Browse the repository at this point in the history
  • Loading branch information
asamuzaK committed Sep 3, 2024
1 parent f08faae commit 3a2d05e
Show file tree
Hide file tree
Showing 3 changed files with 312 additions and 6 deletions.
11 changes: 5 additions & 6 deletions src/js/finder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
92 changes: 92 additions & 0 deletions src/js/utility.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
215 changes: 215 additions & 0 deletions test/utility.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down

0 comments on commit 3a2d05e

Please sign in to comment.