mirror of
https://github.com/lexmount/moli.git
synced 2026-10-09 08:01:05 +00:00
fix(aria): resolve element reference attributes
This commit is contained in:
@@ -2910,8 +2910,6 @@ html/browsers/windows/nested-browsing-contexts/window-top-null.html
|
||||
html/browsers/windows/noreferrer-window-name.html
|
||||
html/cross-origin-embedder-policy/coep-on-response-from-service-worker.https.html
|
||||
html/cross-origin-embedder-policy/service-worker-cache-storage.https.html
|
||||
html/dom/aria-element-reflection-disconnected.html
|
||||
html/dom/aria-element-reflection.html
|
||||
html/dom/documents/dom-tree-accessors/nameditem-01.html
|
||||
html/dom/documents/dom-tree-accessors/nameditem-02.html
|
||||
html/dom/documents/dom-tree-accessors/nameditem-04.html
|
||||
|
||||
@@ -5380,6 +5380,8 @@ html/cross-origin-embedder-policy/no-secure-context.html
|
||||
html/cross-origin-opener-policy/no-https.html
|
||||
html/dom/access-key-label.html
|
||||
html/dom/aria-attribute-reflection.html
|
||||
html/dom/aria-element-reflection-disconnected.html
|
||||
html/dom/aria-element-reflection.html
|
||||
html/dom/documents/dom-tree-accessors/Document.body.html
|
||||
html/dom/documents/dom-tree-accessors/Document.getElementsByClassName-null-undef.html
|
||||
html/dom/documents/dom-tree-accessors/Element.getElementsByClassName-null-undef.html
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use super::Attribute;
|
||||
use crate::forms::{InputValueSanitizationContext, sanitize_input_value_for_type_with_context};
|
||||
use crate::native::NativeNodeId;
|
||||
use indexmap::IndexSet;
|
||||
use moli_html_input_type::InputType;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SelectedFile {
|
||||
@@ -123,6 +125,11 @@ impl ScriptElementState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct ExplicitAriaElementReferenceState {
|
||||
references_by_attribute: HashMap<String, Vec<NativeNodeId>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ElementControlState {
|
||||
input_value: Option<String>,
|
||||
@@ -164,9 +171,39 @@ pub struct ElementControlState {
|
||||
dialog_modal: bool,
|
||||
dialog_return_value: String,
|
||||
custom_states: IndexSet<String>,
|
||||
explicit_aria_element_references: Option<Box<ExplicitAriaElementReferenceState>>,
|
||||
}
|
||||
|
||||
impl ElementControlState {
|
||||
pub fn explicit_aria_element_references(&self, attribute: &str) -> Option<&[NativeNodeId]> {
|
||||
self.explicit_aria_element_references
|
||||
.as_deref()?
|
||||
.references_by_attribute
|
||||
.get(attribute)
|
||||
.map(Vec::as_slice)
|
||||
}
|
||||
|
||||
pub fn set_explicit_aria_element_references(
|
||||
&mut self,
|
||||
attribute: &str,
|
||||
references: Vec<NativeNodeId>,
|
||||
) {
|
||||
self.explicit_aria_element_references
|
||||
.get_or_insert_with(Default::default)
|
||||
.references_by_attribute
|
||||
.insert(attribute.to_owned(), references);
|
||||
}
|
||||
|
||||
pub fn clear_explicit_aria_element_references(&mut self, attribute: &str) {
|
||||
let Some(references) = self.explicit_aria_element_references.as_mut() else {
|
||||
return;
|
||||
};
|
||||
references.references_by_attribute.remove(attribute);
|
||||
if references.references_by_attribute.is_empty() {
|
||||
self.explicit_aria_element_references = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_element_parts(
|
||||
namespace: &str,
|
||||
local_name: &str,
|
||||
|
||||
@@ -59,6 +59,20 @@ fn initial_custom_element_state_for_identity(
|
||||
}
|
||||
}
|
||||
|
||||
fn is_aria_element_reference_attribute(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"aria-activedescendant"
|
||||
| "aria-controls"
|
||||
| "aria-describedby"
|
||||
| "aria-details"
|
||||
| "aria-errormessage"
|
||||
| "aria-flowto"
|
||||
| "aria-labelledby"
|
||||
| "aria-owns"
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Element {
|
||||
local_name: LocalName,
|
||||
@@ -167,6 +181,27 @@ impl Element {
|
||||
self.prefix.as_ref().map(AsRef::as_ref)
|
||||
}
|
||||
|
||||
pub fn explicit_aria_element_references(&self, attribute: &str) -> Option<&[NativeNodeId]> {
|
||||
self.control_state()
|
||||
.explicit_aria_element_references(attribute)
|
||||
}
|
||||
|
||||
pub fn set_explicit_aria_element_references(
|
||||
&mut self,
|
||||
attribute: &str,
|
||||
references: Vec<NativeNodeId>,
|
||||
) {
|
||||
self.control_state_mut()
|
||||
.set_explicit_aria_element_references(attribute, references);
|
||||
}
|
||||
|
||||
fn synchronize_aria_element_reference_attribute(&mut self, namespace: &str, local_name: &str) {
|
||||
if namespace.is_empty() && is_aria_element_reference_attribute(local_name) {
|
||||
self.rare_data
|
||||
.clear_explicit_aria_element_references(local_name);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_prefix(&mut self, prefix: Option<String>) -> bool {
|
||||
if self.prefix.as_deref() == prefix.as_deref() {
|
||||
return false;
|
||||
@@ -1205,6 +1240,7 @@ impl Element {
|
||||
prefix: Option<String>,
|
||||
value: String,
|
||||
) -> bool {
|
||||
self.synchronize_aria_element_reference_attribute(&namespace, &local_name);
|
||||
let next_value = value.clone();
|
||||
if let Some(index) = self
|
||||
.attributes
|
||||
@@ -1254,6 +1290,7 @@ impl Element {
|
||||
value: String,
|
||||
units: Vec<u16>,
|
||||
) -> bool {
|
||||
self.synchronize_aria_element_reference_attribute(&namespace, &local_name);
|
||||
let next_value = value.clone();
|
||||
let value_utf16_units =
|
||||
utf16_units_contain_unpaired_surrogate(&units).then(|| units.into_boxed_slice());
|
||||
@@ -1305,6 +1342,7 @@ impl Element {
|
||||
prefix: Option<String>,
|
||||
value: String,
|
||||
) -> bool {
|
||||
self.synchronize_aria_element_reference_attribute(&namespace, &local_name);
|
||||
let next_value = value.clone();
|
||||
if let Some(index) = self.attributes.iter().position(|attribute| {
|
||||
attribute.local_name() == local_name && attribute.namespace() == namespace
|
||||
@@ -1337,6 +1375,7 @@ impl Element {
|
||||
}
|
||||
|
||||
pub fn remove_attribute(&mut self, name: &str) -> bool {
|
||||
self.synchronize_aria_element_reference_attribute("", name);
|
||||
let Some(index) = self
|
||||
.attributes
|
||||
.iter()
|
||||
@@ -1353,6 +1392,7 @@ impl Element {
|
||||
}
|
||||
|
||||
pub fn remove_attribute_ns(&mut self, namespace: &str, local_name: &str) -> bool {
|
||||
self.synchronize_aria_element_reference_attribute(namespace, local_name);
|
||||
let Some(index) = self.attributes.iter().position(|attribute| {
|
||||
attribute.namespace() == namespace && attribute.local_name() == local_name
|
||||
}) else {
|
||||
|
||||
@@ -118,6 +118,16 @@ impl ElementRareData {
|
||||
.get_or_insert_with(ElementControlState::default)
|
||||
}
|
||||
|
||||
pub(super) fn clear_explicit_aria_element_references(&mut self, attribute: &str) {
|
||||
if let Some(control_state) = self
|
||||
.payload
|
||||
.as_deref_mut()
|
||||
.and_then(|payload| payload.control_state.as_mut())
|
||||
{
|
||||
control_state.clear_explicit_aria_element_references(attribute);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn custom_element_state(&self) -> CustomElementState {
|
||||
self.payload
|
||||
.as_deref()
|
||||
|
||||
@@ -154,6 +154,47 @@ fn template_contents_uses_and_releases_rare_data() {
|
||||
assert!(!template.rare_data.is_materialized());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aria_element_reference_state_is_owned_by_the_content_attribute() {
|
||||
let mut element = Element::new_html("div");
|
||||
assert!(element.set_attribute(
|
||||
"aria-controls".to_owned(),
|
||||
String::new(),
|
||||
None,
|
||||
String::new(),
|
||||
));
|
||||
element.set_explicit_aria_element_references("aria-controls", vec![NativeNodeId::new(7)]);
|
||||
|
||||
assert!(!element.set_attribute(
|
||||
"aria-controls".to_owned(),
|
||||
String::new(),
|
||||
None,
|
||||
String::new(),
|
||||
));
|
||||
assert_eq!(
|
||||
element.explicit_aria_element_references("aria-controls"),
|
||||
None
|
||||
);
|
||||
|
||||
element.set_explicit_aria_element_references("aria-controls", vec![NativeNodeId::new(8)]);
|
||||
assert!(element.set_attribute_ns(
|
||||
"aria-controls".to_owned(),
|
||||
"urn:example".to_owned(),
|
||||
Some("x".to_owned()),
|
||||
"foreign".to_owned(),
|
||||
));
|
||||
assert_eq!(
|
||||
element.explicit_aria_element_references("aria-controls"),
|
||||
Some([NativeNodeId::new(8)].as_slice())
|
||||
);
|
||||
|
||||
assert!(element.remove_attribute("aria-controls"));
|
||||
assert_eq!(
|
||||
element.explicit_aria_element_references("aria-controls"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn html_element_interface_name_covers_replay_tags() {
|
||||
assert_eq!(html_element_interface_name("meta"), "HTMLMetaElement");
|
||||
|
||||
@@ -3,6 +3,33 @@ use std::sync::Arc;
|
||||
use super::*;
|
||||
|
||||
impl DomHost {
|
||||
pub fn explicit_aria_element_references(
|
||||
&self,
|
||||
handle: DomHandle,
|
||||
attribute: &str,
|
||||
) -> Option<Vec<DomHandle>> {
|
||||
self.node(handle)?
|
||||
.as_element()?
|
||||
.explicit_aria_element_references(attribute)
|
||||
.map(<[DomHandle]>::to_vec)
|
||||
}
|
||||
|
||||
pub fn set_explicit_aria_element_references(
|
||||
&mut self,
|
||||
handle: DomHandle,
|
||||
attribute: &str,
|
||||
references: Vec<DomHandle>,
|
||||
) -> bool {
|
||||
let Some(element) = self
|
||||
.node_mut(handle)
|
||||
.and_then(|node| node.data_mut().as_element_mut())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
element.set_explicit_aria_element_references(attribute, references);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn get_attribute(&self, handle: DomHandle, name: &str) -> Option<String> {
|
||||
self.dom.get_attribute(handle, name)
|
||||
}
|
||||
|
||||
@@ -8,16 +8,16 @@ use super::super::{
|
||||
v8_string, v8str,
|
||||
},
|
||||
};
|
||||
use super::JsContextHost;
|
||||
use super::node::{
|
||||
element_name_for_owner_document, node_is_document, node_is_element,
|
||||
node_runtime_and_handle_from_object, node_runtime_and_handle_from_object_or_detached,
|
||||
node_text_content_getter_function, require_element_getter_receiver,
|
||||
require_element_setter_receiver, set_text_content_in_reaction_scope,
|
||||
throw_incompatible_getter_receiver, throw_incompatible_method_receiver,
|
||||
throw_incompatible_setter_receiver,
|
||||
current_or_live_delegate_node_arg_handle, element_name_for_owner_document, node_is_document,
|
||||
node_is_element, node_runtime_and_handle_from_object,
|
||||
node_runtime_and_handle_from_object_or_detached, node_text_content_getter_function,
|
||||
require_element_getter_receiver, require_element_setter_receiver,
|
||||
set_text_content_in_reaction_scope, throw_incompatible_getter_receiver,
|
||||
throw_incompatible_method_receiver, throw_incompatible_setter_receiver,
|
||||
};
|
||||
use crate::{document_runtime::DomHandle, web_api_interfaces, webidl};
|
||||
use super::{JsContextHost, wrapped_handle_value};
|
||||
use crate::{custom_elements, document_runtime::DomHandle, web_api_interfaces, webidl};
|
||||
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiTemplateValue};
|
||||
|
||||
mod activation;
|
||||
@@ -7527,14 +7527,217 @@ fn aria_string_attribute_setter_callback<'s>(
|
||||
rv.set_undefined();
|
||||
}
|
||||
|
||||
fn aria_element_reference_slot(attribute: &str) -> String {
|
||||
format!("__moliAriaElementReference:{attribute}")
|
||||
fn aria_element_reference_array_cache_slot(attribute: &str) -> String {
|
||||
format!("__moliAriaElementReferenceArrayCache:{attribute}")
|
||||
}
|
||||
|
||||
fn aria_element_reference_is_singular(attribute: &str) -> bool {
|
||||
attribute == "aria-activedescendant"
|
||||
}
|
||||
|
||||
struct AriaElementReferenceValue<'s> {
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
runtime_ptr: *mut JsContextHost,
|
||||
handle: DomHandle,
|
||||
}
|
||||
|
||||
impl<'s> webidl::WebIdlConverter<'s> for AriaElementReferenceValue<'s> {
|
||||
type Options = ();
|
||||
|
||||
fn convert(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
_context: webidl::Context,
|
||||
_options: &Self::Options,
|
||||
) -> std::result::Result<Self, webidl::WebIdlError> {
|
||||
let object = v8::Local::<v8::Object>::try_from(value).map_err(|_| {
|
||||
webidl::WebIdlError::custom_message("ARIA element references must be Elements.")
|
||||
})?;
|
||||
let (runtime_ptr, handle) = node_runtime_and_handle_from_object_or_detached(scope, object)
|
||||
.map_err(|_| {
|
||||
webidl::WebIdlError::custom_message("ARIA element references must be Elements.")
|
||||
})?;
|
||||
if !node_is_element(unsafe { &*runtime_ptr }, handle) {
|
||||
return Err(webidl::WebIdlError::custom_message(
|
||||
"ARIA element references must be Elements.",
|
||||
));
|
||||
}
|
||||
Ok(Self {
|
||||
object,
|
||||
runtime_ptr,
|
||||
handle,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn aria_element_reference_handle_for_owner(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
owner_runtime_ptr: *mut JsContextHost,
|
||||
reference: AriaElementReferenceValue<'_>,
|
||||
) -> Option<DomHandle> {
|
||||
current_or_live_delegate_node_arg_handle(scope, owner_runtime_ptr, reference.object.into())
|
||||
.filter(|handle| node_is_element(unsafe { &*owner_runtime_ptr }, *handle))
|
||||
.or_else(|| (reference.runtime_ptr == owner_runtime_ptr).then_some(reference.handle))
|
||||
}
|
||||
|
||||
fn aria_element_reference_is_in_valid_scope(
|
||||
runtime: &JsContextHost,
|
||||
owner: DomHandle,
|
||||
candidate: DomHandle,
|
||||
) -> bool {
|
||||
if !node_is_element(runtime, candidate) {
|
||||
return false;
|
||||
}
|
||||
let Some(candidate_root) = runtime.dom_host().root_node_handle(candidate) else {
|
||||
return false;
|
||||
};
|
||||
let Some(mut owner_root) = runtime.dom_host().root_node_handle(owner) else {
|
||||
return false;
|
||||
};
|
||||
loop {
|
||||
if candidate_root == owner_root {
|
||||
return true;
|
||||
}
|
||||
if !runtime.dom_host().is_shadow_root(owner_root) {
|
||||
return false;
|
||||
}
|
||||
let Some(host) = runtime.dom_host().shadow_root_host(owner_root) else {
|
||||
return false;
|
||||
};
|
||||
let Some(next_root) = runtime.dom_host().root_node_handle(host) else {
|
||||
return false;
|
||||
};
|
||||
owner_root = next_root;
|
||||
}
|
||||
}
|
||||
|
||||
fn aria_element_by_id_including_disconnected(
|
||||
runtime: &JsContextHost,
|
||||
owner: DomHandle,
|
||||
id: &str,
|
||||
) -> Option<DomHandle> {
|
||||
if id.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let root = runtime.dom_host().root_node_handle(owner)?;
|
||||
let mut stack = runtime
|
||||
.dom_host()
|
||||
.child_handles_reversed(root)
|
||||
.collect::<Vec<_>>();
|
||||
while let Some(candidate) = stack.pop() {
|
||||
if node_is_element(runtime, candidate)
|
||||
&& runtime.dom_host().get_attribute(candidate, "id").as_deref() == Some(id)
|
||||
{
|
||||
return Some(candidate);
|
||||
}
|
||||
stack.extend(runtime.dom_host().child_handles_reversed(candidate));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn aria_element_reference_content_handles(
|
||||
runtime: &JsContextHost,
|
||||
owner: DomHandle,
|
||||
attribute: &str,
|
||||
) -> Option<Vec<DomHandle>> {
|
||||
let value = runtime.dom_host().get_attribute(owner, attribute)?;
|
||||
if aria_element_reference_is_singular(attribute) {
|
||||
return Some(
|
||||
aria_element_by_id_including_disconnected(runtime, owner, &value)
|
||||
.into_iter()
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
Some(
|
||||
value
|
||||
.split([' ', '\t', '\n', '\r', '\u{000c}'])
|
||||
.filter(|token| !token.is_empty())
|
||||
.filter_map(|token| aria_element_by_id_including_disconnected(runtime, owner, token))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn aria_element_reference_handles(
|
||||
runtime: &JsContextHost,
|
||||
owner: DomHandle,
|
||||
attribute: &str,
|
||||
) -> Option<Vec<DomHandle>> {
|
||||
match runtime
|
||||
.dom_host()
|
||||
.explicit_aria_element_references(owner, attribute)
|
||||
{
|
||||
Some(references) => Some(
|
||||
references
|
||||
.into_iter()
|
||||
.filter(|candidate| {
|
||||
aria_element_reference_is_in_valid_scope(runtime, owner, *candidate)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
None => aria_element_reference_content_handles(runtime, owner, attribute),
|
||||
}
|
||||
}
|
||||
|
||||
fn aria_element_reference_value<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
runtime_ptr: *mut JsContextHost,
|
||||
handle: DomHandle,
|
||||
) -> Option<v8::Local<'s, v8::Value>> {
|
||||
wrapped_handle_value(scope, runtime_ptr, handle)
|
||||
}
|
||||
|
||||
fn aria_element_reference_array_values<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
runtime_ptr: *mut JsContextHost,
|
||||
handles: Vec<DomHandle>,
|
||||
) -> Option<Vec<v8::Local<'s, v8::Value>>> {
|
||||
handles
|
||||
.into_iter()
|
||||
.map(|handle| aria_element_reference_value(scope, runtime_ptr, handle))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn aria_cached_frozen_element_array<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
receiver: v8::Local<'s, v8::Object>,
|
||||
attribute: &str,
|
||||
values: &[v8::Local<'s, v8::Value>],
|
||||
) -> v8::Local<'s, v8::Array> {
|
||||
let slot = aria_element_reference_array_cache_slot(attribute);
|
||||
if let Some(cached) = get_private_value(scope, receiver, &slot)
|
||||
.and_then(|value| v8::Local::<v8::Array>::try_from(value).ok())
|
||||
&& cached.length() as usize == values.len()
|
||||
{
|
||||
let mut equal = true;
|
||||
for (index, expected) in values.iter().copied().enumerate() {
|
||||
if !cached
|
||||
.get_index(scope, index as u32)
|
||||
.is_some_and(|actual| actual.strict_equals(expected))
|
||||
{
|
||||
equal = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if equal {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
let array = v8::Array::new_with_elements(scope, values);
|
||||
let _ = array.set_integrity_level(scope, v8::IntegrityLevel::Frozen);
|
||||
set_private_value(scope, receiver, &slot, array.into());
|
||||
array
|
||||
}
|
||||
|
||||
fn clear_aria_element_reference_array_cache(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
receiver: v8::Local<'_, v8::Object>,
|
||||
attribute: &str,
|
||||
) {
|
||||
let slot = aria_element_reference_array_cache_slot(attribute);
|
||||
let undefined = v8::undefined(scope);
|
||||
set_private_value(scope, receiver, &slot, undefined.into());
|
||||
}
|
||||
|
||||
fn aria_element_reference_attribute_getter_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
@@ -7544,15 +7747,85 @@ fn aria_element_reference_attribute_getter_callback<'s>(
|
||||
rv.set_null();
|
||||
return;
|
||||
};
|
||||
let slot = aria_element_reference_slot(&attribute);
|
||||
if let Some(value) = get_private_value(scope, args.this(), &slot) {
|
||||
rv.set(value);
|
||||
let Some((runtime_ptr, owner)) = element_getter_receiver(scope, args.this(), &attribute) else {
|
||||
return;
|
||||
};
|
||||
let Some(handles) = aria_element_reference_handles(unsafe { &*runtime_ptr }, owner, &attribute)
|
||||
else {
|
||||
clear_aria_element_reference_array_cache(scope, args.this(), &attribute);
|
||||
rv.set_null();
|
||||
return;
|
||||
};
|
||||
if aria_element_reference_is_singular(&attribute) {
|
||||
match handles
|
||||
.into_iter()
|
||||
.next()
|
||||
.and_then(|handle| aria_element_reference_value(scope, runtime_ptr, handle))
|
||||
{
|
||||
Some(value) => rv.set(value),
|
||||
None => rv.set_null(),
|
||||
}
|
||||
return;
|
||||
}
|
||||
if aria_element_reference_is_singular(&attribute) {
|
||||
let Some(values) = aria_element_reference_array_values(scope, runtime_ptr, handles) else {
|
||||
rv.set_null();
|
||||
} else {
|
||||
rv.set(v8::Array::new(scope, 0).into());
|
||||
return;
|
||||
};
|
||||
let array = aria_cached_frozen_element_array(scope, args.this(), &attribute, &values);
|
||||
rv.set(array.into());
|
||||
}
|
||||
|
||||
fn set_explicit_aria_element_references(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
runtime_ptr: *mut JsContextHost,
|
||||
owner: DomHandle,
|
||||
attribute: &str,
|
||||
handles: Vec<DomHandle>,
|
||||
) {
|
||||
custom_elements::with_custom_element_reaction_scope(scope, runtime_ptr, |scope| {
|
||||
let _ = unsafe { &mut *runtime_ptr }.set_attribute_appending_to_current_reaction_queue(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
owner,
|
||||
attribute,
|
||||
"",
|
||||
);
|
||||
let _ = unsafe { &mut *runtime_ptr }
|
||||
.dom_host_mut()
|
||||
.set_explicit_aria_element_references(owner, attribute, handles);
|
||||
});
|
||||
}
|
||||
|
||||
fn clear_explicit_aria_element_references(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
runtime_ptr: *mut JsContextHost,
|
||||
owner: DomHandle,
|
||||
attribute: &str,
|
||||
) {
|
||||
custom_elements::with_custom_element_reaction_scope(scope, runtime_ptr, |scope| {
|
||||
let _ = unsafe { &mut *runtime_ptr }.remove_attribute_appending_to_current_reaction_queue(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
owner,
|
||||
attribute,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn converted_aria_element_reference<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
) -> Option<AriaElementReferenceValue<'s>> {
|
||||
match webidl::convert::<AriaElementReferenceValue<'s>>(
|
||||
scope,
|
||||
value,
|
||||
webidl::Context::argument("Element ARIA element reflection", 1),
|
||||
) {
|
||||
Ok(reference) => Some(reference),
|
||||
Err(error) => {
|
||||
webidl::throw_error(scope, &error);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7565,14 +7838,45 @@ fn aria_element_reference_attribute_setter_callback<'s>(
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
let slot = aria_element_reference_slot(&attribute);
|
||||
set_private_value(scope, args.this(), &slot, args.get(0));
|
||||
let Ok((runtime_ptr, handle)) =
|
||||
node_runtime_and_handle_from_object_or_detached(scope, args.this())
|
||||
else {
|
||||
rv.set_undefined();
|
||||
let Some((runtime_ptr, owner)) = element_setter_receiver(scope, args.this(), &attribute) else {
|
||||
return;
|
||||
};
|
||||
let _ = unsafe { &mut *runtime_ptr }.set_attribute(scope, runtime_ptr, handle, &attribute, "");
|
||||
let value = args.get(0);
|
||||
if value.is_null_or_undefined() {
|
||||
clear_explicit_aria_element_references(scope, runtime_ptr, owner, &attribute);
|
||||
rv.set_undefined();
|
||||
return;
|
||||
}
|
||||
if aria_element_reference_is_singular(&attribute) {
|
||||
let Some(reference) = converted_aria_element_reference(scope, value) else {
|
||||
return;
|
||||
};
|
||||
let handles = aria_element_reference_handle_for_owner(scope, runtime_ptr, reference)
|
||||
.into_iter()
|
||||
.collect();
|
||||
set_explicit_aria_element_references(scope, runtime_ptr, owner, &attribute, handles);
|
||||
rv.set_undefined();
|
||||
return;
|
||||
}
|
||||
let sequence = match webidl::convert::<webidl::Sequence<AriaElementReferenceValue<'s>>>(
|
||||
scope,
|
||||
value,
|
||||
webidl::Context::argument("Element ARIA element reflection", 1),
|
||||
) {
|
||||
Ok(sequence) => sequence,
|
||||
Err(error) => {
|
||||
webidl::throw_error(scope, &error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut handles = Vec::with_capacity(sequence.0.len());
|
||||
for reference in sequence.0 {
|
||||
if let Some(handle) = aria_element_reference_handle_for_owner(scope, runtime_ptr, reference)
|
||||
&& !handles.contains(&handle)
|
||||
{
|
||||
handles.push(handle);
|
||||
}
|
||||
}
|
||||
set_explicit_aria_element_references(scope, runtime_ptr, owner, &attribute, handles);
|
||||
rv.set_undefined();
|
||||
}
|
||||
|
||||
@@ -6063,32 +6063,102 @@ fn custom_element_popover_reflection_is_visible_on_html_element_prototype() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn element_aria_element_reference_reflection_preserves_assigned_value() {
|
||||
fn element_aria_element_reference_reflection_tracks_dom_owned_associations() {
|
||||
let mut vm = new_storage_test_vm("https://example.com/");
|
||||
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
r##"
|
||||
(() => {
|
||||
const target = document.createElement("div");
|
||||
const element = document.createElement("div");
|
||||
const defaultReferences = element.ariaDescribedByElements;
|
||||
element.ariaControlsElements = [target];
|
||||
element.ariaActiveDescendantElement = target;
|
||||
const probe = callback => {
|
||||
try {
|
||||
callback();
|
||||
return "ok";
|
||||
} catch (error) {
|
||||
return error && error.name;
|
||||
}
|
||||
};
|
||||
const html = document.documentElement ||
|
||||
document.appendChild(document.createElement("html"));
|
||||
const body = document.body ||
|
||||
html.appendChild(document.createElement("body"));
|
||||
const container = document.createElement("div");
|
||||
container.innerHTML = `
|
||||
<input aria-activedescendant="first" aria-describedby="first second">
|
||||
<p id="first"></p><p id="second"></p>`;
|
||||
body.appendChild(container);
|
||||
const element = container.querySelector("input");
|
||||
const first = container.querySelector("#first");
|
||||
const second = container.querySelector("#second");
|
||||
const initialDescriptions = element.ariaDescribedByElements;
|
||||
const initialActive = element.ariaActiveDescendantElement;
|
||||
|
||||
container.remove();
|
||||
const disconnectedDescriptions = element.ariaDescribedByElements;
|
||||
const disconnectedActive = element.ariaActiveDescendantElement;
|
||||
body.appendChild(container);
|
||||
|
||||
const assigned = [first, second];
|
||||
element.ariaControlsElements = assigned;
|
||||
assigned.pop();
|
||||
const explicitControlsAttributeBlank =
|
||||
element.getAttribute("aria-controls") === "";
|
||||
const controls = element.ariaControlsElements;
|
||||
const cachedControls = element.ariaControlsElements;
|
||||
container.remove();
|
||||
const disconnectedControls = element.ariaControlsElements;
|
||||
body.appendChild(container);
|
||||
|
||||
element.setAttribute("aria-controls", "second");
|
||||
const contentAttributeControls = element.ariaControlsElements;
|
||||
element.removeAttribute("aria-controls");
|
||||
|
||||
element.ariaActiveDescendantElement = first;
|
||||
const explicitActive = element.ariaActiveDescendantElement;
|
||||
element.ariaActiveDescendantElement = null;
|
||||
|
||||
const missing = document.createElement("div");
|
||||
const reactions = [];
|
||||
class AriaReferenceElement extends HTMLElement {
|
||||
static observedAttributes = ["aria-controls"];
|
||||
attributeChangedCallback() {
|
||||
reactions.push(this.ariaControlsElements?.[0] === first);
|
||||
}
|
||||
}
|
||||
customElements.define("aria-reference-element", AriaReferenceElement);
|
||||
const custom = document.createElement("aria-reference-element");
|
||||
body.appendChild(custom);
|
||||
custom.ariaControlsElements = [first];
|
||||
return [
|
||||
Array.isArray(defaultReferences),
|
||||
defaultReferences.length,
|
||||
element.getAttribute("aria-controls") === "",
|
||||
element.ariaControlsElements[0] === target,
|
||||
element.getAttribute("aria-activedescendant") === "",
|
||||
element.ariaActiveDescendantElement === target
|
||||
missing.ariaDescribedByElements === null,
|
||||
initialDescriptions.length === 2 && initialDescriptions[0] === first && initialDescriptions[1] === second,
|
||||
initialActive === first,
|
||||
disconnectedDescriptions.length === 2 && disconnectedDescriptions[0] === first && disconnectedDescriptions[1] === second,
|
||||
disconnectedActive === first,
|
||||
explicitControlsAttributeBlank,
|
||||
controls.length === 2 && controls[0] === first && controls[1] === second,
|
||||
Object.isFrozen(controls),
|
||||
controls === cachedControls,
|
||||
disconnectedControls === controls,
|
||||
contentAttributeControls.length === 1 && contentAttributeControls[0] === second,
|
||||
element.ariaControlsElements === null,
|
||||
explicitActive === first,
|
||||
element.ariaActiveDescendantElement === null,
|
||||
!element.hasAttribute("aria-activedescendant"),
|
||||
probe(() => { element.ariaActiveDescendantElement = "not an element"; }),
|
||||
probe(() => { element.ariaControlsElements = [first, 1]; }),
|
||||
probe(() => { element.ariaControlsElements = first; }),
|
||||
reactions.length === 1 && reactions[0]
|
||||
].join("|");
|
||||
})()
|
||||
"#,
|
||||
"##,
|
||||
)
|
||||
.expect("ARIA element reference reflection probe should evaluate");
|
||||
|
||||
assert_eq!(result, "true|0|true|true|true|true");
|
||||
assert_eq!(
|
||||
result,
|
||||
"true|true|true|true|true|true|true|true|true|true|true|true|true|true|true|TypeError|TypeError|TypeError|true"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2677,7 +2677,11 @@ fn detached_reflected_element_attributes_use_owner_prototype_accessors() {
|
||||
assert(section.getAttribute("aria-label") === "Proxy label", "proxy aria-label attribute");
|
||||
const controls = [div];
|
||||
section.ariaControlsElements = controls;
|
||||
assert(section.ariaControlsElements === controls, "proxy ariaControlsElements");
|
||||
const reflectedControls = section.ariaControlsElements;
|
||||
assert(reflectedControls !== controls, "proxy ariaControlsElements snapshots input");
|
||||
assert(reflectedControls.length === 1 && reflectedControls[0] === div, "proxy ariaControlsElements");
|
||||
assert(Object.isFrozen(reflectedControls), "proxy ariaControlsElements frozen");
|
||||
assert(section.ariaControlsElements === reflectedControls, "proxy ariaControlsElements cached");
|
||||
assert(section.getAttribute("aria-controls") === "", "proxy aria-controls attribute");
|
||||
assert(!own(section, "ariaControlsElements"), "ariaControlsElements should stay inherited after set");
|
||||
assert(section.contentEditable === "plaintext-only", "proxy contentEditable");
|
||||
@@ -2709,7 +2713,7 @@ fn detached_reflected_element_attributes_use_owner_prototype_accessors() {
|
||||
assert(section.attributes.getNamedItem("data-x").value === "1", "proxy attributes after delete");
|
||||
assert(section.shadowRoot === root, "proxy shadowRoot after delete");
|
||||
assert(section.ariaLabel === "Proxy label", "proxy ariaLabel after delete");
|
||||
assert(section.ariaControlsElements === controls, "proxy ariaControlsElements after delete");
|
||||
assert(section.ariaControlsElements === reflectedControls, "proxy ariaControlsElements after delete");
|
||||
assert(section.contentEditable === "plaintext-only", "proxy contentEditable after delete");
|
||||
assert(section.isContentEditable === true, "proxy isContentEditable after delete");
|
||||
assert(section.getHTML() === "<b>x</b>", "proxy getHTML after delete");
|
||||
|
||||
Reference in New Issue
Block a user