@wordpress/dom 4.54.1-next.v.202608281122.0 → 4.55.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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 4.55.0 (2026-09-10)
6
+
7
+ ### Bug Fixes
8
+
9
+ - `focusable.find`: Exclude elements hidden by CSS `visibility` or `content-visibility` while preserving explicitly visible descendants ([#82574](https://github.com/WordPress/gutenberg/pull/82574)).
10
+
5
11
  ## 4.54.0 (2026-08-26)
6
12
 
7
13
  ### Bug Fixes
@@ -40,6 +40,18 @@ function buildSelector(sequential) {
40
40
  ].join(",");
41
41
  }
42
42
  function isVisible(element) {
43
+ if (typeof element.checkVisibility === "function") {
44
+ if (!element.checkVisibility({ visibilityProperty: true })) {
45
+ return false;
46
+ }
47
+ } else {
48
+ const visibility = element.ownerDocument.defaultView?.getComputedStyle(
49
+ element
50
+ ).visibility;
51
+ if (visibility === "hidden" || visibility === "collapse") {
52
+ return false;
53
+ }
54
+ }
43
55
  return element.offsetWidth > 0 || element.offsetHeight > 0 || element.getClientRects().length > 0;
44
56
  }
45
57
  function isValidFocusableArea(element) {
@@ -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 is visible (i.e. neither display: none\n * nor visibility: hidden).\n *\n * @param {HTMLElement} element DOM element to test.\n *\n * @return {boolean} Whether element is visible.\n */\nfunction isVisible( element ) {\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 ( ! isVisible( element ) ) {\n\t\t\treturn false;\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\tconst { nodeName } = element;\n\t\tif ( 'AREA' === nodeName ) {\n\t\t\treturn isValidFocusableArea(\n\t\t\t\t/** @type {HTMLAreaElement} */ ( element )\n\t\t\t);\n\t\t}\n\n\t\treturn true;\n\t} );\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;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,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;AACjC;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,QAAK,CAAE,UAAW,OAAQ,GAAI;AAC7B,aAAO;AAAA,IACR;AAGA,QAAK,QAAQ,QAAS,SAAU,GAAI;AACnC,aAAO;AAAA,IACR;AAEA,UAAM,EAAE,SAAS,IAAI;AACrB,QAAK,WAAW,UAAW;AAC1B,aAAO;AAAA;AAAA,QAC2B;AAAA,MAClC;AAAA,IACD;AAEA,WAAO;AAAA,EACR,CAAE;AACH;",
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 ( ! isVisible( element ) ) {\n\t\t\treturn false;\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\tconst { nodeName } = element;\n\t\tif ( 'AREA' === nodeName ) {\n\t\t\treturn isValidFocusableArea(\n\t\t\t\t/** @type {HTMLAreaElement} */ ( element )\n\t\t\t);\n\t\t}\n\n\t\treturn true;\n\t} );\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;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;AACjC;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,QAAK,CAAE,UAAW,OAAQ,GAAI;AAC7B,aAAO;AAAA,IACR;AAGA,QAAK,QAAQ,QAAS,SAAU,GAAI;AACnC,aAAO;AAAA,IACR;AAEA,UAAM,EAAE,SAAS,IAAI;AACrB,QAAK,WAAW,UAAW;AAC1B,aAAO;AAAA;AAAA,QAC2B;AAAA,MAClC;AAAA,IACD;AAEA,WAAO;AAAA,EACR,CAAE;AACH;",
6
6
  "names": []
7
7
  }
@@ -16,6 +16,18 @@ function buildSelector(sequential) {
16
16
  ].join(",");
17
17
  }
18
18
  function isVisible(element) {
19
+ if (typeof element.checkVisibility === "function") {
20
+ if (!element.checkVisibility({ visibilityProperty: true })) {
21
+ return false;
22
+ }
23
+ } else {
24
+ const visibility = element.ownerDocument.defaultView?.getComputedStyle(
25
+ element
26
+ ).visibility;
27
+ if (visibility === "hidden" || visibility === "collapse") {
28
+ return false;
29
+ }
30
+ }
19
31
  return element.offsetWidth > 0 || element.offsetHeight > 0 || element.getClientRects().length > 0;
20
32
  }
