aria-element-reflection.html (41864B)
1 <!DOCTYPE HTML> 2 <html> 3 <head> 4 <meta charset="utf-8" /> 5 <title>Element Reflection for aria-activedescendant and aria-errormessage</title> 6 <link rel=help href="https://whatpr.org/html/3917/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes:element"> 7 <link rel="author" title="Meredith Lane" href="meredithl@chromium.org"> 8 <script src="/resources/testharness.js"></script> 9 <script src="/resources/testharnessreport.js"></script> 10 </head> 11 12 <div id="activedescendant" aria-activedescendant="x"></div> 13 14 <div id="parentListbox" role="listbox" aria-activedescendant="i1"> 15 <div role="option" id="i1">Item 1</div> 16 <div role="option" id="i2">Item 2</div> 17 </div> 18 19 <script> 20 test(function(t) { 21 assert_equals(activedescendant.ariaActiveDescendantElement, null, 22 "invalid ID for relationship returns null"); 23 24 // Element reference should be set if the content attribute was included. 25 assert_equals(parentListbox.getAttribute("aria-activedescendant"), "i1", "check content attribute after parsing."); 26 assert_equals(parentListbox.ariaActiveDescendantElement, i1, "check idl attribute after parsing."); 27 assert_equals(parentListbox.ariaActiveDescendantElement, parentListbox.ariaActiveDescendantElement, "check idl attribute caching after parsing."); 28 29 // If we set the content attribute, the element reference should reflect this. 30 parentListbox.setAttribute("aria-activedescendant", "i2"); 31 assert_equals(parentListbox.ariaActiveDescendantElement, i2, "setting the content attribute updates the element reference."); 32 assert_equals(parentListbox.ariaActiveDescendantElement, parentListbox.ariaActiveDescendantElement, "check idl attribute caching after update."); 33 34 // Setting the element reference should set the empty string in the content attribute. 35 parentListbox.ariaActiveDescendantElement = i1; 36 assert_equals(parentListbox.ariaActiveDescendantElement, i1, "getter should return the right element reference."); 37 assert_equals(parentListbox.getAttribute("aria-activedescendant"), "", "content attribute should be empty."); 38 39 // Both content and IDL attribute should be nullable. 40 parentListbox.ariaActiveDescendantElement = null; 41 assert_equals(parentListbox.ariaActiveDescendantElement, null); 42 assert_false(parentListbox.hasAttribute("aria-activedescendant")); 43 assert_equals(parentListbox.getAttribute("aria-activedescendant"), null, "nullifying the idl attribute removes the content attribute."); 44 45 // Setting content attribute to non-existent or non compatible element should nullify the IDL attribute. 46 // Reset the element to an existant one. 47 parentListbox.setAttribute("aria-activedescendant", "i1"); 48 assert_equals(parentListbox.ariaActiveDescendantElement, i1, "reset attribute."); 49 50 parentListbox.setAttribute("aria-activedescendant", "non-existent-element"); 51 assert_equals(parentListbox.getAttribute("aria-activedescendant"), "non-existent-element"); 52 assert_equals(parentListbox.ariaActiveDescendantElement, null,"non-DOM content attribute should null the element reference"); 53 }, "aria-activedescendant element reflection"); 54 </script> 55 56 <div id="parentListbox2" role="listbox" aria-activedescendant="option1"> 57 <div role="option" id="option1">Item 1</div> 58 <div role="option" id="option2">Item 2</div> 59 </div> 60 61 <script> 62 test(function(t) { 63 const option1 = document.getElementById("option1"); 64 const option2 = document.getElementById("option2"); 65 assert_equals(parentListbox2.ariaActiveDescendantElement, option1); 66 option1.removeAttribute("id"); 67 option2.setAttribute("id", "option1"); 68 const option2Duplicate = document.getElementById("option1"); 69 assert_equals(option2, option2Duplicate); 70 71 assert_equals(parentListbox2.ariaActiveDescendantElement, option2); 72 }, "If the content attribute is set directly, the IDL attribute getter always returns the first element whose ID matches the content attribute."); 73 </script> 74 75 <div id="blankIdParent" role="listbox"> 76 <div role="option" id="multiple-id"></div> 77 <div role="option" id="multiple-id"></div> 78 </div> 79 80 <script> 81 test(function(t) { 82 // Get second child of parent. This violates the setting of a reflected element 83 // as it will not be the first child of the parent with that ID, which should 84 // result in an empty string for the content attribute. 85 blankIdParent.ariaActiveDescendantElement = blankIdParent.children[1]; 86 assert_true(blankIdParent.hasAttribute("aria-activedescendant")); 87 assert_equals(blankIdParent.getAttribute("aria-activedescendant"), ""); 88 assert_equals(blankIdParent.ariaActiveDescendantElement, blankIdParent.children[1]); 89 }, "Setting the IDL attribute to an element which is not the first element in DOM order with its ID causes the content attribute to be an empty string"); 90 </script> 91 92 <div id="outerContainer"> 93 <p id="lightParagraph">Hello world!</p> 94 <span id="shadowHost"> 95 </span> 96 </div> 97 98 <script> 99 test(function(t) { 100 const shadow = shadowHost.attachShadow({mode: "open"}); 101 const link = document.createElement("a"); 102 shadow.appendChild(link); 103 104 assert_equals(lightParagraph.ariaActiveDescendantElement, null); 105 106 // The given element crosses a shadow dom boundary, so it cannot be 107 // set as an element reference. 108 lightParagraph.ariaActiveDescendantElement = link; 109 assert_equals(lightParagraph.ariaActiveDescendantElement, null); 110 111 // The given element crosses a shadow dom boundary (upwards), so 112 // can be used as an element reference, but the content attribute 113 // should reflect the empty string. 114 link.ariaActiveDescendantElement = lightParagraph; 115 assert_equals(link.ariaActiveDescendantElement, lightParagraph); 116 assert_equals(link.getAttribute("aria-activedescendant"), ""); 117 }, "Setting an element reference that crosses into a shadow tree is disallowed, but setting one that is in a shadow inclusive ancestor is allowed."); 118 </script> 119 120 <input id="startTime" ></input> 121 <span id="errorMessage">Invalid Time</span> 122 123 <script> 124 test(function(t) { 125 startTime.ariaErrorMessageElements = [errorMessage]; 126 assert_equals(startTime.getAttribute("aria-errormessage"), ""); 127 assert_array_equals(startTime.ariaErrorMessageElements, [errorMessage]); 128 129 startTime.ariaErrorMessageElements = []; 130 assert_array_equals(startTime.ariaErrorMessageElements, []); 131 assert_equals(startTime.getAttribute("aria-errormessage"), ""); 132 133 startTime.setAttribute("aria-errormessage", "errorMessage"); 134 assert_array_equals(startTime.ariaErrorMessageElements, [errorMessage]); 135 136 }, "aria-errormessage"); 137 138 test(function (t) { 139 assert_false('ariaErrorMessageElement' in startTime); 140 }, 'ariaErrorMessageElement is not defined') 141 142 </script> 143 144 <label> 145 Password: 146 <input id="passwordField" type="password" aria-details="pw"> 147 </label> 148 149 <ul> 150 <li id="listItem1">First description.</li> 151 <li id="listItem2">Second description.</li> 152 </ul> 153 154 <script> 155 156 test(function(t) { 157 assert_array_equals(passwordField.ariaDetailsElements, []); 158 passwordField.ariaDetailsElements = [ listItem1 ]; 159 assert_equals(passwordField.getAttribute("aria-details"), ""); 160 assert_array_equals(passwordField.ariaDetailsElements, [ listItem1 ]); 161 162 passwordField.ariaDetailsElements = [ listItem2 ]; 163 assert_equals(passwordField.getAttribute("aria-details"), ""); 164 assert_array_equals(passwordField.ariaDetailsElements, [ listItem2 ]); 165 }, "aria-details"); 166 </script> 167 168 <div id="deletionParent" role="listbox" aria-activedescendant="contentAttrElement"> 169 <div role="option" id="contentAttrElement">Item 1</div> 170 <div role="option" id="idlAttrElement">Item 2</div> 171 </div> 172 173 <script> 174 175 test(function(t) { 176 const contentAttrElement = document.getElementById("contentAttrElement"); 177 const idlAttrElement = document.getElementById("idlAttrElement"); 178 179 assert_equals(deletionParent.getAttribute("aria-activedescendant"), "contentAttrElement"); 180 assert_equals(deletionParent.ariaActiveDescendantElement, contentAttrElement); 181 182 // Deleting an element set via the content attribute. 183 deletionParent.removeChild(contentAttrElement); 184 assert_equals(deletionParent.getAttribute("aria-activedescendant"), "contentAttrElement"); 185 186 // As it was not explitly set, the attr-associated-element is computed from the content attribute, 187 // and since descendant1 has been removed from the DOM, it is not valid. 188 assert_equals(deletionParent.ariaActiveDescendantElement, null); 189 190 // Deleting an element set via the IDL attribute. 191 deletionParent.ariaActiveDescendantElement = idlAttrElement; 192 assert_equals(deletionParent.getAttribute("aria-activedescendant"), ""); 193 194 deletionParent.removeChild(idlAttrElement); 195 assert_equals(deletionParent.ariaActiveDescendantElement, null); 196 197 // The content attribute is still empty. 198 assert_equals(deletionParent.getAttribute("aria-activedescendant"), ""); 199 }, "Deleting a reflected element should return null for the IDL attribute and the content attribute will be empty."); 200 </script> 201 202 <div id="parentNode" role="listbox" aria-activedescendant="changingIdElement"> 203 <div role="option" id="changingIdElement">Item 1</div> 204 <div role="option" id="persistantIDElement">Item 2</div> 205 </div> 206 207 <script> 208 test(function(t) { 209 const changingIdElement = document.getElementById("changingIdElement"); 210 assert_equals(parentNode.ariaActiveDescendantElement, changingIdElement); 211 212 // Modify the id attribute. 213 changingIdElement.setAttribute("id", "new-id"); 214 215 // The content attribute still reflects the old id, and we expect the 216 // Element reference to be null as there is no DOM node with id "original" 217 assert_equals(parentNode.getAttribute("aria-activedescendant"), "changingIdElement"); 218 assert_equals(parentNode.ariaActiveDescendantElement, null, "Element set via content attribute with a changed id will return null on getting"); 219 220 parentNode.ariaActiveDescendantElement = changingIdElement; 221 assert_equals(parentNode.getAttribute("aria-activedescendant"), ""); 222 assert_equals(parentNode.ariaActiveDescendantElement, changingIdElement); 223 224 // The explicitly set element takes precendance over the content attribute. 225 // This means that we still return the same element reference, but the 226 // content attribute is empty. 227 changingIdElement.setAttribute("id", "newer-id"); 228 assert_equals(parentNode.ariaActiveDescendantElement, changingIdElement, "explicitly set element is still present even after the id has been changed"); 229 assert_equals(parentNode.getAttribute("aria-activedescendant"), "", "content attribute is empty."); 230 }, "Changing the ID of an element doesn't lose the reference."); 231 </script> 232 233 <!-- TODO(chrishall): change naming scheme to inner/outer --> 234 <div id="lightParent" role="listbox"> 235 <div id="lightElement" role="option">Hello world!</div> 236 </div> 237 <div id="shadowHostElement"></div> 238 239 <script> 240 test(function(t) { 241 const lightElement = document.getElementById("lightElement"); 242 const shadowRoot = shadowHostElement.attachShadow({mode: "open"}); 243 244 assert_equals(lightParent.ariaActiveDescendantElement, null, 'null before'); 245 assert_equals(lightParent.getAttribute('aria-activedescendant'), null, 'null before'); 246 247 lightParent.ariaActiveDescendantElement = lightElement; 248 assert_equals(lightParent.ariaActiveDescendantElement, lightElement); 249 assert_equals(lightParent.getAttribute('aria-activedescendant'), ""); 250 251 // Move the referenced element into shadow DOM. 252 // This will cause the computed attr-associated element to be null as the 253 // referenced element will no longer be in a valid scope. 254 // The underlying reference is kept intact, so if the referenced element is 255 // later restored to a valid scope the computed attr-associated element will 256 // then reflect 257 shadowRoot.appendChild(lightElement); 258 assert_equals(lightParent.ariaActiveDescendantElement, null, "computed attr-assoc element should be null as referenced element is in an invalid scope"); 259 assert_equals(lightParent.getAttribute("aria-activedescendant"), ""); 260 261 // Move the referenced element back into light DOM. 262 // Since the underlying reference was kept intact, after moving the 263 // referenced element back to a valid scope should be reflected in the 264 // computed attr-associated element. 265 lightParent.appendChild(lightElement); 266 assert_equals(lightParent.ariaActiveDescendantElement, lightElement, "computed attr-assoc element should be restored as referenced element is back in a valid scope"); 267 assert_equals(lightParent.getAttribute("aria-activedescendant"), ""); 268 }, "Reparenting an element into a descendant shadow scope hides the element reference."); 269 </script> 270 271 <div id='fruitbowl' role='listbox'> 272 <div id='apple' role='option'>I am an apple</div> 273 <div id='pear' role='option'>I am a pear</div> 274 <div id='banana' role='option'>I am a banana</div> 275 </div> 276 <div id='shadowFridge'></div> 277 278 <script> 279 test(function(t) { 280 const shadowRoot = shadowFridge.attachShadow({mode: "open"}); 281 const banana = document.getElementById("banana"); 282 283 fruitbowl.ariaActiveDescendantElement = apple; 284 assert_equals(fruitbowl.ariaActiveDescendantElement, apple); 285 assert_equals(fruitbowl.getAttribute("aria-activedescendant"), ""); 286 287 // Move the referenced element into shadow DOM. 288 shadowRoot.appendChild(apple); 289 assert_equals(fruitbowl.ariaActiveDescendantElement, null, "computed attr-assoc element should be null as referenced element is in an invalid scope"); 290 // The content attribute is still empty. 291 assert_equals(fruitbowl.getAttribute("aria-activedescendant"), ""); 292 293 // let us rename our banana to an apple 294 banana.setAttribute("id", "apple"); 295 const lyingBanana = document.getElementById("apple"); 296 assert_equals(lyingBanana, banana); 297 298 // our ariaActiveDescendantElement thankfully isn't tricked. 299 // this is thanks to the underlying reference being kept intact, it is 300 // checked and found to be in an invalid scope. 301 assert_equals(fruitbowl.ariaActiveDescendantElement, null); 302 // our content attribute is empty. 303 assert_equals(fruitbowl.getAttribute("aria-activedescendant"), ""); 304 305 // when we remove our IDL attribute, the content attribute is also thankfully cleared. 306 fruitbowl.ariaActiveDescendantElement = null; 307 assert_equals(fruitbowl.ariaActiveDescendantElement, null); 308 assert_equals(fruitbowl.getAttribute("aria-activedescendant"), null); 309 }, "Reparenting referenced element cannot cause retargeting of reference."); 310 </script> 311 312 <div id='toaster' role='listbox'></div> 313 <div id='shadowPantry'></div> 314 315 <script> 316 test(function(t) { 317 const shadowRoot = shadowPantry.attachShadow({mode: "open"}); 318 319 // Our toast starts in the shadowPantry. 320 const toast = document.createElement("div"); 321 toast.setAttribute("id", "toast"); 322 shadowRoot.appendChild(toast); 323 324 // Prepare my toast for toasting 325 toaster.ariaActiveDescendantElement = toast; 326 assert_equals(toaster.ariaActiveDescendantElement, null); 327 assert_equals(toaster.getAttribute("aria-activedescendant"), ""); 328 329 // Time to make some toast 330 toaster.appendChild(toast); 331 assert_equals(toaster.ariaActiveDescendantElement, toast); 332 // Current spec behaviour: 333 assert_equals(toaster.getAttribute("aria-activedescendant"), ""); 334 }, "Element reference set in invalid scope remains intact throughout move to valid scope."); 335 </script> 336 337 <div id="billingElementContainer"> 338 <div id="billingElement">Billing</div> 339 </div> 340 <div> 341 <div id="nameElement">Name</div> 342 <input type="text" id="input1" aria-labelledby="billingElement nameElement"/> 343 </div> 344 <div> 345 <div id="addressElement">Address</div> 346 <input type="text" id="input2"/> 347 </div> 348 349 <script> 350 test(function(t) { 351 const billingElement = document.getElementById("billingElement") 352 assert_array_equals(input1.ariaLabelledByElements, [billingElement, nameElement], "parsed content attribute sets element references."); 353 assert_equals(input1.ariaLabelledByElements, input1.ariaLabelledByElements, "check idl attribute caching after parsing"); 354 assert_equals(input2.ariaLabelledByElements, null, "Testing missing content attribute after parsing."); 355 356 input2.ariaLabelledByElements = [billingElement, addressElement]; 357 assert_array_equals(input2.ariaLabelledByElements, [billingElement, addressElement], "Testing IDL setter/getter."); 358 assert_equals(input1.ariaLabelledByElements, input1.ariaLabelledByElements, "check idl attribute caching after update"); 359 assert_equals(input2.getAttribute("aria-labelledby"), ""); 360 361 // Remove the billingElement from the DOM. 362 // As it was explicitly set the underlying association will remain intact, 363 // but it will be hidden until the element is moved back into a valid scope. 364 billingElement.remove(); 365 assert_array_equals(input2.ariaLabelledByElements, [addressElement], "Computed ariaLabelledByElements shouldn't include billing when out of scope."); 366 367 // Insert the billingElement back into the DOM and check that it is visible 368 // again, as the underlying association should have been kept intact. 369 billingElementContainer.appendChild(billingElement); 370 assert_array_equals(input2.ariaLabelledByElements, [billingElement, addressElement], "Billing element back in scope."); 371 372 input2.ariaLabelledByElements = []; 373 assert_array_equals(input2.ariaLabelledByElements, [], "Testing IDL setter/getter for empty array."); 374 assert_equals(input2.getAttribute("aria-labelledby"), ""); 375 376 input1.removeAttribute("aria-labelledby"); 377 assert_equals(input1.ariaLabelledByElements, null); 378 379 input1.setAttribute("aria-labelledby", "nameElement addressElement"); 380 assert_array_equals(input1.ariaLabelledByElements, [nameElement, addressElement], 381 "computed value after setting attribute directly"); 382 383 input1.ariaLabelledByElements = null; 384 assert_false(input1.hasAttribute("aria-labelledby", "Nullifying the IDL attribute should remove the content attribute.")); 385 }, "aria-labelledby."); 386 </script> 387 388 <ul role="tablist"> 389 <li role="presentation"><a id="link1" role="tab" aria-controls="panel1">Tab 1</a></li> 390 <li role="presentation"><a id="link2" role="tab">Tab 2</a></li> 391 </ul> 392 393 <div role="tabpanel" id="panel1"></div> 394 <div role="tabpanel" id="panel2"></div> 395 396 <script> 397 test(function(t) { 398 assert_array_equals(link1.ariaControlsElements, [panel1]); 399 assert_equals(link2.ariaControlsElements, null); 400 401 link2.setAttribute("aria-controls", "panel1 panel2"); 402 assert_array_equals(link2.ariaControlsElements, [panel1, panel2]); 403 404 link1.ariaControlsElements = []; 405 assert_equals(link1.getAttribute("aria-controls"), ""); 406 407 link2.ariaControlsElements = [panel1, panel2]; 408 assert_equals(link2.getAttribute("aria-controls"), ""); 409 assert_array_equals(link2.ariaControlsElements, [panel1, panel2]); 410 411 link2.removeAttribute("aria-controls"); 412 assert_equals(link2.ariaControlsElements, null); 413 414 link2.ariaControlsElements = [panel1, panel2]; 415 assert_equals(link2.getAttribute("aria-controls"), ""); 416 assert_array_equals(link2.ariaControlsElements, [panel1, panel2]); 417 418 link2.ariaControlsElements = null; 419 assert_false(link2.hasAttribute("aria-controls", "Nullifying the IDL attribute should remove the content attribute.")); 420 }, "aria-controls."); 421 </script> 422 423 <a id="describedLink" aria-describedby="description1 description2">Fruit</a> 424 <div id="description1">Delicious</div> 425 <div id="description2">Nutritious</div> 426 427 <script> 428 test(function(t) { 429 assert_array_equals(describedLink.ariaDescribedByElements, [description1, description2]); 430 431 describedLink.ariaDescribedByElements = [description1, description2]; 432 assert_equals(describedLink.getAttribute("aria-describedby"), ""); 433 assert_array_equals(describedLink.ariaDescribedByElements, [description1, description2]); 434 435 describedLink.ariaDescribedByElements = []; 436 assert_equals(describedLink.getAttribute("aria-describedby"), ""); 437 438 describedLink.setAttribute("aria-describedby", "description1"); 439 assert_array_equals(describedLink.ariaDescribedByElements, [description1]); 440 441 describedLink.removeAttribute("aria-describedby"); 442 assert_equals(describedLink.ariaDescribedByElements, null); 443 444 describedLink.ariaDescribedByElements = [description1, description2]; 445 assert_equals(describedLink.getAttribute("aria-describedby"), ""); 446 assert_array_equals(describedLink.ariaDescribedByElements, [description1, description2]); 447 448 describedLink.ariaDescribedByElements = null; 449 assert_false(describedLink.hasAttribute("aria-describedby", "Nullifying the IDL attribute should remove the content attribute.")); 450 }, "aria-describedby."); 451 </script> 452 453 <h2 id="titleHeading" aria-flowto="article1 article2">Title</h2> 454 <div>Next</div> 455 <article id="article2">Content2</article> 456 <article id="article1">Content1</article> 457 458 <script> 459 test(function(t) { 460 const article1 = document.getElementById("article1"); 461 const article2 = document.getElementById("article2"); 462 463 assert_array_equals(titleHeading.ariaFlowToElements, [article1, article2]); 464 465 titleHeading.ariaFlowToElements = [article1, article2]; 466 assert_equals(titleHeading.getAttribute("aria-flowto"), ""); 467 assert_array_equals(titleHeading.ariaFlowToElements, [article1, article2]); 468 469 titleHeading.ariaFlowToElements = []; 470 assert_equals(titleHeading.getAttribute("aria-flowto"), ""); 471 472 titleHeading.setAttribute("aria-flowto", "article1"); 473 assert_array_equals(titleHeading.ariaFlowToElements, [article1]); 474 475 titleHeading.removeAttribute("aria-flowto"); 476 assert_equals(titleHeading.ariaFlowToElements, null); 477 478 titleHeading.ariaFlowToElements = [article1, article2]; 479 assert_equals(titleHeading.getAttribute("aria-flowto"), ""); 480 assert_array_equals(titleHeading.ariaFlowToElements, [article1, article2]); 481 482 titleHeading.ariaFlowToElements = null; 483 assert_false(titleHeading.hasAttribute("aria-flowto", "Nullifying the IDL attribute should remove the content attribute.")); 484 }, "aria-flowto."); 485 </script> 486 487 <ul> 488 <li id="listItemOwner" aria-owns="child1 child2">Parent</li> 489 </ul> 490 <ul> 491 <li id="child1">Child 1</li> 492 <li id="child2">Child 2</li> 493 </ul> 494 <script> 495 test(function(t) { 496 assert_array_equals(listItemOwner.ariaOwnsElements, [child1, child2]); 497 498 listItemOwner.removeAttribute("aria-owns"); 499 assert_equals(listItemOwner.ariaOwnsElements, null); 500 501 listItemOwner.ariaOwnsElements = [child1, child2]; 502 assert_equals(listItemOwner.getAttribute("aria-owns"), ""); 503 assert_array_equals(listItemOwner.ariaOwnsElements, [child1, child2]); 504 505 listItemOwner.ariaOwnsElements = []; 506 assert_equals(listItemOwner.getAttribute("aria-owns"), ""); 507 508 listItemOwner.setAttribute("aria-owns", "child1"); 509 assert_array_equals(listItemOwner.ariaOwnsElements, [child1]); 510 511 listItemOwner.ariaOwnsElements = [child1, child2]; 512 assert_equals(listItemOwner.getAttribute("aria-owns"), ""); 513 assert_array_equals(listItemOwner.ariaOwnsElements, [child1, child2]); 514 515 listItemOwner.ariaOwnsElements = null; 516 assert_false(listItemOwner.hasAttribute("aria-owns", "Nullifying the IDL attribute should remove the content attribute.")); 517 }, "aria-owns."); 518 </script> 519 520 <div id="lightDomContainer"> 521 <h2 id="lightDomHeading" aria-flowto="shadowChild1 shadowChild2">Light DOM Heading</h2> 522 <div id="host"></div> 523 <p id="lightDomText1">Light DOM text</p> 524 <p id="lightDomText2">Light DOM text</p> 525 </div> 526 527 <script> 528 test(function(t) { 529 const shadowRoot = host.attachShadow({mode: "open"}); 530 const shadowChild1 = document.createElement("article"); 531 shadowChild1.setAttribute("id", "shadowChild1"); 532 shadowRoot.appendChild(shadowChild1); 533 const shadowChild2 = document.createElement("article"); 534 shadowChild2.setAttribute("id", "shadowChild1"); 535 shadowRoot.appendChild(shadowChild2); 536 537 // The elements in the content attribute are in a "darker" tree - they 538 // enter a shadow encapsulation boundary, so not be associated any more. 539 assert_array_equals(lightDomHeading.ariaFlowToElements, []); 540 541 // These elements are in a shadow including ancestor, i.e "lighter" tree. 542 // Valid for the IDL attribute, but content attribute should be null. 543 shadowChild1.ariaFlowToElements = [lightDomText1, lightDomText2]; 544 assert_equals(shadowChild1.getAttribute("aria-flowto"), "", "empty content attribute for elements that cross shadow boundaries."); 545 546 // These IDs belong to a different scope, so the attr-associated-element 547 // cannot be computed. 548 shadowChild2.setAttribute("aria-flowto", "lightDomText1 lightDomText2"); 549 assert_array_equals(shadowChild2.ariaFlowToElements, []); 550 551 // Elements that cross into shadow DOM are dropped, only reflect the valid 552 // elements in IDL and in the content attribute. 553 lightDomHeading.ariaFlowToElements = [shadowChild1, shadowChild2, lightDomText1, lightDomText2]; 554 assert_array_equals(lightDomHeading.ariaFlowToElements, [lightDomText1, lightDomText2], "IDL should only include valid elements"); 555 assert_equals(lightDomHeading.getAttribute("aria-flowto"), "", "empty content attribute if any given elements cross shadow boundaries"); 556 557 // Using a mixture of elements in the same scope and in a shadow including 558 // ancestor should set the IDL attribute, but should reflect the empty 559 // string in the content attribute. 560 shadowChild1.removeAttribute("aria-flowto"); 561 shadowChild1.ariaFlowToElements = [shadowChild1, lightDomText1]; 562 assert_equals(shadowChild1.getAttribute("aria-flowto"), "", "Setting IDL elements with a mix of scopes should reflect an empty string in the content attribute") 563 564 }, "shadow DOM behaviour for FrozenArray element reflection."); 565 </script> 566 567 <div id="describedButtonContainer"> 568 <div id="buttonDescription1">Delicious</div> 569 <div id="buttonDescription2">Nutritious</div> 570 <div id="outerShadowHost"></div> 571 </div> 572 573 <script> 574 test(function(t) { 575 const description1 = document.getElementById("buttonDescription1"); 576 const description2 = document.getElementById("buttonDescription2"); 577 const outerShadowRoot = outerShadowHost.attachShadow({mode: "open"}); 578 const innerShadowHost = document.createElement("div"); 579 outerShadowRoot.appendChild(innerShadowHost); 580 const innerShadowRoot = innerShadowHost.attachShadow({mode: "open"}); 581 582 // Create an element, add some attr associated light DOM elements and append it to the outer shadow root. 583 const describedElement = document.createElement("button"); 584 describedButtonContainer.appendChild(describedElement); 585 describedElement.ariaDescribedByElements = [description1, description2]; 586 587 // All elements were in the same scope, so elements are gettable and the content attribute is empty. 588 assert_array_equals(describedElement.ariaDescribedByElements, [description1, description2], "same scope reference"); 589 assert_equals(describedElement.getAttribute("aria-describedby"), ""); 590 591 outerShadowRoot.appendChild(describedElement); 592 593 // Explicitly set attr-associated-elements should still be gettable because we are referencing elements in a lighter scope. 594 // The content attr is empty. 595 assert_array_equals(describedElement.ariaDescribedByElements, [description1, description2], "lighter scope reference"); 596 assert_equals(describedElement.getAttribute("aria-describedby"), ""); 597 598 // Move the explicitly set elements into a deeper shadow DOM to test the relationship should not be gettable. 599 innerShadowRoot.appendChild(description1); 600 innerShadowRoot.appendChild(description2); 601 602 // Explicitly set elements are no longer retrievable, because they are no longer in a valid scope. 603 assert_array_equals(describedElement.ariaDescribedByElements, [], "invalid scope reference"); 604 assert_equals(describedElement.getAttribute("aria-describedby"), ""); 605 606 // Move into the same shadow scope as the explicitly set elements to test that the elements are gettable. 607 innerShadowRoot.appendChild(describedElement); 608 assert_array_equals(describedElement.ariaDescribedByElements, [description1, description2], "restored valid scope reference"); 609 assert_equals(describedElement.getAttribute("aria-describedby"), ""); 610 }, "Moving explicitly set elements across shadow DOM boundaries."); 611 </script> 612 613 <div id="sameScopeContainer"> 614 <div id="labelledby" aria-labelledby="headingLabel1 headingLabel2">Misspelling</div> 615 <div id="headingLabel1">Wonderful</div> 616 <div id="headingLabel2">Fantastic</div> 617 618 <div id="headingShadowHost"></div> 619 </div> 620 621 <script> 622 test(function(t) { 623 const shadowRoot = headingShadowHost.attachShadow({mode: "open"}); 624 const headingElement = document.createElement("h1"); 625 const headingLabel1 = document.getElementById("headingLabel1") 626 const headingLabel2 = document.getElementById("headingLabel2") 627 shadowRoot.appendChild(headingElement); 628 629 assert_array_equals(labelledby.ariaLabelledByElements, [headingLabel1, headingLabel2], "aria-labelledby is supported by IDL getter."); 630 631 // Explicitly set elements are in a lighter shadow DOM, so that's ok. 632 headingElement.ariaLabelledByElements = [headingLabel1, headingLabel2]; 633 assert_array_equals(headingElement.ariaLabelledByElements, [headingLabel1, headingLabel2], "Lighter elements are gettable when explicitly set."); 634 assert_equals(headingElement.getAttribute("aria-labelledby"), ""); 635 636 // Move into Light DOM, explicitly set elements should still be gettable. 637 // Note that the content attribute is still empty. 638 sameScopeContainer.appendChild(headingElement); 639 assert_array_equals(headingElement.ariaLabelledByElements, [headingLabel1, headingLabel2], "Elements are all in same scope, so gettable."); 640 assert_equals(headingElement.getAttribute("aria-labelledby"), "", "Content attribute is empty."); 641 642 // Reset the association, the content attribute is sitll empty. 643 headingElement.ariaLabelledByElements = [headingLabel1, headingLabel2]; 644 assert_equals(headingElement.getAttribute("aria-labelledby"), ""); 645 646 // Remove the referring element from the DOM, elements are no longer longer exposed, 647 // underlying internal reference is still kept intact. 648 headingElement.remove(); 649 assert_array_equals(headingElement.ariaLabelledByElements, [], "Element is no longer in the document, so references should no longer be exposed."); 650 assert_equals(headingElement.getAttribute("aria-labelledby"), ""); 651 652 // Insert it back in. 653 sameScopeContainer.appendChild(headingElement); 654 assert_array_equals(headingElement.ariaLabelledByElements, [headingLabel1, headingLabel2], "Element is restored to valid scope, so should be gettable."); 655 assert_equals(headingElement.getAttribute("aria-labelledby"), ""); 656 657 // Remove everything from the DOM, nothing is exposed again. 658 headingLabel1.remove(); 659 headingLabel2.remove(); 660 assert_array_equals(headingElement.ariaLabelledByElements, []); 661 assert_equals(headingElement.getAttribute("aria-labelledby"), ""); 662 assert_equals(document.getElementById("headingLabel1"), null); 663 assert_equals(document.getElementById("headingLabel2"), null); 664 665 // Reset the association. 666 headingElement.ariaLabelledByElements = [headingLabel1, headingLabel2]; 667 assert_array_equals(headingElement.ariaLabelledByElements, []); 668 assert_equals(headingElement.getAttribute("aria-labelledby"), ""); 669 }, "Moving explicitly set elements around within the same scope, and removing from the DOM."); 670 </script> 671 672 <input id="input"> 673 <optgroup> 674 <option id="first">First option</option> 675 <option id="second">Second option</option> 676 </optgroup> 677 678 <script> 679 test(function(t) { 680 input.ariaActiveDescendantElement = first; 681 first.parentElement.appendChild(first); 682 683 assert_equals(input.ariaActiveDescendantElement, first); 684 }, "Reparenting."); 685 </script> 686 687 <div id='fromDiv'></div> 688 689 <script> 690 test(function(t) { 691 const toSpan = document.createElement('span'); 692 toSpan.setAttribute("id", "toSpan"); 693 fromDiv.ariaActiveDescendantElement = toSpan; 694 695 assert_equals(fromDiv.ariaActiveDescendantElement, null, "Referenced element not inserted into document, so is in an invalid scope."); 696 assert_equals(fromDiv.getAttribute("aria-activedescendant"), "", "Invalid scope, so content attribute not set."); 697 698 fromDiv.appendChild(toSpan); 699 assert_equals(fromDiv.ariaActiveDescendantElement, toSpan, "Referenced element now inserted into the document."); 700 assert_equals(fromDiv.getAttribute("aria-activedescendant"), "", "Content attribute remains empty, as it is only updated at set time."); 701 702 }, "Attaching element reference before it's inserted into the DOM."); 703 </script> 704 705 <div id='originalDocumentDiv'></div> 706 707 <script> 708 test(function(t) { 709 const newDoc = document.implementation.createHTMLDocument('new document'); 710 const newDocSpan = newDoc.createElement('span'); 711 newDoc.body.appendChild(newDocSpan); 712 713 // Create a reference across documents. 714 originalDocumentDiv.ariaActiveDescendantElement = newDocSpan; 715 716 assert_equals(originalDocumentDiv.ariaActiveDescendantElement, null, "Cross-document is an invalid scope, so reference will not be visible."); 717 assert_equals(fromDiv.getAttribute("aria-activedescendant"), "", "Invalid scope when set, so content attribute not set."); 718 719 // "Move" span to first document. 720 originalDocumentDiv.appendChild(newDocSpan); 721 722 // Implementation defined: moving object into same document from other document may cause reference to become visible. 723 assert_equals(originalDocumentDiv.ariaActiveDescendantElement, newDocSpan, "Implementation defined: moving object back *may* make reference visible."); 724 assert_equals(fromDiv.getAttribute("aria-activedescendant"), "", "Invalid scope when set, so content attribute not set."); 725 }, "Cross-document references and moves."); 726 </script> 727 728 729 <script> 730 test(function(t) { 731 const otherDoc = document.implementation.createHTMLDocument('otherDoc'); 732 const otherDocDiv = otherDoc.createElement('div'); 733 const otherDocSpan = otherDoc.createElement('span'); 734 otherDocDiv.appendChild(otherDocSpan); 735 otherDoc.body.appendChild(otherDocDiv); 736 737 otherDocDiv.ariaActiveDescendantElement = otherDocSpan; 738 assert_equals(otherDocDiv.ariaActiveDescendantElement, otherDocSpan, "Setting reference on a different document."); 739 740 // Adopt element from other oducment. 741 document.body.appendChild(document.adoptNode(otherDocDiv)); 742 assert_equals(otherDocDiv.ariaActiveDescendantElement, otherDocSpan, "Reference should be kept on the new document too."); 743 }, "Adopting element keeps references."); 744 </script> 745 746 <div id="cachingInvariantMain"></div> 747 <div id="cachingInvariantElement1"></div> 748 <div id="cachingInvariantElement2"></div> 749 <div id="cachingInvariantElement3"></div> 750 <div id="cachingInvariantElement4"></div> 751 <div id="cachingInvariantElement5"></div> 752 753 <script> 754 test(function(t) { 755 cachingInvariantMain.ariaControlsElements = [cachingInvariantElement1, cachingInvariantElement2]; 756 cachingInvariantMain.ariaDescribedByElements = [cachingInvariantElement3, cachingInvariantElement4]; 757 cachingInvariantMain.ariaDetailsElements = [cachingInvariantElement5]; 758 cachingInvariantMain.ariaFlowToElements = [cachingInvariantElement1, cachingInvariantElement3]; 759 cachingInvariantMain.ariaLabelledByElements = [cachingInvariantElement2, cachingInvariantElement4]; 760 cachingInvariantMain.ariaOwnsElements = [cachingInvariantElement1, cachingInvariantElement2, cachingInvariantElement3]; 761 762 let ariaControlsElementsArray = cachingInvariantMain.ariaControlsElements; 763 let ariaDescribedByElementsArray = cachingInvariantMain.ariaDescribedByElements; 764 let ariaDetailsElementsArray = cachingInvariantMain.ariaDetailsElements; 765 let ariaFlowToElementsArray = cachingInvariantMain.ariaFlowToElements; 766 let ariaLabelledByElementsArray = cachingInvariantMain.ariaLabelledByElements; 767 let ariaOwnsElementsArray = cachingInvariantMain.ariaOwnsElements; 768 769 assert_equals(ariaControlsElementsArray, cachingInvariantMain.ariaControlsElements, "Caching invariant for ariaControlsElements"); 770 assert_equals(ariaDescribedByElementsArray, cachingInvariantMain.ariaDescribedByElements, "Caching invariant for ariaDescribedByElements"); 771 assert_equals(ariaDetailsElementsArray, cachingInvariantMain.ariaDetailsElements, "Caching invariant for ariaDetailsElements"); 772 assert_equals(ariaFlowToElementsArray, cachingInvariantMain.ariaFlowToElements, "Caching invariant for ariaFlowToElements"); 773 assert_equals(ariaLabelledByElementsArray, cachingInvariantMain.ariaLabelledByElements, "Caching invariant for ariaLabelledByElements"); 774 assert_equals(ariaOwnsElementsArray, cachingInvariantMain.ariaOwnsElements, "Caching invariant for ariaOwnsElements"); 775 776 // Ensure that stale values don't continue to be cached 777 cachingInvariantMain.ariaControlsElements = [cachingInvariantElement4, cachingInvariantElement5]; 778 cachingInvariantMain.ariaDescribedByElements = [cachingInvariantElement1, cachingInvariantElement2]; 779 cachingInvariantMain.ariaDetailsElements = [cachingInvariantElement3]; 780 cachingInvariantMain.ariaFlowToElements = [cachingInvariantElement4, cachingInvariantElement5]; 781 cachingInvariantMain.ariaLabelledByElements = [cachingInvariantElement1, cachingInvariantElement2]; 782 cachingInvariantMain.ariaOwnsElements = [cachingInvariantElement3, cachingInvariantElement4, cachingInvariantElement1]; 783 784 ariaControlsElementsArray = cachingInvariantMain.ariaControlsElements; 785 ariaDescribedByElementsArray = cachingInvariantMain.ariaDescribedByElements; 786 ariaDetailsElementsArray = cachingInvariantMain.ariaDetailsElements; 787 ariaFlowToElementsArray = cachingInvariantMain.ariaFlowToElements; 788 ariaLabelledByElementsArray = cachingInvariantMain.ariaLabelledByElements; 789 ariaOwnsElementsArray = cachingInvariantMain.ariaOwnsElements; 790 791 assert_equals(ariaControlsElementsArray, cachingInvariantMain.ariaControlsElements, "Caching invariant for ariaControlsElements"); 792 assert_equals(ariaDescribedByElementsArray, cachingInvariantMain.ariaDescribedByElements, "Caching invariant for ariaDescribedByElements"); 793 assert_equals(ariaDetailsElementsArray, cachingInvariantMain.ariaDetailsElements, "Caching invariant for ariaDetailsElements"); 794 assert_equals(ariaFlowToElementsArray, cachingInvariantMain.ariaFlowToElements, "Caching invariant for ariaFlowToElements"); 795 assert_equals(ariaLabelledByElementsArray, cachingInvariantMain.ariaLabelledByElements, "Caching invariant for ariaLabelledByElements"); 796 assert_equals(ariaOwnsElementsArray, cachingInvariantMain.ariaOwnsElements, "Caching invariant for ariaOwnsElements"); 797 798 }, "Caching invariant different attributes."); 799 </script> 800 801 <div id="cachingInvariantMain1"></div> 802 <div id="cachingInvariantMain2"></div> 803 804 <script> 805 test(function(t) { 806 cachingInvariantMain1.ariaDescribedByElements = [cachingInvariantElement1, cachingInvariantElement2]; 807 cachingInvariantMain2.ariaDescribedByElements = [cachingInvariantElement3, cachingInvariantElement4]; 808 809 let ariaDescribedByElementsArray1 = cachingInvariantMain1.ariaDescribedByElements; 810 let ariaDescribedByElementsArray2 = cachingInvariantMain2.ariaDescribedByElements; 811 812 assert_equals(ariaDescribedByElementsArray1, cachingInvariantMain1.ariaDescribedByElements, "Caching invariant for ariaDescribedByElements in one elemnt"); 813 assert_equals(ariaDescribedByElementsArray2, cachingInvariantMain2.ariaDescribedByElements, "Caching invariant for ariaDescribedByElements in onother elemnt"); 814 }, "Caching invariant different elements."); 815 </script> 816 817 <div id="badInputValues"></div> 818 <div id="badInputValues2"></div> 819 820 <script> 821 test(function(t) { 822 assert_throws_js(TypeError, () => { badInputValues.ariaActiveDescendantElement = "a string"; }); 823 assert_throws_js(TypeError, () => { badInputValues.ariaActiveDescendantElement = 1; }); 824 assert_throws_js(TypeError, () => { badInputValues.ariaActiveDescendantElement = [ badInputValues2 ]; }); 825 826 assert_throws_js(TypeError, () => { badInputValues.ariaControlsElements = "a string" }); 827 assert_throws_js(TypeError, () => { badInputValues.ariaControlsElements = 1 }); 828 assert_throws_js(TypeError, () => { badInputValues.ariaControlsElements = [1, 2, 3] }); 829 assert_throws_js(TypeError, () => { badInputValues.ariaControlsElements = badInputValues2 }); 830 }, "Passing values of the wrong type should throw a TypeError"); 831 </script> 832 833 <!-- TODO(chrishall): add additional GC test covering: 834 if an element is in an invalid scope but attached to the document, it's 835 not GC'd; 836 --> 837 838 <!-- TODO(chrishall): add additional GC test covering: 839 if an element is not attached to the document, but is in a tree fragment 840 which is not GC'd because there is a script reference to another element 841 in the tree fragment, and the relationship is valid because it is between 842 two elements in that tree fragment, the relationship is exposed *and* the 843 element is not GC'd 844 --> 845 846 </html>