focus-utils.js (6121B)
1 'use strict'; 2 3 function waitForRender() { 4 return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); 5 } 6 7 async function navigateFocusForward() { 8 await waitForRender(); 9 const kTab = '\uE004'; 10 await new test_driver.send_keys(document.body, kTab); 11 await waitForRender(); 12 } 13 14 async function navigateFocusBackward() { 15 await waitForRender(); 16 const kShift = '\uE008'; 17 const kTab = '\uE004'; 18 await new test_driver.Actions() 19 .keyDown(kShift) 20 .keyDown(kTab) 21 .keyUp(kTab) 22 .keyUp(kShift) 23 .send(); 24 await waitForRender(); 25 } 26 27 // If shadow root is open, can find element using element path 28 // If shadow root is open, can find the shadowRoot from the element 29 30 function innermostActiveElement(element) { 31 element = element || document.activeElement; 32 if (isIFrameElement(element)) { 33 if (element.contentDocument.activeElement) 34 return innermostActiveElement(element.contentDocument.activeElement); 35 return element; 36 } 37 if (isShadowHost(element)) { 38 let shadowRoot = element.shadowRoot; 39 if (shadowRoot) { 40 if (shadowRoot.activeElement) 41 return innermostActiveElement(shadowRoot.activeElement); 42 } 43 } 44 return element; 45 } 46 47 function isInnermostActiveElement(path) { 48 const element = getNodeInComposedTree(path); 49 if (!element) 50 return false; 51 return element === innermostActiveElement(); 52 } 53 54 async function shouldNavigateFocus(fromElement, direction) { 55 if (!fromElement) 56 return false; 57 58 fromElement.focus(); 59 if (fromElement !== innermostActiveElement()) 60 return false; 61 62 if (direction == 'forward') 63 await navigateFocusForward(); 64 else 65 await navigateFocusBackward(); 66 67 return true; 68 } 69 70 async function assert_focus_navigation_element(fromPath, toPath, direction) { 71 const fromElement = getNodeInComposedTree(fromPath); 72 const result = await shouldNavigateFocus(fromElement, direction); 73 assert_true(result, 'Failed to focus ' + fromPath); 74 75 const message = 76 `Focus should move ${direction} from ${fromPath} to ${toPath}`; 77 const toElement = getNodeInComposedTree(toPath); 78 assert_equals(innermostActiveElement(), toElement, message); 79 } 80 81 async function assert_focus_navigation_elements(elements, direction) { 82 assert_true( 83 elements.length >= 2, 84 'length of elements should be greater than or equal to 2.'); 85 for (var i = 0; i + 1 < elements.length; ++i) 86 await assert_focus_navigation_element(elements[i], elements[i + 1], direction); 87 88 } 89 90 async function assert_focus_navigation_forward(elements) { 91 return assert_focus_navigation_elements(elements, 'forward'); 92 } 93 94 async function assert_focus_navigation_backward(elements) { 95 return assert_focus_navigation_elements(elements, 'backward'); 96 } 97 98 async function assert_focus_navigation_bidirectional(elements) { 99 await assert_focus_navigation_forward(elements); 100 elements.reverse(); 101 await assert_focus_navigation_backward(elements); 102 } 103 104 105 // If shadow root is closed, need to pass shadowRoot and element to find 106 // innermost active element 107 108 function isShadowHostOfRoot(shadowRoot, node) { 109 return shadowRoot && shadowRoot.host.isEqualNode(node); 110 } 111 112 function innermostActiveElementWithShadowRoot(shadowRoot, element) { 113 element = element || document.activeElement; 114 if (isIFrameElement(element)) { 115 if (element.contentDocument.activeElement) 116 return innermostActiveElementWithShadowRoot(shadowRoot, element.contentDocument.activeElement); 117 return element; 118 } 119 if (isShadowHostOfRoot(shadowRoot, element)) { 120 if (shadowRoot.activeElement) 121 return innermostActiveElementWithShadowRoot(shadowRoot, shadowRoot.activeElement); 122 } 123 return element; 124 } 125 126 async function shouldNavigateFocusWithShadowRoot(from, direction) { 127 const [fromElement, shadowRoot] = from; 128 if (!fromElement) 129 return false; 130 131 fromElement.focus(); 132 if (fromElement !== innermostActiveElementWithShadowRoot(shadowRoot)) 133 return false; 134 135 if (direction == 'forward') 136 await navigateFocusForward(); 137 else 138 await navigateFocusBackward(); 139 140 return true; 141 } 142 143 async function assert_focus_navigation_element_with_shadow_root(from, to, direction) { 144 const result = await shouldNavigateFocusWithShadowRoot(from, direction); 145 const [fromElement] = from; 146 const [toElement, toShadowRoot] = to; 147 assert_true(result, 'Failed to focus ' + fromElement.id); 148 const message = 149 `Focus should move ${direction} from ${fromElement.id} to ${toElement.id}`; 150 assert_equals(innermostActiveElementWithShadowRoot(toShadowRoot), toElement, message); 151 } 152 153 async function assert_focus_navigation_elements_with_shadow_root(elements, direction) { 154 assert_true( 155 elements.length >= 2, 156 'length of elements should be greater than or equal to 2.'); 157 for (var i = 0; i + 1 < elements.length; ++i) 158 await assert_focus_navigation_element_with_shadow_root(elements[i], elements[i + 1], direction); 159 } 160 161 async function assert_focus_navigation_forward_with_shadow_root(elements) { 162 return assert_focus_navigation_elements_with_shadow_root(elements, 'forward'); 163 } 164 165 async function assert_focus_navigation_backward_with_shadow_root(elements) { 166 return assert_focus_navigation_elements_with_shadow_root(elements, 'backward'); 167 } 168 169 async function assert_focus_navigation_bidirectional_with_shadow_root(elements) { 170 await assert_focus_navigation_forward_with_shadow_root(elements); 171 elements.reverse(); 172 await assert_focus_navigation_backward_with_shadow_root(elements); 173 } 174 175 // This Promise will run each test case that is: 176 // 1. Wrapped in an element with class name "test-case". 177 // 2. Has data-expect attribute be an ordered list of elements to focus. 178 // 3. Has data-description attribute be a string explaining the test. 179 // e.g <div class="test-case" data-expect="b,a,c" 180 // data-description="Focus navigation"> 181 async function runFocusTestCases() { 182 const testCases = Array.from(document.querySelectorAll('.test-case')); 183 for (let testCase of testCases) { 184 promise_test(async () => { 185 const expected = testCase.dataset.expect.split(','); 186 await assert_focus_navigation_bidirectional(expected); 187 }, testCase.dataset.description); 188 } 189 }