21
33
  function isValidFocusableArea(element) {
@@ -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 is visible (i.e. neither display: none\n * nor visibility: hidden).\n *\n * @param {HTMLElement} element DOM element to test.\n *\n * @return {boolean} Whether element is visible.\n */\nfunction isVisible( element ) {\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 ( ! isVisible( element ) ) {\n\t\t\treturn false;\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\tconst { nodeName } = element;\n\t\tif ( 'AREA' === nodeName ) {\n\t\t\treturn isValidFocusableArea(\n\t\t\t\t/** @type {HTMLAreaElement} */ ( element )\n\t\t\t);\n\t\t}\n\n\t\treturn true;\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,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;AACjC;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,QAAK,CAAE,UAAW,OAAQ,GAAI;AAC7B,aAAO;AAAA,IACR;AAGA,QAAK,QAAQ,QAAS,SAAU,GAAI;AACnC,aAAO;AAAA,IACR;AAEA,UAAM,EAAE,SAAS,IAAI;AACrB,QAAK,WAAW,UAAW;AAC1B,aAAO;AAAA;AAAA,QAC2B;AAAA,MAClC;AAAA,IACD;AAEA,WAAO;AAAA,EACR,CAAE;AACH;",
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 ( ! isVisible( element ) ) {\n\t\t\treturn false;\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\tconst { nodeName } = element;\n\t\tif ( 'AREA' === nodeName ) {\n\t\t\treturn isValidFocusableArea(\n\t\t\t\t/** @type {HTMLAreaElement} */ ( element )\n\t\t\t);\n\t\t}\n\n\t\treturn true;\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,UAAW,GAAI;AACjC;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,QAAK,CAAE,UAAW,OAAQ,GAAI;AAC7B,aAAO;AAAA,IACR;AAGA,QAAK,QAAQ,QAAS,SAAU,GAAI;AACnC,aAAO;AAAA,IACR;AAEA,UAAM,EAAE,SAAS,IAAI;AACrB,QAAK,WAAW,UAAW;AAC1B,aAAO;AAAA;AAAA,QAC2B;AAAA,MAClC;AAAA,IACD;AAEA,WAAO;AAAA,EACR,CAAE;AACH;",
6
6
  "names": []
7
7
  }
@@ -1 +1 @@
1
- {"version":3,"file":"focusable.d.ts","sourceRoot":"","sources":["../src/focusable.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAqEH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,IAAI,CAAE,OAAO,EAXlB,OAWkB,EAAE,EAAE,UAAkB,EAAE,GATlD;IAA0B,UAAU,AAApC,CAOA,EAPQ,OAAO,CAOf;CAEuD,GAF9C,WAAW,EAAE,CAyBxB"}
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,CAyBxB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/dom",
3
- "version": "4.54.1-next.v.202608281122.0",
3
+ "version": "4.55.0",
4
4
  "description": "DOM utilities module for WordPress.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -44,10 +44,13 @@
44
44
  "types": "build-types",
45
45
  "sideEffects": false,
46
46
  "dependencies": {
47
- "@wordpress/deprecated": "^4.54.1-next.v.202608281122.0"
47
+ "@wordpress/deprecated": "^4.55.0"
48
+ },
49
+ "devDependencies": {
50
+ "vitest": "^5.0.0"
48
51
  },
49
52
  "publishConfig": {
50
53
  "access": "public"
51
54
  },
52
- "gitHead": "de479d415171765f7b573c81aeec5334ff506a12"
55
+ "gitHead": "485f42ae8a1c58ceea18371a507fd4acfa86fbd8"
53
56
  }
@@ -1,3 +1,4 @@
1
+ import { describe, expect, it } from 'vitest';
1
2
  import safeHTML from '../safe-html';
2
3
 
3
4
  describe( 'safeHTML', () => {
@@ -1,3 +1,4 @@
1
+ import { describe, expect, it } from 'vitest';
1
2
  import stripHTML from '../strip-html';
2
3
 
3
4
  describe( 'stripHTML', () => {
package/src/focusable.js CHANGED
@@ -46,14 +46,28 @@ function buildSelector( sequential ) {
46
46
  }
47
47
 
48
48
  /**
49
- * Returns true if the specified element is visible (i.e. neither display: none
50
- * nor visibility: hidden).
49
+ * Returns true if the specified element has a layout box and is not hidden by
50
+ * CSS visibility or content visibility.
51
51
  *
52
52
  * @param {HTMLElement} element DOM element to test.
53
53
  *
54
54
  * @return {boolean} Whether element is visible.
55
55
  */
56
56
  function isVisible( element ) {
57
+ if ( typeof element.checkVisibility === 'function' ) {
58
+ if ( ! element.checkVisibility( { visibilityProperty: true } ) ) {
59
+ return false;
60
+ }
61
+ } else {
62
+ const visibility =
63
+ element.ownerDocument.defaultView?.getComputedStyle(
64
+ element
65
+ ).visibility;
66
+ if ( visibility === 'hidden' || visibility === 'collapse' ) {
67
+ return false;
68
+ }
69
+ }
70
+
57
71
  return (
58
72
  element.offsetWidth > 0 ||
59
73
  element.offsetHeight > 0 ||
@@ -1,3 +1,4 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
1
2
  import {
2
3
  isHorizontalEdge,
3
4
  placeCaretAtHorizontalEdge,
@@ -0,0 +1,83 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { find } from '../focusable';
3
+
4
+ describe( 'focusable.find() CSS visibility', () => {
5
+ let node;
6
+
7
+ beforeEach( () => {
8
+ node = document.createElement( 'div' );
9
+ document.body.appendChild( node );
10
+ } );
11
+
12
+ afterEach( () => {
13
+ node.remove();
14
+ } );
15
+
16
+ it.each( [
17
+ [ 'visibility: hidden', '<input style="visibility: hidden">' ],
18
+ [
19
+ 'inherited visibility: hidden',
20
+ '<div style="visibility: hidden"><input></div>',
21
+ ],
22
+ [ 'visibility: collapse', '<input style="visibility: collapse">' ],
23
+ [
24
+ 'content-visibility: hidden ancestor',
25
+ '<div style="content-visibility: hidden"><input></div>',
26
+ ],
27
+ ] )(
28
+ 'excludes inputs with %s even when they have layout boxes',
29
+ ( _, html ) => {
30
+ node.innerHTML = html;
31
+ const input = node.querySelector( 'input' );
32
+ const checkVisibility = vi.spyOn( input, 'checkVisibility' );
33
+
34
+ expect( input.offsetWidth ).toBeGreaterThan( 0 );
35
+ expect( input.offsetHeight ).toBeGreaterThan( 0 );
36
+ expect( input.getClientRects().length ).toBeGreaterThan( 0 );
37
+ input.focus();
38
+ expect( document.activeElement ).not.toBe( input );
39
+ expect( find( node ) ).toEqual( [] );
40
+ expect( checkVisibility ).toHaveBeenCalledWith( {
41
+ visibilityProperty: true,
42
+ } );
43
+ }
44
+ );
45
+
46
+ it( 'includes inputs that restore visibility inside a hidden ancestor', () => {
47
+ node.style.visibility = 'hidden';
48
+ node.innerHTML = '<input style="visibility: visible">';
49
+ const input = node.querySelector( 'input' );
50
+
51
+ input.focus();
52
+ expect( document.activeElement ).toBe( input );
53
+ expect( find( node ) ).toEqual( [ input ] );
54
+ } );
55
+
56
+ it( 'uses the owning window for the computed-style fallback', () => {
57
+ const iframe = document.createElement( 'iframe' );
58
+ node.appendChild( iframe );
59
+ const iframeDocument = iframe.contentDocument;
60
+ iframeDocument.body.innerHTML = `
61
+ <div style="visibility: hidden">
62
+ <input>
63
+ <input style="visibility: visible">
64
+ </div>
65
+ `;
66
+ const [ hiddenInput, visibleInput ] =
67
+ iframeDocument.querySelectorAll( 'input' );
68
+ Object.defineProperties( hiddenInput, {
69
+ checkVisibility: { value: undefined },
70
+ } );
71
+ Object.defineProperties( visibleInput, {
72
+ checkVisibility: { value: undefined },
73
+ } );
74
+ const getComputedStyle = vi.spyOn(
75
+ iframeDocument.defaultView,
76
+ 'getComputedStyle'
77
+ );
78
+
79
+ expect( find( iframeDocument.body ) ).toEqual( [ visibleInput ] );
80
+ expect( getComputedStyle ).toHaveBeenCalledWith( hiddenInput );
81
+ expect( getComputedStyle ).toHaveBeenCalledWith( visibleInput );
82
+ } );
83
+ } );
@@ -1,3 +1,4 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
1
2
  import createElement from './utils/create-element';
2
3
  import { find } from '../focusable';
3
4
 
@@ -107,6 +108,8 @@ describe( 'focusable', () => {
107
108
  const node = createElement( 'div' );
108
109
  const input = createElement( 'input' );
109
110
  node.appendChild( input );
111
+ // Keep the fixture connected so JSDOM invalidates computed styles.
112
+ document.body.appendChild( node );
110
113
 
111
114
  input.style.visibility = 'hidden';
112
115
  expect( find( node ) ).toEqual( [] );
@@ -125,6 +128,8 @@ describe( 'focusable', () => {
125
128
  const node = createElement( 'div' );
126
129
  const input = createElement( 'input' );
127
130
  node.appendChild( input );
131
+ // Keep the fixture connected so JSDOM invalidates computed styles.
132
+ document.body.appendChild( node );
128
133
 
129
134
  node.style.visibility = 'hidden';
130
135
  expect( find( node ) ).toEqual( [] );
@@ -1,3 +1,4 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
1
2
  import createElement from './utils/create-element';
2
3
  import { find } from '../tabbable';
3
4
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Given an element type, returns an HTMLElement with an emulated layout,
3
- * since JSDOM does have its own internal layout engine.
3
+ * since JSDOM does not have its own internal layout engine.
4
4
  *
5
5
  * @param {string} type Element type.
6
6
  *
@@ -14,9 +14,8 @@ export default function createElement( type ) {
14
14
  let isHidden = false;
15
15
  let node = this;
16
16
  do {
17
- isHidden =
18
- node.style.display === 'none' ||
19
- node.style.visibility === 'hidden';
17
+ // CSS visibility does not remove an element's layout box.
18
+ isHidden = node.style.display === 'none';
20
19
 
21
20
  node = node.parentNode;
22
21
  } while (
package/src/test/utils.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { describe, expect, it } from 'vitest';
1
2
  import { assertIsDefined } from '../utils/assert-is-defined';
2
3
 
3
4
  describe( 'assertIsDefined', () => {