Update document focus when element focusability changes.

This commit is contained in:
Josh Matthews 2020-06-12 18:19:49 -04:00
parent 757371f4f0
commit d55424e88f
8 changed files with 89 additions and 72 deletions

View file

@ -1025,6 +1025,17 @@ impl Document {
*self.focus_transaction.borrow_mut() = FocusTransaction::InTransaction(Default::default()); *self.focus_transaction.borrow_mut() = FocusTransaction::InTransaction(Default::default());
} }
/// <https://html.spec.whatwg.org/multipage/#focus-fixup-rule>
pub(crate) fn perform_focus_fixup_rule(&self, not_focusable: &Element) {
if Some(not_focusable) != self.focused.get().as_ref().map(|e| &**e) {
return;
}
self.request_focus(
self.GetBody().as_ref().map(|e| &*e.upcast()),
FocusType::Element,
)
}
/// Request that the given element receive focus once the current transaction is complete. /// Request that the given element receive focus once the current transaction is complete.
/// If None is passed, then whatever element is currently focused will no longer be focused /// If None is passed, then whatever element is currently focused will no longer be focused
/// once the transaction is complete. /// once the transaction is complete.

View file

@ -1300,12 +1300,12 @@ impl Element {
if self.is_actually_disabled() { if self.is_actually_disabled() {
return false; return false;
} }
// TODO: Check whether the element is being rendered (i.e. not hidden).
let node = self.upcast::<Node>(); let node = self.upcast::<Node>();
if node.get_flag(NodeFlags::SEQUENTIALLY_FOCUSABLE) { if node.get_flag(NodeFlags::SEQUENTIALLY_FOCUSABLE) {
return true; return true;
} }
// https://html.spec.whatwg.org/multipage/#specially-focusable
// <a>, <input>, <select>, and <textrea> are inherently focusable.
match node.type_id() { match node.type_id() {
NodeTypeId::Element(ElementTypeId::HTMLElement( NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLAnchorElement, HTMLElementTypeId::HTMLAnchorElement,
@ -1832,6 +1832,67 @@ impl Element {
pub fn get_name(&self) -> Option<Atom> { pub fn get_name(&self) -> Option<Atom> {
self.rare_data().as_ref()?.name_attribute.clone() self.rare_data().as_ref()?.name_attribute.clone()
} }
fn is_sequentially_focusable(&self) -> bool {
let element = self.upcast::<Element>();
let node = self.upcast::<Node>();
if !node.is_connected() {
return false;
}
if element.has_attribute(&local_name!("hidden")) {
return false;
}
if self.disabled_state() {
return false;
}
if element.has_attribute(&local_name!("tabindex")) {
return true;
}
match node.type_id() {
// <button>, <select>, <iframe>, and <textarea> are implicitly focusable.
NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLButtonElement,
)) |
NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLSelectElement,
)) |
NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLIFrameElement,
)) |
NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLTextAreaElement,
)) => true,
// Links that generate actual links are focusable.
NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLLinkElement)) |
NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLAnchorElement,
)) => element.has_attribute(&local_name!("href")),
//TODO focusable if editing host
//TODO focusable if "sorting interface th elements"
_ => {
// Draggable elements are focusable.
element.get_string_attribute(&local_name!("draggable")) == "true"
},
}
}
pub(crate) fn update_sequentially_focusable_status(&self) {
let node = self.upcast::<Node>();
let is_sequentially_focusable = self.is_sequentially_focusable();
node.set_flag(NodeFlags::SEQUENTIALLY_FOCUSABLE, is_sequentially_focusable);
// https://html.spec.whatwg.org/multipage/#focus-fixup-rule
if !is_sequentially_focusable {
let document = document_from_node(self);
document.perform_focus_fixup_rule(self);
}
}
} }
impl ElementMethods for Element { impl ElementMethods for Element {
@ -2751,6 +2812,9 @@ impl VirtualMethods for Element {
let node = self.upcast::<Node>(); let node = self.upcast::<Node>();
let doc = node.owner_doc(); let doc = node.owner_doc();
match attr.local_name() { match attr.local_name() {
&local_name!("tabindex") | &local_name!("draggable") | &local_name!("hidden") => {
self.update_sequentially_focusable_status()
},
&local_name!("style") => { &local_name!("style") => {
// Modifying the `style` attribute might change style. // Modifying the `style` attribute might change style.
*self.style_attribute.borrow_mut() = match mutation { *self.style_attribute.borrow_mut() = match mutation {
@ -2917,6 +2981,8 @@ impl VirtualMethods for Element {
return; return;
} }
self.update_sequentially_focusable_status();
if let Some(ref value) = *self.id_attribute.borrow() { if let Some(ref value) = *self.id_attribute.borrow() {
if let Some(shadow_root) = self.upcast::<Node>().containing_shadow_root() { if let Some(shadow_root) = self.upcast::<Node>().containing_shadow_root() {
shadow_root.register_element_id(self, value.clone()); shadow_root.register_element_id(self, value.clone());
@ -2945,6 +3011,8 @@ impl VirtualMethods for Element {
return; return;
} }
self.update_sequentially_focusable_status();
let doc = document_from_node(self); let doc = document_from_node(self);
if let Some(ref shadow_root) = self.shadow_root() { if let Some(ref shadow_root) = self.shadow_root() {

View file

@ -233,6 +233,7 @@ impl VirtualMethods for HTMLButtonElement {
el.check_ancestors_disabled_state_for_form_control(); el.check_ancestors_disabled_state_for_form_control();
}, },
} }
el.update_sequentially_focusable_status();
}, },
&local_name!("type") => match mutation { &local_name!("type") => match mutation {
AttributeMutation::Set(_) => { AttributeMutation::Set(_) => {

View file

@ -28,7 +28,7 @@ use crate::dom::htmlinputelement::{HTMLInputElement, InputType};
use crate::dom::htmllabelelement::HTMLLabelElement; use crate::dom::htmllabelelement::HTMLLabelElement;
use crate::dom::htmltextareaelement::HTMLTextAreaElement; use crate::dom::htmltextareaelement::HTMLTextAreaElement;
use crate::dom::node::{document_from_node, window_from_node}; use crate::dom::node::{document_from_node, window_from_node};
use crate::dom::node::{BindContext, Node, NodeFlags, ShadowIncluding}; use crate::dom::node::{Node, ShadowIncluding};
use crate::dom::text::Text; use crate::dom::text::Text;
use crate::dom::virtualmethods::VirtualMethods; use crate::dom::virtualmethods::VirtualMethods;
use dom_struct::dom_struct; use dom_struct::dom_struct;
@ -91,53 +91,6 @@ impl HTMLElement {
let eventtarget = self.upcast::<EventTarget>(); let eventtarget = self.upcast::<EventTarget>();
eventtarget.is::<HTMLBodyElement>() || eventtarget.is::<HTMLFrameSetElement>() eventtarget.is::<HTMLBodyElement>() || eventtarget.is::<HTMLFrameSetElement>()
} }
fn update_sequentially_focusable_status(&self) {
let element = self.upcast::<Element>();
let node = self.upcast::<Node>();
if element.has_attribute(&local_name!("tabindex")) {
node.set_flag(NodeFlags::SEQUENTIALLY_FOCUSABLE, true);
} else {
match node.type_id() {
NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLButtonElement,
)) |
NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLSelectElement,
)) |
NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLIFrameElement,
)) |
NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLTextAreaElement,
)) => node.set_flag(NodeFlags::SEQUENTIALLY_FOCUSABLE, true),
NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLLinkElement,
)) |
NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLAnchorElement,
)) => {
if element.has_attribute(&local_name!("href")) {
node.set_flag(NodeFlags::SEQUENTIALLY_FOCUSABLE, true);
}
},
_ => {
if let Some(attr) = element.get_attribute(&ns!(), &local_name!("draggable")) {
let value = attr.value();
let is_true = match *value {
AttrValue::String(ref string) => string == "true",
_ => false,
};
node.set_flag(NodeFlags::SEQUENTIALLY_FOCUSABLE, is_true);
} else {
node.set_flag(NodeFlags::SEQUENTIALLY_FOCUSABLE, false);
}
//TODO set SEQUENTIALLY_FOCUSABLE flag if editing host
//TODO set SEQUENTIALLY_FOCUSABLE flag if "sorting interface th elements"
},
}
}
}
} }
impl HTMLElementMethods for HTMLElement { impl HTMLElementMethods for HTMLElement {
@ -855,13 +808,6 @@ impl VirtualMethods for HTMLElement {
} }
} }
fn bind_to_tree(&self, context: &BindContext) {
if let Some(ref s) = self.super_type() {
s.bind_to_tree(context);
}
self.update_sequentially_focusable_status();
}
fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue { fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue {
match name { match name {
&local_name!("itemprop") => AttrValue::from_serialized_tokenlist(value.into()), &local_name!("itemprop") => AttrValue::from_serialized_tokenlist(value.into()),

View file

@ -182,14 +182,17 @@ impl VirtualMethods for HTMLFieldSetElement {
let el = field.downcast::<Element>().unwrap(); let el = field.downcast::<Element>().unwrap();
el.set_disabled_state(true); el.set_disabled_state(true);
el.set_enabled_state(false); el.set_enabled_state(false);
el.update_sequentially_focusable_status();
} }
} else { } else {
for field in fields { for field in fields {
let el = field.downcast::<Element>().unwrap(); let el = field.downcast::<Element>().unwrap();
el.check_disabled_attribute(); el.check_disabled_attribute();
el.check_ancestors_disabled_state_for_form_control(); el.check_ancestors_disabled_state_for_form_control();
el.update_sequentially_focusable_status();
} }
} }
el.update_sequentially_focusable_status();
}, },
&local_name!("form") => { &local_name!("form") => {
self.form_attribute_mutated(mutation); self.form_attribute_mutated(mutation);

View file

@ -2291,6 +2291,8 @@ impl VirtualMethods for HTMLInputElement {
let read_write = !(self.ReadOnly() || el.disabled_state()); let read_write = !(self.ReadOnly() || el.disabled_state());
el.set_read_write_state(read_write); el.set_read_write_state(read_write);
} }
el.update_sequentially_focusable_status();
}, },
&local_name!("checked") if !self.checked_changed.get() => { &local_name!("checked") if !self.checked_changed.get() => {
let checked_state = match mutation { let checked_state = match mutation {

View file

@ -478,6 +478,7 @@ impl VirtualMethods for HTMLTextAreaElement {
} }
}, },
} }
el.update_sequentially_focusable_status();
}, },
local_name!("maxlength") => match *attr.value() { local_name!("maxlength") => match *attr.value() {
AttrValue::Int(_, value) => { AttrValue::Int(_, value) => {

View file

@ -2,21 +2,6 @@
[Disabling contenteditable] [Disabling contenteditable]
expected: FAIL expected: FAIL
[Hiding the active element]
expected: FAIL
[Disabling the active element (making it expressly inert)]
expected: FAIL
[Changing the first legend element in disabled <fieldset>] [Changing the first legend element in disabled <fieldset>]
expected: FAIL expected: FAIL
[Disabling <fieldset> affects its descendants]
expected: FAIL
[Removing the tabindex attribute from a div]
expected: FAIL
[Removing the active element from the DOM]
expected: FAIL