Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/blitz-dom/assets/default.css
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,7 @@ textarea {
background-color: white;
}

input:focus,
textarea:focus {
:focus-visible {
outline: 2px solid #4D90FE;
}

Expand Down
41 changes: 38 additions & 3 deletions packages/blitz-dom/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,16 @@ impl Document for Rc<RefCell<BaseDocument>> {
}
}

/// How focus was moved to an element. Determines whether the element
/// matches `:focus-visible` (keyboard and script focus show a focus ring;
/// pointer focus only does so for keyboard-input elements).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FocusSource {
Keyboard,
Pointer,
Script,
}

pub enum DocumentEvent {
ResourceLoad(ResourceLoadResponse),
/// A navigation originating from within an iframe's sub-document
Expand Down Expand Up @@ -1617,15 +1627,15 @@ impl BaseDocument {
pub fn focus_next_node(&mut self) -> Option<NodeId> {
let focussed_node_id = self.get_focussed_node_id()?;
let id = self.next_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
self.set_focus_to(id);
self.set_focus_to_with_source(id, FocusSource::Keyboard);
Some(id)
}

/// Move focus to the previous focussable node in the document
pub fn focus_prev_node(&mut self) -> Option<NodeId> {
let focussed_node_id = self.get_focussed_node_id()?;
let id = self.prev_node(&self.nodes[focussed_node_id], |node| node.is_focussable())?;
self.set_focus_to(id);
self.set_focus_to_with_source(id, FocusSource::Keyboard);
Some(id)
}

Expand All @@ -1644,10 +1654,35 @@ impl BaseDocument {
self.mousedown_node_id = node_id.and_then(|id| self.nearest_non_anonymous_ancestor(id));
}
pub fn set_focus_to(&mut self, focus_node_id: NodeId) -> bool {
self.set_focus_to_with_source(focus_node_id, FocusSource::Script)
}

pub fn set_focus_to_with_source(&mut self, focus_node_id: NodeId, source: FocusSource) -> bool {
let Some(focus_node_id) = self.nearest_non_anonymous_ancestor(focus_node_id) else {
return false;
};
let focus_visible = match source {
FocusSource::Keyboard | FocusSource::Script => true,
// Pointer-initiated focus only shows a focus ring on elements which
// support keyboard input (per the :focus-visible heuristics)
FocusSource::Pointer => self.nodes[focus_node_id]
.element_data()
.is_some_and(|el| el.text_input_data().is_some()),
};
if Some(focus_node_id) == self.focus_node_id {
// Update the focus ring visibility to reflect the latest focus source
let has_ring = self.nodes[focus_node_id]
.element_state()
.contains(ElementState::FOCUSRING);
if has_ring != focus_visible {
self.snapshot_node_and(focus_node_id, ElementState::FOCUSRING, |node| {
if let Some(data) = node.element_data_mut() {
data.element_state
.set(ElementState::FOCUSRING, focus_visible);
}
node.mark_ancestors_dirty();
});
}
return false;
}

Expand All @@ -1667,7 +1702,7 @@ impl BaseDocument {
self.snapshot_node_and(
focus_node_id,
ElementState::FOCUS | ElementState::FOCUSRING,
|node| node.focus(shell_provider),
|node| node.focus(shell_provider, focus_visible),
);

self.focus_node_id = Some(focus_node_id);
Expand Down
6 changes: 3 additions & 3 deletions packages/blitz-dom/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use keyboard::{KeyboardOrTextInputEvent, handle_key_or_input_event};
pub(crate) use pointer::DragMode;
use pointer::{handle_click, handle_pointerdown, handle_pointermove, handle_pointerup};

use crate::{BaseDocument, events::pointer::handle_wheel};
use crate::{BaseDocument, document::FocusSource, events::pointer::handle_wheel};

fn adjust_coords_for_subdocument(
coords: &mut PointerCoords,
Expand Down Expand Up @@ -133,7 +133,7 @@ pub(crate) fn handle_dom_event<F: FnMut(DomEvent)>(
generate_focus_events(
doc,
&mut |doc| {
doc.set_focus_to(target_node_id);
doc.set_focus_to_with_source(target_node_id, FocusSource::Pointer);
},
&mut dispatch_event,
);
Expand Down Expand Up @@ -163,7 +163,7 @@ pub(crate) fn handle_dom_event<F: FnMut(DomEvent)>(
generate_focus_events(
doc,
&mut |doc| {
doc.set_focus_to(target_node_id);
doc.set_focus_to_with_source(target_node_id, FocusSource::Pointer);
},
&mut dispatch_event,
);
Expand Down
9 changes: 5 additions & 4 deletions packages/blitz-dom/src/events/pointer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use taffy::AbsoluteAxis;

use crate::{
BaseDocument,
document::FocusSource,
node::{ScrollbarRef, SpecialElementData},
scrolling::{FlingState, ScrollAnimationState},
};
Expand Down Expand Up @@ -544,7 +545,7 @@ pub(crate) fn handle_pointerdown(
generate_focus_events(
doc,
&mut |doc| {
doc.set_focus_to(hit.node_id);
doc.set_focus_to_with_source(hit.node_id, FocusSource::Pointer);
},
dispatch_event,
);
Expand Down Expand Up @@ -658,7 +659,7 @@ pub(crate) fn handle_click(
generate_focus_events(
doc,
&mut |doc| {
doc.set_focus_to(node_id);
doc.set_focus_to_with_source(node_id, FocusSource::Pointer);
},
dispatch_event,
);
Expand Down Expand Up @@ -686,7 +687,7 @@ pub(crate) fn handle_click(
generate_focus_events(
doc,
&mut |doc| {
doc.set_focus_to(node_id);
doc.set_focus_to_with_source(node_id, FocusSource::Pointer);
},
dispatch_event,
);
Expand All @@ -712,7 +713,7 @@ pub(crate) fn handle_click(
generate_focus_events(
doc,
&mut |doc| {
doc.set_focus_to(node_id);
doc.set_focus_to_with_source(node_id, FocusSource::Pointer);
},
dispatch_event,
);
Expand Down
2 changes: 1 addition & 1 deletion packages/blitz-dom/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ pub use crate::node::Widget;

pub use blitz_traits::node_id::NodeId;
pub use config::{DocumentConfig, StyleThreading};
pub use document::{BaseDocument, DocGuard, DocGuardMut, Document, PlainDocument};
pub use document::{BaseDocument, DocGuard, DocGuardMut, Document, FocusSource, PlainDocument};
pub use markup5ever::{
LocalName, Namespace, NamespaceStaticSet, Prefix, PrefixStaticSet, QualName, local_name,
namespace_prefix, namespace_url, ns,
Expand Down
5 changes: 3 additions & 2 deletions packages/blitz-dom/src/node/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,10 +665,11 @@ impl Node {
.is_some_and(|data| data.element_state.contains(ElementState::HOVER))
}

pub fn focus(&mut self, shell_provider: Arc<dyn ShellProvider>) {
pub fn focus(&mut self, shell_provider: Arc<dyn ShellProvider>, focus_visible: bool) {
if let Some(data) = self.element_data_mut() {
data.element_state.insert(ElementState::FOCUS);
data.element_state
.insert(ElementState::FOCUS | ElementState::FOCUSRING);
.set(ElementState::FOCUSRING, focus_visible);
}
self.mark_ancestors_dirty();

Expand Down
4 changes: 3 additions & 1 deletion packages/blitz-dom/src/stylo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,9 @@ impl selectors::Element for BlitzNode<'_> {
NonTSPseudoClass::Enabled => self.element_state().contains(ElementState::ENABLED),
NonTSPseudoClass::Focus => self.element_state().contains(ElementState::FOCUS),
NonTSPseudoClass::FocusWithin => false,
NonTSPseudoClass::FocusVisible => false,
NonTSPseudoClass::FocusVisible => {
self.element_state().contains(ElementState::FOCUSRING)
}
NonTSPseudoClass::Fullscreen => false,
NonTSPseudoClass::Hover => self.element_state().contains(ElementState::HOVER),
NonTSPseudoClass::Indeterminate => false,
Expand Down
Loading