diff --git a/src/__tests__/role-helpers.js b/src/__tests__/role-helpers.js
index f47daa42..e8714f4f 100644
--- a/src/__tests__/role-helpers.js
+++ b/src/__tests__/role-helpers.js
@@ -4,6 +4,7 @@ import {
logRoles,
getImplicitAriaRoles,
isInaccessible,
+ isSubtreeInaccessible,
} from '../role-helpers'
import {render} from './helpers/test-utils'
@@ -25,7 +26,7 @@ function setup() {
invalid link
-
+
Main Heading
Sub Heading
Tertiary Heading
@@ -211,3 +212,58 @@ test.each([
expect(isInaccessible(container.querySelector('button'))).toBe(expected)
})
+
+describe('checkVisibility API integration', () => {
+ beforeEach(() => {
+ if (Element.prototype.checkVisibility) {
+ delete Element.prototype.checkVisibility
+ }
+ })
+
+ test('uses checkVisibility when available', () => {
+ const mockCheckVisibility = jest.fn().mockReturnValue(false)
+ Element.prototype.checkVisibility = mockCheckVisibility
+
+ const {container} = render('')
+ const button = container.querySelector('button')
+
+ const result = isSubtreeInaccessible(button)
+
+ expect(mockCheckVisibility).toHaveBeenCalledWith({
+ visibilityProperty: true,
+ opacityProperty: false,
+ })
+ expect(result).toBe(true)
+ })
+
+ test('falls back to getComputedStyle when checkVisibility unavailable', () => {
+ const {container} = render('')
+ const button = container.querySelector('button')
+
+ expect(isSubtreeInaccessible(button)).toBe(true)
+ })
+
+ test('checkVisibility and fallback produce same results', () => {
+ const testCases = [
+ '',
+ '',
+ '',
+ '',
+ '',
+ ]
+
+ testCases.forEach(html => {
+ const {container: container1} = render(html)
+ const button1 = container1.querySelector('button')
+
+ const resultWithAPI = isInaccessible(button1)
+
+ delete Element.prototype.checkVisibility
+ const {container: container2} = render(html)
+ const button2 = container2.querySelector('button')
+ const resultWithoutAPI = isInaccessible(button2)
+
+ expect(resultWithAPI).toBe(resultWithoutAPI)
+ })
+ })
+})
diff --git a/src/role-helpers.js b/src/role-helpers.js
index 8d7e7cb0..d1b55a8d 100644
--- a/src/role-helpers.js
+++ b/src/role-helpers.js
@@ -8,6 +8,17 @@ import {getConfig} from './config'
const elementRoleList = buildElementRoleList(elementRoles)
+const checkVisibilityOptions = {
+ visibilityProperty: true,
+ opacityProperty: false,
+}
+
+function supportsCheckVisibility() {
+ return (
+ typeof Element !== 'undefined' && 'checkVisibility' in Element.prototype
+ )
+}
+
/**
* @param {Element} element -
* @returns {boolean} - `true` if `element` and its subtree are inaccessible
@@ -21,6 +32,10 @@ function isSubtreeInaccessible(element) {
return true
}
+ if (supportsCheckVisibility()) {
+ return !element.checkVisibility(checkVisibilityOptions)
+ }
+
const window = element.ownerDocument.defaultView
if (window.getComputedStyle(element).display === 'none') {
return true
@@ -47,6 +62,27 @@ function isInaccessible(element, options = {}) {
const {
isSubtreeInaccessible: isSubtreeInaccessibleImpl = isSubtreeInaccessible,
} = options
+
+ if (supportsCheckVisibility()) {
+ if (!element.checkVisibility(checkVisibilityOptions)) {
+ return true
+ }
+
+ // Still need to walk up the tree for aria-hidden and hidden attributes
+ let currentElement = element
+ while (currentElement) {
+ if (
+ currentElement.hidden === true ||
+ currentElement.getAttribute('aria-hidden') === 'true'
+ ) {
+ return true
+ }
+ currentElement = currentElement.parentElement
+ }
+
+ return false
+ }
+
const window = element.ownerDocument.defaultView
// since visibility is inherited we can exit early
if (window.getComputedStyle(element).visibility === 'hidden') {