@wordpress/dom 4.55.0 → 4.55.2-next.v.202609171837.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +73 -73
- package/build/dom/get-rectangle-from-range.cjs +47 -22
- package/build/dom/get-rectangle-from-range.cjs.map +2 -2
- package/build/focusable.cjs +5 -8
- package/build/focusable.cjs.map +2 -2
- package/build-module/dom/get-rectangle-from-range.mjs +47 -22
- package/build-module/dom/get-rectangle-from-range.mjs.map +2 -2
- package/build-module/focusable.mjs +5 -8
- package/build-module/focusable.mjs.map +2 -2
- package/build-types/dom/get-rectangle-from-range.d.ts.map +1 -1
- package/build-types/focusable.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/dom/get-rectangle-from-range.js +64 -31
- package/src/dom/test/get-rectangle-from-range.browser.test.js +128 -0
- package/src/focusable.js +8 -11
- package/src/test/focusable-area.jsdom.test.js +43 -0
- package/src/test/focusable.browser.test.js +243 -2
- package/src/test/{tabbable.jsdom.test.js → tabbable.browser.test.js} +11 -5
- package/src/test/focusable.jsdom.test.js +0 -193
- package/src/test/utils/create-element.js +0 -54
|
@@ -40,19 +40,54 @@ function getRectangleFromRange(range) {
|
|
|
40
40
|
furthestBottom - furthestTop
|
|
41
41
|
);
|
|
42
42
|
}
|
|
43
|
-
const { startContainer } = range;
|
|
43
|
+
const { startContainer, startOffset } = range;
|
|
44
44
|
const { ownerDocument } = startContainer;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
assertIsDefined(parentNode, "parentNode");
|
|
48
|
-
const index = (
|
|
49
|
-
/** @type {Node[]} */
|
|
50
|
-
Array.from(parentNode.childNodes).indexOf(startContainer)
|
|
51
|
-
);
|
|
52
|
-
assertIsDefined(ownerDocument, "ownerDocument");
|
|
45
|
+
assertIsDefined(ownerDocument, "ownerDocument");
|
|
46
|
+
if (startContainer.nodeType !== startContainer.TEXT_NODE && !startContainer.childNodes.length && startContainer.parentNode) {
|
|
53
47
|
range = ownerDocument.createRange();
|
|
54
|
-
range.
|
|
55
|
-
range.
|
|
48
|
+
range.setStartBefore(startContainer);
|
|
49
|
+
range.collapse(true);
|
|
50
|
+
return getRectangleFromRange(range);
|
|
51
|
+
}
|
|
52
|
+
if (startContainer.nodeType !== startContainer.TEXT_NODE) {
|
|
53
|
+
let before = startContainer.childNodes[startOffset - 1];
|
|
54
|
+
while (before?.lastChild) {
|
|
55
|
+
before = before.lastChild;
|
|
56
|
+
}
|
|
57
|
+
let after = startContainer.childNodes[startOffset];
|
|
58
|
+
while (after?.firstChild) {
|
|
59
|
+
after = after.firstChild;
|
|
60
|
+
}
|
|
61
|
+
let beforeRange;
|
|
62
|
+
if (before && before.nodeType === before.TEXT_NODE) {
|
|
63
|
+
beforeRange = ownerDocument.createRange();
|
|
64
|
+
beforeRange.setStart(
|
|
65
|
+
before,
|
|
66
|
+
/** @type {Text} */
|
|
67
|
+
before.length
|
|
68
|
+
);
|
|
69
|
+
beforeRange.collapse(true);
|
|
70
|
+
}
|
|
71
|
+
let afterRange;
|
|
72
|
+
if (after && after.nodeType === after.TEXT_NODE) {
|
|
73
|
+
afterRange = ownerDocument.createRange();
|
|
74
|
+
afterRange.setStart(after, 0);
|
|
75
|
+
afterRange.collapse(true);
|
|
76
|
+
}
|
|
77
|
+
if (beforeRange && afterRange) {
|
|
78
|
+
const beforeRect = beforeRange.getClientRects()[0];
|
|
79
|
+
const afterRect = afterRange.getClientRects()[0];
|
|
80
|
+
if (beforeRect && afterRect && beforeRect.bottom <= afterRect.top) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (afterRange) {
|
|
85
|
+
range = afterRange;
|
|
86
|
+
} else if (beforeRange) {
|
|
87
|
+
range = beforeRange;
|
|
88
|
+
} else {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
56
91
|
}
|
|
57
92
|
const rects = range.getClientRects();
|
|
58
93
|
if (rects.length > 1) {
|
|
@@ -73,17 +108,7 @@ function getRectangleFromRange(range) {
|
|
|
73
108
|
return null;
|
|
74
109
|
}
|
|
75
110
|
}
|
|
76
|
-
|
|
77
|
-
if (!rect || rect.height === 0) {
|
|
78
|
-
assertIsDefined(ownerDocument, "ownerDocument");
|
|
79
|
-
const padNode = ownerDocument.createTextNode("");
|
|
80
|
-
range = range.cloneRange();
|
|
81
|
-
range.insertNode(padNode);
|
|
82
|
-
rect = range.getClientRects()[0];
|
|
83
|
-
assertIsDefined(padNode.parentNode, "padNode.parentNode");
|
|
84
|
-
padNode.parentNode.removeChild(padNode);
|
|
85
|
-
}
|
|
86
|
-
return rect;
|
|
111
|
+
return rects[0] ?? null;
|
|
87
112
|
}
|
|
88
113
|
export {
|
|
89
114
|
getRectangleFromRange as default
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/dom/get-rectangle-from-range.js"],
|
|
4
|
-
"sourcesContent": ["import { assertIsDefined } from '../utils/assert-is-defined';\n\n/**\n * Get the rectangle of a given Range. Returns `null` if no suitable rectangle\n * can be found. Use instead of `Range.getBoundingClientRect()`, which is often\n * broken, especially for collapsed ranges.\n *\n * @param {Range} range The range.\n *\n * @return {DOMRect?} The rectangle.\n */\nexport default function getRectangleFromRange( range ) {\n\t// For uncollapsed ranges, get the rectangle that bounds the contents of the\n\t// range; this a rectangle enclosing the union of the bounding rectangles\n\t// for all the elements in the range.\n\tif ( ! range.collapsed ) {\n\t\tconst rects = Array.from( range.getClientRects() );\n\n\t\t// If there's just a single rect, return it.\n\t\tif ( rects.length === 1 ) {\n\t\t\treturn rects[ 0 ];\n\t\t}\n\n\t\t// Ignore tiny selection at the edge of a range.\n\t\tconst filteredRects = rects.filter( ( { width } ) => width > 1 );\n\n\t\t// If it's full of tiny selections, return browser default.\n\t\tif ( filteredRects.length === 0 ) {\n\t\t\treturn range.getBoundingClientRect();\n\t\t}\n\n\t\tif ( filteredRects.length === 1 ) {\n\t\t\treturn filteredRects[ 0 ];\n\t\t}\n\n\t\tlet {\n\t\t\ttop: furthestTop,\n\t\t\tbottom: furthestBottom,\n\t\t\tleft: furthestLeft,\n\t\t\tright: furthestRight,\n\t\t} = filteredRects[ 0 ];\n\n\t\tfor ( const { top, bottom, left, right } of filteredRects ) {\n\t\t\tif ( top < furthestTop ) {\n\t\t\t\tfurthestTop = top;\n\t\t\t}\n\t\t\tif ( bottom > furthestBottom ) {\n\t\t\t\tfurthestBottom = bottom;\n\t\t\t}\n\t\t\tif ( left < furthestLeft ) {\n\t\t\t\tfurthestLeft = left;\n\t\t\t}\n\t\t\tif ( right > furthestRight ) {\n\t\t\t\tfurthestRight = right;\n\t\t\t}\n\t\t}\n\n\t\treturn new window.DOMRect(\n\t\t\tfurthestLeft,\n\t\t\tfurthestTop,\n\t\t\tfurthestRight - furthestLeft,\n\t\t\tfurthestBottom - furthestTop\n\t\t);\n\t}\n\n\tconst { startContainer } = range;\n\tconst { ownerDocument } = startContainer;\n\n\t//
|
|
5
|
-
"mappings": ";AAAA,SAAS,uBAAuB;AAWjB,SAAR,sBAAwC,OAAQ;AAItD,MAAK,CAAE,MAAM,WAAY;AACxB,UAAMA,SAAQ,MAAM,KAAM,MAAM,eAAe,CAAE;AAGjD,QAAKA,OAAM,WAAW,GAAI;AACzB,aAAOA,OAAO,CAAE;AAAA,IACjB;AAGA,UAAM,gBAAgBA,OAAM,OAAQ,CAAE,EAAE,MAAM,MAAO,QAAQ,CAAE;AAG/D,QAAK,cAAc,WAAW,GAAI;AACjC,aAAO,MAAM,sBAAsB;AAAA,IACpC;AAEA,QAAK,cAAc,WAAW,GAAI;AACjC,aAAO,cAAe,CAAE;AAAA,IACzB;AAEA,QAAI;AAAA,MACH,KAAK;AAAA,MACL,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,OAAO;AAAA,IACR,IAAI,cAAe,CAAE;AAErB,eAAY,EAAE,KAAK,QAAQ,MAAM,MAAM,KAAK,eAAgB;AAC3D,UAAK,MAAM,aAAc;AACxB,sBAAc;AAAA,MACf;AACA,UAAK,SAAS,gBAAiB;AAC9B,yBAAiB;AAAA,MAClB;AACA,UAAK,OAAO,cAAe;AAC1B,uBAAe;AAAA,MAChB;AACA,UAAK,QAAQ,eAAgB;AAC5B,wBAAgB;AAAA,MACjB;AAAA,IACD;AAEA,WAAO,IAAI,OAAO;AAAA,MACjB;AAAA,MACA;AAAA,MACA,gBAAgB;AAAA,MAChB,iBAAiB;AAAA,IAClB;AAAA,EACD;AAEA,QAAM,EAAE,
|
|
4
|
+
"sourcesContent": ["import { assertIsDefined } from '../utils/assert-is-defined';\n\n/**\n * Get the rectangle of a given Range. Returns `null` if no suitable rectangle\n * can be found. Use instead of `Range.getBoundingClientRect()`, which is often\n * broken, especially for collapsed ranges.\n *\n * @param {Range} range The range.\n *\n * @return {DOMRect?} The rectangle.\n */\nexport default function getRectangleFromRange( range ) {\n\t// For uncollapsed ranges, get the rectangle that bounds the contents of the\n\t// range; this a rectangle enclosing the union of the bounding rectangles\n\t// for all the elements in the range.\n\tif ( ! range.collapsed ) {\n\t\tconst rects = Array.from( range.getClientRects() );\n\n\t\t// If there's just a single rect, return it.\n\t\tif ( rects.length === 1 ) {\n\t\t\treturn rects[ 0 ];\n\t\t}\n\n\t\t// Ignore tiny selection at the edge of a range.\n\t\tconst filteredRects = rects.filter( ( { width } ) => width > 1 );\n\n\t\t// If it's full of tiny selections, return browser default.\n\t\tif ( filteredRects.length === 0 ) {\n\t\t\treturn range.getBoundingClientRect();\n\t\t}\n\n\t\tif ( filteredRects.length === 1 ) {\n\t\t\treturn filteredRects[ 0 ];\n\t\t}\n\n\t\tlet {\n\t\t\ttop: furthestTop,\n\t\t\tbottom: furthestBottom,\n\t\t\tleft: furthestLeft,\n\t\t\tright: furthestRight,\n\t\t} = filteredRects[ 0 ];\n\n\t\tfor ( const { top, bottom, left, right } of filteredRects ) {\n\t\t\tif ( top < furthestTop ) {\n\t\t\t\tfurthestTop = top;\n\t\t\t}\n\t\t\tif ( bottom > furthestBottom ) {\n\t\t\t\tfurthestBottom = bottom;\n\t\t\t}\n\t\t\tif ( left < furthestLeft ) {\n\t\t\t\tfurthestLeft = left;\n\t\t\t}\n\t\t\tif ( right > furthestRight ) {\n\t\t\t\tfurthestRight = right;\n\t\t\t}\n\t\t}\n\n\t\treturn new window.DOMRect(\n\t\t\tfurthestLeft,\n\t\t\tfurthestTop,\n\t\t\tfurthestRight - furthestLeft,\n\t\t\tfurthestBottom - furthestTop\n\t\t);\n\t}\n\n\tconst { startContainer, startOffset } = range;\n\tconst { ownerDocument } = startContainer;\n\tassertIsDefined( ownerDocument, 'ownerDocument' );\n\n\t// A range inside an element with no children, like a line break or the\n\t// placeholder, is the same spot as the position before that element.\n\tif (\n\t\tstartContainer.nodeType !== startContainer.TEXT_NODE &&\n\t\t! startContainer.childNodes.length &&\n\t\tstartContainer.parentNode\n\t) {\n\t\trange = ownerDocument.createRange();\n\t\trange.setStartBefore( startContainer );\n\t\trange.collapse( true );\n\t\treturn getRectangleFromRange( range );\n\t}\n\n\t// A collapsed range at an element offset has no rectangle. Translate it\n\t// to the text offset next to it: the end of the text before or the start\n\t// of the text after. When those sit on different lines the caret could\n\t// be on either, so return null.\n\tif ( startContainer.nodeType !== startContainer.TEXT_NODE ) {\n\t\tlet before = startContainer.childNodes[ startOffset - 1 ];\n\t\twhile ( before?.lastChild ) {\n\t\t\tbefore = before.lastChild;\n\t\t}\n\t\tlet after = startContainer.childNodes[ startOffset ];\n\t\twhile ( after?.firstChild ) {\n\t\t\tafter = after.firstChild;\n\t\t}\n\n\t\tlet beforeRange;\n\t\tif ( before && before.nodeType === before.TEXT_NODE ) {\n\t\t\tbeforeRange = ownerDocument.createRange();\n\t\t\tbeforeRange.setStart(\n\t\t\t\tbefore,\n\t\t\t\t/** @type {Text} */ ( before ).length\n\t\t\t);\n\t\t\tbeforeRange.collapse( true );\n\t\t}\n\t\tlet afterRange;\n\t\tif ( after && after.nodeType === after.TEXT_NODE ) {\n\t\t\tafterRange = ownerDocument.createRange();\n\t\t\tafterRange.setStart( after, 0 );\n\t\t\tafterRange.collapse( true );\n\t\t}\n\n\t\tif ( beforeRange && afterRange ) {\n\t\t\tconst beforeRect = beforeRange.getClientRects()[ 0 ];\n\t\t\tconst afterRect = afterRange.getClientRects()[ 0 ];\n\t\t\tif (\n\t\t\t\tbeforeRect &&\n\t\t\t\tafterRect &&\n\t\t\t\tbeforeRect.bottom <= afterRect.top\n\t\t\t) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\n\t\tif ( afterRange ) {\n\t\t\trange = afterRange;\n\t\t} else if ( beforeRange ) {\n\t\t\trange = beforeRange;\n\t\t} else {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tconst rects = range.getClientRects();\n\n\t// If we have multiple rectangles for a collapsed range, there's no way to\n\t// know which it is, so don't return anything.\n\tif ( rects.length > 1 ) {\n\t\treturn null;\n\t}\n\n\t// A collapsed range at a soft line wrap is equally ambiguous: the same\n\t// position ends one line and starts the next. Some browsers (Gecko)\n\t// return a single rectangle for it, on the upper line, regardless of\n\t// where the caret is. Detect the boundary by measuring the characters\n\t// around the position: when they sit on different lines, there is no\n\t// way to know which line the caret is on, so don't return anything.\n\tif (\n\t\trects.length === 1 &&\n\t\tstartContainer.nodeType === startContainer.TEXT_NODE &&\n\t\trange.startOffset > 0 &&\n\t\trange.startOffset < /** @type {Text} */ ( startContainer ).length\n\t) {\n\t\tassertIsDefined( ownerDocument, 'ownerDocument' );\n\t\tconst measure = (\n\t\t\t/** @type {number} */ start,\n\t\t\t/** @type {number} */ end\n\t\t) => {\n\t\t\tconst charRange = ownerDocument.createRange();\n\t\t\tcharRange.setStart( startContainer, start );\n\t\t\tcharRange.setEnd( startContainer, end );\n\t\t\treturn charRange.getBoundingClientRect();\n\t\t};\n\t\tconst before = measure( range.startOffset - 1, range.startOffset );\n\t\tconst after = measure( range.startOffset, range.startOffset + 1 );\n\n\t\tif ( before.bottom <= after.top ) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\treturn rects[ 0 ] ?? null;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["rects"]
|
|
7
7
|
}
|
|
@@ -38,17 +38,11 @@ function isValidFocusableArea(element) {
|
|
|
38
38
|
const img = element.ownerDocument.querySelector(
|
|
39
39
|
'img[usemap="#' + map.name + '"]'
|
|
40
40
|
);
|
|
41
|
-
return !!img && isVisible(img);
|
|
41
|
+
return !!img && !img.closest("[inert]") && isVisible(img);
|
|
42
42
|
}
|
|
43
43
|
function find(context, { sequential = false } = {}) {
|
|
44
44
|
const elements = context.querySelectorAll(buildSelector(sequential));
|
|
45
45
|
return Array.from(elements).filter((element) => {
|
|
46
|
-
if (!isVisible(element)) {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
if (element.closest("[inert]")) {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
46
|
const { nodeName } = element;
|
|
53
47
|
if ("AREA" === nodeName) {
|
|
54
48
|
return isValidFocusableArea(
|
|
@@ -56,7 +50,10 @@ function find(context, { sequential = false } = {}) {
|
|
|
56
50
|
element
|
|
57
51
|
);
|
|
58
52
|
}
|
|
59
|
-
|
|
53
|
+
if (element.closest("[inert]")) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return isVisible(element);
|
|
60
57
|
});
|
|
61
58
|
}
|
|
62
59
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/focusable.js"],
|
|
4
|
-
"sourcesContent": ["/**\n * References:\n *\n * Focusable:\n * - https://www.w3.org/TR/html5/editing.html#focus-management\n *\n * Sequential focus navigation:\n * - https://www.w3.org/TR/html5/editing.html#sequential-focus-navigation-and-the-tabindex-attribute\n *\n * Disabled elements:\n * - https://www.w3.org/TR/html5/disabled-elements.html#disabled-elements\n *\n * getClientRects algorithm (requiring layout box):\n * - https://www.w3.org/TR/cssom-view-1/#extension-to-the-element-interface\n *\n * AREA elements associated with an IMG:\n * - https://w3c.github.io/html/editing.html#data-model\n */\n\n/**\n * Returns a CSS selector used to query for focusable elements.\n *\n * @param {boolean} sequential If set, only query elements that are sequentially\n * focusable. Non-interactive elements with a\n * negative `tabindex` are focusable but not\n * sequentially focusable.\n * https://html.spec.whatwg.org/multipage/interaction.html#the-tabindex-attribute\n *\n * @return {string} CSS selector.\n */\nfunction buildSelector( sequential ) {\n\treturn [\n\t\tsequential ? '[tabindex]:not([tabindex^=\"-\"])' : '[tabindex]',\n\t\t'a[href]',\n\t\t'button:not([disabled])',\n\t\t'input:not([type=\"hidden\"]):not([disabled])',\n\t\t'select:not([disabled])',\n\t\t'textarea:not([disabled])',\n\t\t'iframe:not([tabindex^=\"-\"])',\n\t\t'object',\n\t\t'embed',\n\t\t'summary',\n\t\t'area[href]',\n\t\t'[contenteditable]:not([contenteditable=false])',\n\t].join( ',' );\n}\n\n/**\n * Returns true if the specified element has a layout box and is not hidden by\n * CSS visibility or content visibility.\n *\n * @param {HTMLElement} element DOM element to test.\n *\n * @return {boolean} Whether element is visible.\n */\nfunction isVisible( element ) {\n\tif ( typeof element.checkVisibility === 'function' ) {\n\t\tif ( ! element.checkVisibility( { visibilityProperty: true } ) ) {\n\t\t\treturn false;\n\t\t}\n\t} else {\n\t\tconst visibility =\n\t\t\telement.ownerDocument.defaultView?.getComputedStyle(\n\t\t\t\telement\n\t\t\t).visibility;\n\t\tif ( visibility === 'hidden' || visibility === 'collapse' ) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn (\n\t\telement.offsetWidth > 0 ||\n\t\telement.offsetHeight > 0 ||\n\t\telement.getClientRects().length > 0\n\t);\n}\n\n/**\n * Returns true if the specified area element is a valid focusable element, or\n * false otherwise. Area is only focusable if within a map where a named map\n * referenced by an image somewhere in the document.\n *\n * @param {HTMLAreaElement} element DOM area element to test.\n *\n * @return {boolean} Whether area element is valid for focus.\n */\nfunction isValidFocusableArea( element ) {\n\t/** @type {HTMLMapElement | null} */\n\tconst map = element.closest( 'map[name]' );\n\tif ( ! map ) {\n\t\treturn false;\n\t}\n\n\t/** @type {HTMLImageElement | null} */\n\tconst img = element.ownerDocument.querySelector(\n\t\t'img[usemap=\"#' + map.name + '\"]'\n\t);\n\treturn !! img && isVisible( img );\n}\n\n/**\n * Returns all focusable elements within a given context.\n *\n * @param {Element} context Element in which to search.\n * @param {Object} options\n * @param {boolean} [options.sequential] If set, only return elements that are\n * sequentially focusable.\n * Non-interactive elements with a\n * negative `tabindex` are focusable but\n * not sequentially focusable.\n * https://html.spec.whatwg.org/multipage/interaction.html#the-tabindex-attribute\n *\n * @return {HTMLElement[]} Focusable elements.\n */\nexport function find( context, { sequential = false } = {} ) {\n\t/** @type {NodeListOf<HTMLElement>} */\n\tconst elements = context.querySelectorAll( buildSelector( sequential ) );\n\n\treturn Array.from( elements ).filter( ( element ) => {\n\t\tif (
|
|
5
|
-
"mappings": ";AA8BA,SAAS,cAAe,YAAa;AACpC,SAAO;AAAA,IACN,aAAa,oCAAoC;AAAA,IACjD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACD,EAAE,KAAM,GAAI;AACb;AAUA,SAAS,UAAW,SAAU;AAC7B,MAAK,OAAO,QAAQ,oBAAoB,YAAa;AACpD,QAAK,CAAE,QAAQ,gBAAiB,EAAE,oBAAoB,KAAK,CAAE,GAAI;AAChE,aAAO;AAAA,IACR;AAAA,EACD,OAAO;AACN,UAAM,aACL,QAAQ,cAAc,aAAa;AAAA,MAClC;AAAA,IACD,EAAE;AACH,QAAK,eAAe,YAAY,eAAe,YAAa;AAC3D,aAAO;AAAA,IACR;AAAA,EACD;AAEA,SACC,QAAQ,cAAc,KACtB,QAAQ,eAAe,KACvB,QAAQ,eAAe,EAAE,SAAS;AAEpC;AAWA,SAAS,qBAAsB,SAAU;AAExC,QAAM,MAAM,QAAQ,QAAS,WAAY;AACzC,MAAK,CAAE,KAAM;AACZ,WAAO;AAAA,EACR;AAGA,QAAM,MAAM,QAAQ,cAAc;AAAA,IACjC,kBAAkB,IAAI,OAAO;AAAA,EAC9B;AACA,SAAO,CAAC,CAAE,OAAO,UAAW,GAAI;
|
|
4
|
+
"sourcesContent": ["/**\n * References:\n *\n * Focusable:\n * - https://www.w3.org/TR/html5/editing.html#focus-management\n *\n * Sequential focus navigation:\n * - https://www.w3.org/TR/html5/editing.html#sequential-focus-navigation-and-the-tabindex-attribute\n *\n * Disabled elements:\n * - https://www.w3.org/TR/html5/disabled-elements.html#disabled-elements\n *\n * getClientRects algorithm (requiring layout box):\n * - https://www.w3.org/TR/cssom-view-1/#extension-to-the-element-interface\n *\n * AREA elements associated with an IMG:\n * - https://w3c.github.io/html/editing.html#data-model\n */\n\n/**\n * Returns a CSS selector used to query for focusable elements.\n *\n * @param {boolean} sequential If set, only query elements that are sequentially\n * focusable. Non-interactive elements with a\n * negative `tabindex` are focusable but not\n * sequentially focusable.\n * https://html.spec.whatwg.org/multipage/interaction.html#the-tabindex-attribute\n *\n * @return {string} CSS selector.\n */\nfunction buildSelector( sequential ) {\n\treturn [\n\t\tsequential ? '[tabindex]:not([tabindex^=\"-\"])' : '[tabindex]',\n\t\t'a[href]',\n\t\t'button:not([disabled])',\n\t\t'input:not([type=\"hidden\"]):not([disabled])',\n\t\t'select:not([disabled])',\n\t\t'textarea:not([disabled])',\n\t\t'iframe:not([tabindex^=\"-\"])',\n\t\t'object',\n\t\t'embed',\n\t\t'summary',\n\t\t'area[href]',\n\t\t'[contenteditable]:not([contenteditable=false])',\n\t].join( ',' );\n}\n\n/**\n * Returns true if the specified element has a layout box and is not hidden by\n * CSS visibility or content visibility.\n *\n * @param {HTMLElement} element DOM element to test.\n *\n * @return {boolean} Whether element is visible.\n */\nfunction isVisible( element ) {\n\tif ( typeof element.checkVisibility === 'function' ) {\n\t\tif ( ! element.checkVisibility( { visibilityProperty: true } ) ) {\n\t\t\treturn false;\n\t\t}\n\t} else {\n\t\tconst visibility =\n\t\t\telement.ownerDocument.defaultView?.getComputedStyle(\n\t\t\t\telement\n\t\t\t).visibility;\n\t\tif ( visibility === 'hidden' || visibility === 'collapse' ) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn (\n\t\telement.offsetWidth > 0 ||\n\t\telement.offsetHeight > 0 ||\n\t\telement.getClientRects().length > 0\n\t);\n}\n\n/**\n * Returns true if the specified area element is a valid focusable element, or\n * false otherwise. Area is only focusable if within a map where a named map\n * referenced by an image somewhere in the document.\n *\n * @param {HTMLAreaElement} element DOM area element to test.\n *\n * @return {boolean} Whether area element is valid for focus.\n */\nfunction isValidFocusableArea( element ) {\n\t/** @type {HTMLMapElement | null} */\n\tconst map = element.closest( 'map[name]' );\n\tif ( ! map ) {\n\t\treturn false;\n\t}\n\n\t/** @type {HTMLImageElement | null} */\n\tconst img = element.ownerDocument.querySelector(\n\t\t'img[usemap=\"#' + map.name + '\"]'\n\t);\n\treturn !! img && ! img.closest( '[inert]' ) && isVisible( img );\n}\n\n/**\n * Returns all focusable elements within a given context.\n *\n * @param {Element} context Element in which to search.\n * @param {Object} options\n * @param {boolean} [options.sequential] If set, only return elements that are\n * sequentially focusable.\n * Non-interactive elements with a\n * negative `tabindex` are focusable but\n * not sequentially focusable.\n * https://html.spec.whatwg.org/multipage/interaction.html#the-tabindex-attribute\n *\n * @return {HTMLElement[]} Focusable elements.\n */\nexport function find( context, { sequential = false } = {} ) {\n\t/** @type {NodeListOf<HTMLElement>} */\n\tconst elements = context.querySelectorAll( buildSelector( sequential ) );\n\n\treturn Array.from( elements ).filter( ( element ) => {\n\t\tconst { nodeName } = element;\n\t\tif ( 'AREA' === nodeName ) {\n\t\t\t// The mapped image determines whether this region is visible or inert.\n\t\t\treturn isValidFocusableArea(\n\t\t\t\t/** @type {HTMLAreaElement} */ ( element )\n\t\t\t);\n\t\t}\n\n\t\t// Elements inside an inert subtree are not focusable.\n\t\tif ( element.closest( '[inert]' ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn isVisible( element );\n\t} );\n}\n"],
|
|
5
|
+
"mappings": ";AA8BA,SAAS,cAAe,YAAa;AACpC,SAAO;AAAA,IACN,aAAa,oCAAoC;AAAA,IACjD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACD,EAAE,KAAM,GAAI;AACb;AAUA,SAAS,UAAW,SAAU;AAC7B,MAAK,OAAO,QAAQ,oBAAoB,YAAa;AACpD,QAAK,CAAE,QAAQ,gBAAiB,EAAE,oBAAoB,KAAK,CAAE,GAAI;AAChE,aAAO;AAAA,IACR;AAAA,EACD,OAAO;AACN,UAAM,aACL,QAAQ,cAAc,aAAa;AAAA,MAClC;AAAA,IACD,EAAE;AACH,QAAK,eAAe,YAAY,eAAe,YAAa;AAC3D,aAAO;AAAA,IACR;AAAA,EACD;AAEA,SACC,QAAQ,cAAc,KACtB,QAAQ,eAAe,KACvB,QAAQ,eAAe,EAAE,SAAS;AAEpC;AAWA,SAAS,qBAAsB,SAAU;AAExC,QAAM,MAAM,QAAQ,QAAS,WAAY;AACzC,MAAK,CAAE,KAAM;AACZ,WAAO;AAAA,EACR;AAGA,QAAM,MAAM,QAAQ,cAAc;AAAA,IACjC,kBAAkB,IAAI,OAAO;AAAA,EAC9B;AACA,SAAO,CAAC,CAAE,OAAO,CAAE,IAAI,QAAS,SAAU,KAAK,UAAW,GAAI;AAC/D;AAgBO,SAAS,KAAM,SAAS,EAAE,aAAa,MAAM,IAAI,CAAC,GAAI;AAE5D,QAAM,WAAW,QAAQ,iBAAkB,cAAe,UAAW,CAAE;AAEvE,SAAO,MAAM,KAAM,QAAS,EAAE,OAAQ,CAAE,YAAa;AACpD,UAAM,EAAE,SAAS,IAAI;AACrB,QAAK,WAAW,UAAW;AAE1B,aAAO;AAAA;AAAA,QAC2B;AAAA,MAClC;AAAA,IACD;AAGA,QAAK,QAAQ,QAAS,SAAU,GAAI;AACnC,aAAO;AAAA,IACR;AAEA,WAAO,UAAW,OAAQ;AAAA,EAC3B,CAAE;AACH;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-rectangle-from-range.d.ts","sourceRoot":"","sources":["../../src/dom/get-rectangle-from-range.js"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,UAAU,qBAAqB,CAAE,KAAK,EAJzC,KAIyC,GAFxC,OAAO,OAAC,
|
|
1
|
+
{"version":3,"file":"get-rectangle-from-range.d.ts","sourceRoot":"","sources":["../../src/dom/get-rectangle-from-range.js"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,UAAU,qBAAqB,CAAE,KAAK,EAJzC,KAIyC,GAFxC,OAAO,OAAC,CAmKnB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"focusable.d.ts","sourceRoot":"","sources":["../src/focusable.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAmFH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,IAAI,CAAE,OAAO,EAXlB,OAWkB,EAAE,EAAE,UAAkB,EAAE,GATlD;IAA0B,UAAU,AAApC,CAOA,EAPQ,OAAO,CAOf;CAEuD,GAF9C,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"focusable.d.ts","sourceRoot":"","sources":["../src/focusable.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAmFH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,IAAI,CAAE,OAAO,EAXlB,OAWkB,EAAE,EAAE,UAAkB,EAAE,GATlD;IAA0B,UAAU,AAApC,CAOA,EAPQ,OAAO,CAOf;CAEuD,GAF9C,WAAW,EAAE,CAsBxB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wordpress/dom",
|
|
3
|
-
"version": "4.55.0",
|
|
3
|
+
"version": "4.55.2-next.v.202609171837.0",
|
|
4
4
|
"description": "DOM utilities module for WordPress.",
|
|
5
5
|
"author": "The WordPress Contributors",
|
|
6
6
|
"license": "GPL-2.0-or-later",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"types": "build-types",
|
|
45
45
|
"sideEffects": false,
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@wordpress/deprecated": "^4.55.0"
|
|
47
|
+
"@wordpress/deprecated": "^4.55.1-next.v.202609171837.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"vitest": "^5.0.0"
|
|
@@ -52,5 +52,5 @@
|
|
|
52
52
|
"publishConfig": {
|
|
53
53
|
"access": "public"
|
|
54
54
|
},
|
|
55
|
-
"gitHead": "
|
|
55
|
+
"gitHead": "7fc9f6676debc888d2f071d17cb71837040d4b48"
|
|
56
56
|
}
|
|
@@ -63,21 +63,72 @@ export default function getRectangleFromRange( range ) {
|
|
|
63
63
|
);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
const { startContainer } = range;
|
|
66
|
+
const { startContainer, startOffset } = range;
|
|
67
67
|
const { ownerDocument } = startContainer;
|
|
68
|
+
assertIsDefined( ownerDocument, 'ownerDocument' );
|
|
68
69
|
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
assertIsDefined( ownerDocument, 'ownerDocument' );
|
|
70
|
+
// A range inside an element with no children, like a line break or the
|
|
71
|
+
// placeholder, is the same spot as the position before that element.
|
|
72
|
+
if (
|
|
73
|
+
startContainer.nodeType !== startContainer.TEXT_NODE &&
|
|
74
|
+
! startContainer.childNodes.length &&
|
|
75
|
+
startContainer.parentNode
|
|
76
|
+
) {
|
|
78
77
|
range = ownerDocument.createRange();
|
|
79
|
-
range.
|
|
80
|
-
range.
|
|
78
|
+
range.setStartBefore( startContainer );
|
|
79
|
+
range.collapse( true );
|
|
80
|
+
return getRectangleFromRange( range );
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// A collapsed range at an element offset has no rectangle. Translate it
|
|
84
|
+
// to the text offset next to it: the end of the text before or the start
|
|
85
|
+
// of the text after. When those sit on different lines the caret could
|
|
86
|
+
// be on either, so return null.
|
|
87
|
+
if ( startContainer.nodeType !== startContainer.TEXT_NODE ) {
|
|
88
|
+
let before = startContainer.childNodes[ startOffset - 1 ];
|
|
89
|
+
while ( before?.lastChild ) {
|
|
90
|
+
before = before.lastChild;
|
|
91
|
+
}
|
|
92
|
+
let after = startContainer.childNodes[ startOffset ];
|
|
93
|
+
while ( after?.firstChild ) {
|
|
94
|
+
after = after.firstChild;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let beforeRange;
|
|
98
|
+
if ( before && before.nodeType === before.TEXT_NODE ) {
|
|
99
|
+
beforeRange = ownerDocument.createRange();
|
|
100
|
+
beforeRange.setStart(
|
|
101
|
+
before,
|
|
102
|
+
/** @type {Text} */ ( before ).length
|
|
103
|
+
);
|
|
104
|
+
beforeRange.collapse( true );
|
|
105
|
+
}
|
|
106
|
+
let afterRange;
|
|
107
|
+
if ( after && after.nodeType === after.TEXT_NODE ) {
|
|
108
|
+
afterRange = ownerDocument.createRange();
|
|
109
|
+
afterRange.setStart( after, 0 );
|
|
110
|
+
afterRange.collapse( true );
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if ( beforeRange && afterRange ) {
|
|
114
|
+
const beforeRect = beforeRange.getClientRects()[ 0 ];
|
|
115
|
+
const afterRect = afterRange.getClientRects()[ 0 ];
|
|
116
|
+
if (
|
|
117
|
+
beforeRect &&
|
|
118
|
+
afterRect &&
|
|
119
|
+
beforeRect.bottom <= afterRect.top
|
|
120
|
+
) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if ( afterRange ) {
|
|
126
|
+
range = afterRange;
|
|
127
|
+
} else if ( beforeRange ) {
|
|
128
|
+
range = beforeRange;
|
|
129
|
+
} else {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
81
132
|
}
|
|
82
133
|
|
|
83
134
|
const rects = range.getClientRects();
|
|
@@ -118,23 +169,5 @@ export default function getRectangleFromRange( range ) {
|
|
|
118
169
|
}
|
|
119
170
|
}
|
|
120
171
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// If the collapsed range starts (and therefore ends) at an element node,
|
|
124
|
-
// `getClientRects` can be empty in some browsers. This can be resolved
|
|
125
|
-
// by adding a temporary text node with zero-width space to the range.
|
|
126
|
-
//
|
|
127
|
-
// See: https://stackoverflow.com/a/6847328/995445
|
|
128
|
-
if ( ! rect || rect.height === 0 ) {
|
|
129
|
-
assertIsDefined( ownerDocument, 'ownerDocument' );
|
|
130
|
-
const padNode = ownerDocument.createTextNode( '\u200b' );
|
|
131
|
-
// Do not modify the live range.
|
|
132
|
-
range = range.cloneRange();
|
|
133
|
-
range.insertNode( padNode );
|
|
134
|
-
rect = range.getClientRects()[ 0 ];
|
|
135
|
-
assertIsDefined( padNode.parentNode, 'padNode.parentNode' );
|
|
136
|
-
padNode.parentNode.removeChild( padNode );
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return rect;
|
|
172
|
+
return rects[ 0 ] ?? null;
|
|
140
173
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import getRectangleFromRange from '../get-rectangle-from-range';
|
|
3
|
+
|
|
4
|
+
describe( 'getRectangleFromRange', () => {
|
|
5
|
+
let container;
|
|
6
|
+
|
|
7
|
+
beforeEach( () => {
|
|
8
|
+
container = document.createElement( 'div' );
|
|
9
|
+
document.body.appendChild( container );
|
|
10
|
+
} );
|
|
11
|
+
|
|
12
|
+
afterEach( () => {
|
|
13
|
+
container.remove();
|
|
14
|
+
} );
|
|
15
|
+
|
|
16
|
+
function collapsedRange( node, offset ) {
|
|
17
|
+
const range = document.createRange();
|
|
18
|
+
range.setStart( node, offset );
|
|
19
|
+
range.collapse( true );
|
|
20
|
+
return range;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The caret rectangle the browser gives for a position in a text node.
|
|
24
|
+
function caretBox( node, offset ) {
|
|
25
|
+
return collapsedRange( node, offset ).getClientRects()[ 0 ].toJSON();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function box( rect ) {
|
|
29
|
+
return rect.toJSON();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
it( 'measures a position after the last child of an element at the end of its text', () => {
|
|
33
|
+
container.innerHTML = '<div>wordpress.org</div>';
|
|
34
|
+
const element = container.firstChild;
|
|
35
|
+
const range = collapsedRange( element, 1 );
|
|
36
|
+
const mutations = [];
|
|
37
|
+
new window.MutationObserver( ( records ) =>
|
|
38
|
+
mutations.push( ...records )
|
|
39
|
+
).observe( element, { childList: true, subtree: true } );
|
|
40
|
+
|
|
41
|
+
expect( range.getClientRects() ).toHaveLength( 0 );
|
|
42
|
+
expect( box( getRectangleFromRange( range ) ) ).toEqual(
|
|
43
|
+
caretBox( element.firstChild, 13 )
|
|
44
|
+
);
|
|
45
|
+
expect( range.startContainer ).toBe( element );
|
|
46
|
+
return Promise.resolve().then( () => {
|
|
47
|
+
expect( mutations ).toEqual( [] );
|
|
48
|
+
} );
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
it( 'measures a position before the first child of an element at the start of its text', () => {
|
|
52
|
+
container.innerHTML = '<div><em>abc</em>def</div>';
|
|
53
|
+
const element = container.firstChild;
|
|
54
|
+
|
|
55
|
+
expect(
|
|
56
|
+
box( getRectangleFromRange( collapsedRange( element, 0 ) ) )
|
|
57
|
+
).toEqual( caretBox( element.querySelector( 'em' ).firstChild, 0 ) );
|
|
58
|
+
} );
|
|
59
|
+
|
|
60
|
+
it( 'measures a position after a line break at the start of the next line', () => {
|
|
61
|
+
container.innerHTML = '<p>abc<br>def</p>';
|
|
62
|
+
const paragraph = container.firstChild;
|
|
63
|
+
const nextLine = caretBox( paragraph.lastChild, 0 );
|
|
64
|
+
|
|
65
|
+
expect( nextLine.top ).toBeGreaterThan(
|
|
66
|
+
caretBox( paragraph.firstChild, 3 ).top
|
|
67
|
+
);
|
|
68
|
+
expect(
|
|
69
|
+
box( getRectangleFromRange( collapsedRange( paragraph, 2 ) ) )
|
|
70
|
+
).toEqual( nextLine );
|
|
71
|
+
} );
|
|
72
|
+
|
|
73
|
+
it( 'returns null for a position between texts on different lines', () => {
|
|
74
|
+
container.innerHTML =
|
|
75
|
+
'<p style="width: 3ch; font-family: monospace; overflow-wrap: anywhere;">abc<em>def</em></p>';
|
|
76
|
+
const paragraph = container.firstChild;
|
|
77
|
+
|
|
78
|
+
expect(
|
|
79
|
+
caretBox( paragraph.lastChild.firstChild, 0 ).top
|
|
80
|
+
).toBeGreaterThan( caretBox( paragraph.firstChild, 3 ).top );
|
|
81
|
+
expect(
|
|
82
|
+
getRectangleFromRange( collapsedRange( paragraph, 1 ) )
|
|
83
|
+
).toBeNull();
|
|
84
|
+
} );
|
|
85
|
+
|
|
86
|
+
it( 'measures a position between texts on the same line', () => {
|
|
87
|
+
container.innerHTML = '<p>abc<em>def</em></p>';
|
|
88
|
+
const paragraph = container.firstChild;
|
|
89
|
+
|
|
90
|
+
expect(
|
|
91
|
+
box( getRectangleFromRange( collapsedRange( paragraph, 1 ) ) )
|
|
92
|
+
).toEqual( caretBox( paragraph.lastChild.firstChild, 0 ) );
|
|
93
|
+
} );
|
|
94
|
+
|
|
95
|
+
it( 'measures a position inside an empty inline element beside the text next to it', () => {
|
|
96
|
+
container.innerHTML =
|
|
97
|
+
'<p><span data-rich-text-placeholder="Type"></span></p>';
|
|
98
|
+
const paragraph = container.firstChild;
|
|
99
|
+
const placeholder = paragraph.querySelector( 'span' );
|
|
100
|
+
|
|
101
|
+
expect(
|
|
102
|
+
box( getRectangleFromRange( collapsedRange( placeholder, 0 ) ) )
|
|
103
|
+
).toEqual( caretBox( paragraph.firstChild, 1 ) );
|
|
104
|
+
} );
|
|
105
|
+
|
|
106
|
+
it( 'measures a position inside a line break at the end of the text before it', () => {
|
|
107
|
+
container.innerHTML = '<p>abc<br>def</p>';
|
|
108
|
+
const paragraph = container.firstChild;
|
|
109
|
+
|
|
110
|
+
expect(
|
|
111
|
+
box(
|
|
112
|
+
getRectangleFromRange(
|
|
113
|
+
collapsedRange( paragraph.querySelector( 'br' ), 0 )
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
).toEqual( caretBox( paragraph.firstChild, 3 ) );
|
|
117
|
+
} );
|
|
118
|
+
|
|
119
|
+
it( 'returns null for a position with no text on either side', () => {
|
|
120
|
+
container.innerHTML = '<p><br></p>';
|
|
121
|
+
const paragraph = container.firstChild;
|
|
122
|
+
|
|
123
|
+
expect(
|
|
124
|
+
getRectangleFromRange( collapsedRange( paragraph, 1 ) )
|
|
125
|
+
).toBeNull();
|
|
126
|
+
expect( paragraph.innerHTML ).toBe( '<br>' );
|
|
127
|
+
} );
|
|
128
|
+
} );
|
package/src/focusable.js
CHANGED
|
@@ -95,7 +95,7 @@ function isValidFocusableArea( element ) {
|
|
|
95
95
|
const img = element.ownerDocument.querySelector(
|
|
96
96
|
'img[usemap="#' + map.name + '"]'
|
|
97
97
|
);
|
|
98
|
-
return !! img && isVisible( img );
|
|
98
|
+
return !! img && ! img.closest( '[inert]' ) && isVisible( img );
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/**
|
|
@@ -117,22 +117,19 @@ export function find( context, { sequential = false } = {} ) {
|
|
|
117
117
|
const elements = context.querySelectorAll( buildSelector( sequential ) );
|
|
118
118
|
|
|
119
119
|
return Array.from( elements ).filter( ( element ) => {
|
|
120
|
-
if ( ! isVisible( element ) ) {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Elements inside an inert subtree are not focusable.
|
|
125
|
-
if ( element.closest( '[inert]' ) ) {
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
120
|
const { nodeName } = element;
|
|
130
121
|
if ( 'AREA' === nodeName ) {
|
|
122
|
+
// The mapped image determines whether this region is visible or inert.
|
|
131
123
|
return isValidFocusableArea(
|
|
132
124
|
/** @type {HTMLAreaElement} */ ( element )
|
|
133
125
|
);
|
|
134
126
|
}
|
|
135
127
|
|
|
136
|
-
|
|
128
|
+
// Elements inside an inert subtree are not focusable.
|
|
129
|
+
if ( element.closest( '[inert]' ) ) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return isVisible( element );
|
|
137
134
|
} );
|
|
138
135
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { find } from '../focusable';
|
|
3
|
+
|
|
4
|
+
function createImageWithLayout() {
|
|
5
|
+
const image = document.createElement( 'img' );
|
|
6
|
+
|
|
7
|
+
Object.defineProperties( image, {
|
|
8
|
+
offsetHeight: { value: 10 },
|
|
9
|
+
offsetWidth: { value: 10 },
|
|
10
|
+
} );
|
|
11
|
+
|
|
12
|
+
return image;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe( 'focusable.find() image map areas', () => {
|
|
16
|
+
beforeEach( () => {
|
|
17
|
+
document.body.innerHTML = '';
|
|
18
|
+
} );
|
|
19
|
+
|
|
20
|
+
it( 'finds an area in a named map referenced by an image', () => {
|
|
21
|
+
const map = document.createElement( 'map' );
|
|
22
|
+
map.name = 'testfocus';
|
|
23
|
+
const area = document.createElement( 'area' );
|
|
24
|
+
area.href = '';
|
|
25
|
+
map.appendChild( area );
|
|
26
|
+
const image = createImageWithLayout();
|
|
27
|
+
image.setAttribute( 'usemap', '#testfocus' );
|
|
28
|
+
document.body.append( map, image );
|
|
29
|
+
|
|
30
|
+
expect( find( map ) ).toEqual( [ area ] );
|
|
31
|
+
} );
|
|
32
|
+
|
|
33
|
+
it( 'ignores an area when no image references its map', () => {
|
|
34
|
+
const map = document.createElement( 'map' );
|
|
35
|
+
map.name = 'testfocus';
|
|
36
|
+
const area = document.createElement( 'area' );
|
|
37
|
+
area.href = '';
|
|
38
|
+
map.appendChild( area );
|
|
39
|
+
document.body.append( map );
|
|
40
|
+
|
|
41
|
+
expect( find( map ) ).toEqual( [] );
|
|
42
|
+
} );
|
|
43
|
+
} );
|