属性反映

一个属性可以扩展一个HTMLXMLSVG或其他元素,改变其行为或提供元数据。

许多属性会在相应的DOM接口中被反射。这意味着属性的值可以直接在JavaScript中通过相应接口的属性进行读写,反之亦然。反射属性比使用getAttribute()setAttribute()方法来获取和设置Element接口的属性值,提供了更自然的编程方法。

本指南将概述反射属性及其用法。

属性getter/setter

首先,我们来看看获取和设置属性的默认机制,无论属性是否被反射都可以使用它。要获取属性,你需要调用Element接口的getAttribute()方法,并指定属性名。要设置属性,你需要调用setAttribute()方法,并指定属性名和新值。

考虑以下HTML

html
<input placeholder="Original placeholder" />

获取和设置placeholder属性

js
const input = document.querySelector("input");

// Get the placeholder attribute
let attr = input.getAttribute("placeholder");

// Set the placeholder attribute
input.setAttribute("placeholder", "Modified placeholder");

反射属性

对于<input>元素,placeholder属性会被HTMLInputElement.placeholder属性反射。给定与前面相同的HTML

html
<input placeholder="Original placeholder" />

使用placeholder属性可以更自然地执行相同的操作

js
const input = document.querySelector("input");

// Get the placeholder attribute
let attr = input.placeholder;

// Set the placeholder attribute
input.placeholder = "Modified placeholder";

请注意,反射属性和属性的名称是相同的:placeholder。但这并非总是如此:属性名通常遵循驼峰式命名约定。这尤其适用于多单词的属性名,这些属性名包含属性名不允许的字符,例如连字符。例如,aria-checked属性由ariaChecked属性反射。

布尔值反射属性

布尔属性与其他属性略有不同,因为它们不必声明名称和值。例如,下面复选框<input>元素的checked属性,在显示时会被勾选

html
<input type="checkbox" checked />

如果输入被勾选,Element.getAttribute()将返回"";如果未勾选,则返回null。相应的HTMLInputElement.checked属性返回truefalse来表示勾选状态。除此以外,布尔值反射属性与其他反射属性相同。

枚举反射属性

在HTML中,枚举属性是指具有有限的、预定义的文本值集合的属性。例如,全局HTMLdir属性有三个有效值:ltrrtlauto

html
<p dir="rtl">Right to left</p>

与HTML标签名一样,HTML枚举属性及其值对大小写不敏感,因此LTRRTLAUTO也是有效的。

html
<p dir="RTL">Right to left</p>

IDL反射属性HTMLElement.dir始终返回规范中提供的规范值(在此示例中为小写值)。设置值也会将其序列化为规范形式。

js
const pElement = document.querySelector("p");
console.log(pElement.dir); // "rtl"
pElement.dir = "RTL";
console.log(pElement.dir); // "rtl"

或者,您可以使用Element接口的getAttribute()方法。它将从HTML中获取属性值而不进行修改。

js
const pElement = document.querySelector("p");
console.log(pElement.getAttribute("dir")); // "RTL"

反射元素引用

注意:本节适用于包含元素引用的反射ARIA属性。其他/未来的反射元素引用属性很可能也适用同样的考虑。

某些属性以元素引用作为值:可以是元素的id值,也可以是以空格分隔的元素id值字符串。这些id值引用了与其他属性相关的其他元素,或者包含了属性所需的其他信息。这些属性通过相应的属性被反射,反射结果是一个HTMLElement派生对象实例的数组,这些实例与id值匹配,但有一些注意事项。

例如,aria-labelledby属性列出了包含元素可访问名称的元素的id值,这些名称包含在它们的内部文本中。下面的HTML展示了这一点,针对的是一个<input>元素,该元素在一个<span>元素中定义了标签,该span元素的id值为label_1label_2label_3

html
<span id="label_1">(Label 1 Text)</span>
<span id="label_2">(Label 2 Text)</span>
<input aria-labelledby="label_1 label_2 label_3" />

此属性由Element.ariaLabelledByElements属性反射,该属性返回具有相应id值的元素数组。属性和相应的属性可以按如下方式返回

js
const inputElement = document.querySelector("input");

console.log(inputElement.getAttribute("aria-labelledby"));
// "label_1 label_2 label_3"

console.log(inputElement.ariaLabelledByElements);
// [HTMLSpanElement, HTMLSpanElement]

从上面的代码可以注意到的第一点是,属性和属性包含的元素数量不同——属性不直接反射属性,因为引用label_3没有对应的元素。引用也可能不匹配,因为id超出了元素的范围。如果被引用的元素不在与该元素相同的DOM或Shadow DOM中,则可能发生这种情况,因为id仅在其声明的范围内有效。

我们可以迭代属性数组中的元素,在本例中是获取其内部文本中的可访问名称(这比使用属性更自然,因为我们不必先获取元素引用,然后使用它们来查找元素,而且我们只需要处理我们知道在当前范围内可用的元素)。

js
const inputElement = document.querySelector("input");
const accessibleName = inputElement.ariaLabelledByElements
  .map((e) => e.textContent.trim())
  .join(" ");
console.log(accessibleName);
// (Label 1 Text) (Label 2 Text)

设置属性和属性

