Skip to content

Commit

Permalink
Refactored selector API to support multiple types
Browse files Browse the repository at this point in the history
Selectors can now be: React component, accessibility role, display text, or data-testname.
  • Loading branch information
Brian Vaughn committed Apr 14, 2020
1 parent 774c7ec commit 41d3047
Show file tree
Hide file tree
Showing 9 changed files with 746 additions and 355 deletions.
479 changes: 292 additions & 187 deletions packages/react-dom/src/__tests__/ReactDOMTestSelectors-test.internal.js

Large diffs are not rendered by default.

140 changes: 140 additions & 0 deletions packages/react-dom/src/client/DOMAccessibilityRoles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

// Below code forked from dom-accessibility-api

const tagToRoleMappings = {
ARTICLE: 'article',
ASIDE: 'complementary',
BODY: 'document',
BUTTON: 'button',
DATALIST: 'listbox',
DD: 'definition',
DETAILS: 'group',
DIALOG: 'dialog',
DT: 'term',
FIELDSET: 'group',
FIGURE: 'figure',
// WARNING: Only with an accessible name
FORM: 'form',
FOOTER: 'contentinfo',
H1: 'heading',
H2: 'heading',
H3: 'heading',
H4: 'heading',
H5: 'heading',
H6: 'heading',
HEADER: 'banner',
HR: 'separator',
LEGEND: 'legend',
LI: 'listitem',
MATH: 'math',
MAIN: 'main',
MENU: 'list',
NAV: 'navigation',
OL: 'list',
OPTGROUP: 'group',
// WARNING: Only in certain context
OPTION: 'option',
OUTPUT: 'status',
PROGRESS: 'progressbar',
// WARNING: Only with an accessible name
SECTION: 'region',
SUMMARY: 'button',
TABLE: 'table',
TBODY: 'rowgroup',
TEXTAREA: 'textbox',
TFOOT: 'rowgroup',
// WARNING: Only in certain context
TD: 'cell',
TH: 'columnheader',
THEAD: 'rowgroup',
TR: 'row',
UL: 'list',
};

function getImplicitRole(element: Element): string | null {
const mappedByTag = tagToRoleMappings[element.tagName];
if (mappedByTag !== undefined) {
return mappedByTag;
}

switch (element.tagName) {
case 'A':
case 'AREA':
case 'LINK':
if (element.hasAttribute('href')) {
return 'link';
}
break;
case 'IMG':
if ((element.getAttribute('alt') || '').length > 0) {
return 'img';
}
break;
case 'INPUT': {
const type = (element: any).type;
switch (type) {
case 'button':
case 'image':
case 'reset':
case 'submit':
return 'button';
case 'checkbox':
case 'radio':
return type;
case 'range':
return 'slider';
case 'email':
case 'tel':
case 'text':
case 'url':
if (element.hasAttribute('list')) {
return 'combobox';
}
return 'textbox';
case 'search':
if (element.hasAttribute('list')) {
return 'combobox';
}
return 'searchbox';
default:
return null;
}
}

case 'SELECT':
if (element.hasAttribute('multiple') || (element: any).size > 1) {
return 'listbox';
}
return 'combobox';
}

return null;
}

function getExplicitRole(element: Element): string | null {
const role = element.getAttribute('role');
if (role) {
return role.trim().split(' ')[0];
}

return null;
}

