From c7b45dfc7e13b48d3e3285b1cae61461aa3ed404 Mon Sep 17 00:00:00 2001 From: bwyard Date: Sun, 5 Apr 2026 00:12:13 -0500 Subject: [PATCH] fix(utils): update isShadowRoot to use spec-compliant custom element regex The previous regex only matched ASCII characters, causing valid custom elements with Unicode names (e.g. cafe-menu, math-pi) to be incorrectly rejected as shadow root candidates. The spec allows a broader set of Unicode characters in Potential Custom Element Names (PCEN). Also adds an explicit check for reserved names (annotation-xml, color-profile, font-face, etc.) that match the pattern but are explicitly excluded by the spec. Closes issue #5030 --- lib/core/utils/is-shadow-root.js | 22 +++++++++++++++++++++- test/core/utils/is-shadow-root.js | 12 ++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/lib/core/utils/is-shadow-root.js b/lib/core/utils/is-shadow-root.js index ab8aedd003..a0ecf173b2 100644 --- a/lib/core/utils/is-shadow-root.js +++ b/lib/core/utils/is-shadow-root.js @@ -18,6 +18,26 @@ const possibleShadowRoots = [ 'section', 'span' ]; + +// Reserved names that match the custom element regex but are not valid custom elements. +// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name +const reservedNames = [ + 'annotation-xml', + 'color-profile', + 'font-face', + 'font-face-src', + 'font-face-uri', + 'font-face-format', + 'font-face-name', + 'missing-glyph' +]; + +// Spec-compliant PCEN (Potential Custom Element Name) regex. +// Extends the ASCII-only regex to include Unicode characters permitted by the spec. +// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name +const customElementRegex = + /^[a-z](?:[a-z0-9._-]|[\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD])*-(?:[a-z0-9._-]|[\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD])*$/; + /** * Test a node to see if it has a spec-conforming shadow root * @@ -29,7 +49,7 @@ function isShadowRoot(node) { const nodeName = node.nodeName.toLowerCase(); if ( possibleShadowRoots.includes(nodeName) || - /^[a-z][a-z0-9_.-]*-[a-z0-9_.-]*$/.test(nodeName) + (!reservedNames.includes(nodeName) && customElementRegex.test(nodeName)) ) { return true; } diff --git a/test/core/utils/is-shadow-root.js b/test/core/utils/is-shadow-root.js index fbfc858e75..21e5f2eb57 100644 --- a/test/core/utils/is-shadow-root.js +++ b/test/core/utils/is-shadow-root.js @@ -33,6 +33,18 @@ describe('axe.utils.isShadowRoot', function () { assert.isFalse(isShadowRoot({ nodeName: '0-BUZZ', shadowRoot: {} })); assert.isFalse(isShadowRoot({ nodeName: '--ELM--', shadowRoot: {} })); }); + it('returns false for reserved custom element names', function () { + assert.isFalse( + isShadowRoot({ nodeName: 'ANNOTATION-XML', shadowRoot: {} }) + ); + assert.isFalse(isShadowRoot({ nodeName: 'COLOR-PROFILE', shadowRoot: {} })); + assert.isFalse(isShadowRoot({ nodeName: 'FONT-FACE', shadowRoot: {} })); + assert.isFalse(isShadowRoot({ nodeName: 'MISSING-GLYPH', shadowRoot: {} })); + }); + it('returns true for Unicode custom element names', function () { + assert.isTrue(isShadowRoot({ nodeName: 'CAF\u00C9-MENU', shadowRoot: {} })); + assert.isTrue(isShadowRoot({ nodeName: 'MATH-\u03A0', shadowRoot: {} })); + }); it('returns false if the native element does not allow shadow DOM', function () { assert.isFalse(isShadowRoot({ nodeName: 'IFRAME', shadowRoot: {} })); assert.isFalse(isShadowRoot({ nodeName: 'STRONG', shadowRoot: {} }));