对于普通的反射属性,对属性的更新会反映在相应的属性中,反之亦然。对于反射元素引用,情况并非如此。相反,设置属性会清除(取消设置)属性,因此属性和属性不再相互反射。例如,给定以下HTML

html
<span id="label_1">(Label 1 Text)</span>
<span id="label_2">(Label 2 Text)</span>
<input aria-labelledby="label_1 label_2" />

aria-labelledby的初始值为"label_1 label_2",但如果我们从DOM API设置它,该属性将重置为""

js
const inputElement = document.querySelector("input");

let attributeValue = inputElement.getAttribute("aria-labelledby");
console.log(attributeValue);
// "label_1 label_2"

// Set attribute using the reflected property
inputElement.ariaLabelledByElements = document.querySelectorAll("span");

attributeValue = inputElement.getAttribute("aria-labelledby");
console.log(attributeValue);
// ""

这是有道理的,因为否则您可能会将没有id引用的元素分配给该属性,因此无法在属性中表示。

设置属性值会恢复属性和属性之间的关系。继续上面的例子

js
inputElement.setAttribute("aria-labelledby", "label_1");

attributeValue = inputElement.getAttribute("aria-labelledby");
console.log(attributeValue);
// "label_1"

// Set attribute using the reflected property
console.log(inputElement.ariaLabelledByElements);
// [HTMLSpanElement] - for `label_1`

属性返回的数组是静态的,因此您不能修改返回的数组来引起相应属性的变化。当一个数组被分配给属性时,它会被复制,因此对属性的任何更改都不会反映在先前返回的属性数组中。

元素ID引用范围

属性元素引用只能引用与元素位于相同DOM或Shadow DOM中的其他元素,因为元素ID仅在其声明的范围内有效。

我们可以在以下代码中看到这一点。aria-labelledby属性的<input>元素引用了id为label_1label_2label_3的元素。然而,在这种情况下,label_3不是一个有效的id,因为它没有在与<input>元素相同的范围内定义。因此,标签将仅来自id为label_1label_2的元素。

html
<div id="in_dom">
  <span id="label_3">(Label 3 Text)</span>
</div>
<div id="host">
  <template shadowrootmode="open">
    <span id="label_1">(Label 1 Text)</span>
    <input aria-labelledby="label_1 label_2 label_3" />
    <span id="label_2">(Label 2 Text)</span>
  </template>
</div>

反射元素引用范围

使用从ARIA元素引用反射的实例属性(例如,aria-labelledbyElement.ariaLabelledByElements)时,范围规则略有不同。要处于范围内,目标元素必须与引用元素位于同一DOM或父DOM中。其他DOM中的元素,包括作为引用DOM的子级或同级元素的Shadow DOM,都超出范围。

下面的示例展示了这种情况:父DOM中的一个元素(label_3)被设置为目标,以及在同一Shadow Root中声明的id为label_1label_2的元素。这之所以有效,是因为所有目标元素都在引用元素的范围内。

html
<div id="in_dom">
  <span id="label_3">(Label 3 Text)</span>
</div>
<div id="host">
  <template shadowrootmode="open">
    <span id="label_1">(Label 1 Text)</span>
    <input id="input" />
    <span id="label_2">(Label 2 Text)</span>
  </template>
</div>
js
const host = document.getElementById("host");
const input = host.shadowRoot.getElementById("input");
input.ariaLabelledByElements = [
  host.shadowRoot.getElementById("label_1"),
  host.shadowRoot.getElementById("label_2"),
  document.getElementById("label_3"),
];

将DOM中的元素引用Shadow DOM中的另一个元素的等效代码将不起作用,因为嵌套Shadow DOM中的目标元素不在范围内。

html
<div id="in_dom">
  <span id="label_1">(Label 1 Text)</span>
  <input id="input" />
  <span id="label_2">(Label 2 Text)</span>
</div>
<div id="host">
  <template shadowrootmode="open">
    <span id="label_3">(Label 3 Text)</span>
  </template>
</div>
js
const host = document.getElementById("host");
const input = document.getElementById("input");
input.ariaLabelledByElements = [
  host.shadowRoot.getElementById("label_3"),
  document.getElementById("label_1"),
  document.getElementById("label_2"),
];

请注意,一个元素最初可能“在范围内”,然后被移出范围到嵌套的Shadow Root。在这种情况下,被引用的元素仍将在属性中列出,但不会在属性中返回。但请注意,如果元素被移回范围内,它将再次出现在反射的属性中。

属性/属性关系总结

包含元素引用的属性与其相应属性之间的关系如下:

  • 属性元素id引用仅对在与元素相同的DOM或Shadow DOM中声明的目标元素有效
  • 反射ARIA元素引用的属性可以定位相同范围或父范围内的元素。嵌套范围内的元素是不可访问的。
  • 设置属性会清除属性,并且属性和属性不再相互反射。如果使用Element.getAttribute()读取属性,则值为""
  • 使用Element.setAttribute()设置属性也会设置属性并恢复“反射关系”。
  • 使用稍后移出范围的引用值设置属性将导致从属性数组中移除相应的元素。但请注意,属性仍然包含引用,如果元素被移回范围内,属性将再次包含该元素(即,关系已恢复)。