// https://w3c.github.io/html-aria/#document-conformance-requirements-for-use-of-aria-attributes-in-html
export function getRole(element: Element): string | null {
const explicitRole = getExplicitRole(element);

if (explicitRole !== null) {
return explicitRole;
}

return getImplicitRole(element);
}
45 changes: 42 additions & 3 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
getInstanceFromNode as getInstanceFromNodeDOMTree,
isContainerMarkedAsRoot,
} from './ReactDOMComponentTree';
import {getRole} from './DOMAccessibilityRoles';
import {
createElement,
createTextNode,
Expand Down Expand Up @@ -81,7 +82,7 @@ import {
enableUseEventAPI,
enableScopeAPI,
} from 'shared/ReactFeatureFlags';
import {HostComponent} from 'react-reconciler/src/ReactWorkTags';
import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags';
import {
RESPONDER_EVENT_SYSTEM,
IS_PASSIVE,
Expand Down Expand Up @@ -1284,8 +1285,9 @@ export const supportsTestSelectors = true;

export function findRootFiber(node: Instance): null | Fiber {
const stack = [node];
while (stack.length > 0) {
const current = stack.shift();
let index = 0;
while (index < stack.length) {
const current = stack[index++];
if (isContainerMarkedAsRoot(current)) {
const root = ((getInstanceFromNodeDOMTree(current): any): Fiber);
return root.stateNode.current;
Expand All @@ -1305,6 +1307,43 @@ export function getBoundingRect(node: Instance): BoundingRect {
};
}

export function matchAccessibilityRole(
fiber: Fiber,
targetRole: string,
): boolean {
if (fiber.tag === HostComponent) {
const node = fiber.stateNode;
const inferredRole = getRole(node);
if (targetRole === inferredRole) {
return true;
}
if (node.getAttribute('role') === targetRole) {
return true;
}
}

return false;
}

export function getTextContent(fiber: Fiber): string | null {
switch (fiber.tag) {
case HostComponent:
let textContent = '';
const childNodes = fiber.stateNode.childNodes;
for (let i = 0; i < childNodes.length; i++) {
const childNode = childNodes[i];
if (childNode.nodeType === Node.TEXT_NODE) {
textContent += childNode.textContent;
}
}
return textContent;
case HostText:
return fiber.stateNode.textContent;
}

return null;
}

export function isHiddenSubtree(workInProgress: Fiber): boolean {
return workInProgress.pendingProps.hidden === true;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/react-dom/testing.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
export * from './index.js';
export {
act,
createComponentSelector,
createHasPsuedoClassSelector,
createRoleSelector,
createTestNameSelector,
createTextSelector,
getFindAllNodesFailureDescription,
findAllNodes,
findBoundingRects,
Expand Down
25 changes: 25 additions & 0 deletions packages/react-reconciler/src/ReactFiberReconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ import {
injectIntoDevTools as injectIntoDevTools_old,
act as act_old,
createPortal as createPortal_old,
createComponentSelector as createComponentSelector_old,
createHasPsuedoClassSelector as createHasPsuedoClassSelector_old,
createRoleSelector as createRoleSelector_old,
createTestNameSelector as createTestNameSelector_old,
createTextSelector as createTextSelector_old,
getFindAllNodesFailureDescription as getFindAllNodesFailureDescription_old,
findAllNodes as findAllNodes_old,
findBoundingRects as findBoundingRects_old,
Expand Down Expand Up @@ -73,6 +78,11 @@ import {
injectIntoDevTools as injectIntoDevTools_new,
act as act_new,
createPortal as createPortal_new,
createComponentSelector as createComponentSelector_new,
createHasPsuedoClassSelector as createHasPsuedoClassSelector_new,
createRoleSelector as createRoleSelector_new,
createTestNameSelector as createTestNameSelector_new,
createTextSelector as createTextSelector_new,
getFindAllNodesFailureDescription as getFindAllNodesFailureDescription_new,
findAllNodes as findAllNodes_new,
findBoundingRects as findBoundingRects_new,
Expand Down Expand Up @@ -151,6 +161,21 @@ export const act = enableNewReconciler ? act_new : act_old;
export const createPortal = enableNewReconciler
? createPortal_new
: createPortal_old;
export const createComponentSelector = enableNewReconciler
? createComponentSelector_new
: createComponentSelector_old;
export const createHasPsuedoClassSelector = enableNewReconciler
? createHasPsuedoClassSelector_new
: createHasPsuedoClassSelector_old;
export const createRoleSelector = enableNewReconciler
? createRoleSelector_new
: createRoleSelector_old;
export const createTextSelector = enableNewReconciler
? createTextSelector_new
: createTextSelector_old;
export const createTestNameSelector = enableNewReconciler
? createTestNameSelector_new
: createTestNameSelector_old;
export const getFindAllNodesFailureDescription = enableNewReconciler
? getFindAllNodesFailureDescription_new
: getFindAllNodesFailureDescription_old;
Expand Down
5 changes: 5 additions & 0 deletions packages/react-reconciler/src/ReactFiberReconciler.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ import {

export {createPortal} from './ReactPortal';
export {
createComponentSelector,
createHasPsuedoClassSelector,
createRoleSelector,
createTestNameSelector,
createTextSelector,
getFindAllNodesFailureDescription,
findAllNodes,
findBoundingRects,
Expand Down
5 changes: 5 additions & 0 deletions packages/react-reconciler/src/ReactFiberReconciler.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ import {

export {createPortal} from './ReactPortal';
export {
createComponentSelector,
createHasPsuedoClassSelector,
createRoleSelector,
createTestNameSelector,
createTextSelector,
getFindAllNodesFailureDescription,
findAllNodes,
findBoundingRects,
Expand Down

0 comments on commit 41d3047

Please sign in to comment.