fix(role): handle display:contents elements (#23607)

Fixes #23521.
This commit is contained in:
Dmitry Gozman 2023-06-08 16:00:48 -07:00 committed by GitHub
parent b92dc47665
commit 11659ceb73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 22 additions and 4 deletions

View file

@ -103,7 +103,7 @@ export function isElementVisible(element: Element): boolean {
return rect.width > 0 && rect.height > 0;
}
function isVisibleTextNode(node: Text) {
export function isVisibleTextNode(node: Text) {
// https://stackoverflow.com/questions/1461059/is-there-an-equivalent-to-getboundingclientrect-for-text-nodes
const range = node.ownerDocument.createRange();
range.selectNode(node);

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { closestCrossShadow, enclosingShadowRootOrDocument, getElementComputedStyle, isElementStyleVisibilityVisible, parentElementOrShadowHost } from './domUtils';
import { closestCrossShadow, enclosingShadowRootOrDocument, getElementComputedStyle, isElementStyleVisibilityVisible, isVisibleTextNode, parentElementOrShadowHost } from './domUtils';
function hasExplicitAccessibleName(e: Element) {
return e.hasAttribute('aria-label') || e.hasAttribute('aria-labelledby');
@ -238,11 +238,22 @@ function getAriaBoolean(attr: string | null) {
export function isElementHiddenForAria(element: Element, cache: Map<Element, boolean>): boolean {
if (['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(element.tagName))
return true;
const style = getElementComputedStyle(element);
const isSlot = element.nodeName === 'SLOT';
if (style?.display === 'contents' && !isSlot) {
// display:contents is not rendered itself, but its child nodes are.
for (let child = element.firstChild; child; child = child.nextSibling) {
if (child.nodeType === 1 /* Node.ELEMENT_NODE */ && !isElementHiddenForAria(child as Element, cache))
return false;
if (child.nodeType === 3 /* Node.TEXT_NODE */ && isVisibleTextNode(child as Text))
return false;
}
return true;
}
// Note: <option> inside <select> are not affected by visibility or content-visibility.
// Same goes for <slot>.
const isOptionInsideSelect = element.nodeName === 'OPTION' && !!element.closest('select');
const isSlot = element.nodeName === 'SLOT';
if (!isOptionInsideSelect && !isSlot && !isElementStyleVisibilityVisible(element))
if (!isOptionInsideSelect && !isSlot && !isElementStyleVisibilityVisible(element, style))
return true;
return belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element, cache);
}

View file

@ -299,6 +299,13 @@ test('native controls labelled-by', async ({ page }) => {
expect.soft(await getNameAndRole(page, '#textarea1')).toEqual({ role: 'textbox', name: 'TEXTAREA1 MORE2' });
});
test('display:contents should be visible when contents are visible', async ({ page }) => {
await page.setContent(`
<button style='display: contents;'>yo</button>
`);
await expect(page.getByRole('button')).toHaveCount(1);
});
function toArray(x: any): any[] {
return Array.isArray(x) ? x : [x];
}