Add pointer events and focus handling for apps run in a Shadow DOM (#5627)
* [x] I have followed the instructions in the PR template This PR handles pointer events and focus which did following changes: - `element_from_point` and focus is now acquired from root node object by using `get_root_node` from document or a shadow root. - `TextAgent` is appended individually in each shadow root. These changes handles pointer events and focus well in a web app that are running in a shadow dom, or else the hover pointer actions and keyboard input events are not triggered in a shadow dom. Helpful for building embeddable/multi-view web-apps.
This commit is contained in:
parent
071e090e2b
commit
43261a5396
|
|
@ -253,6 +253,7 @@ web-sys = { workspace = true, features = [
|
||||||
"ResizeObserverEntry",
|
"ResizeObserverEntry",
|
||||||
"ResizeObserverOptions",
|
"ResizeObserverOptions",
|
||||||
"ResizeObserverSize",
|
"ResizeObserverSize",
|
||||||
|
"ShadowRoot",
|
||||||
"Storage",
|
"Storage",
|
||||||
"Touch",
|
"Touch",
|
||||||
"TouchEvent",
|
"TouchEvent",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
use web_sys::EventTarget;
|
|
||||||
|
|
||||||
use crate::web::string_from_js_value;
|
use crate::web::string_from_js_value;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
|
|
@ -10,6 +8,8 @@ use super::{
|
||||||
DEBUG_RESIZE,
|
DEBUG_RESIZE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use web_sys::{Document, EventTarget, ShadowRoot};
|
||||||
|
|
||||||
// TODO(emilk): there are more calls to `prevent_default` and `stop_propagation`
|
// TODO(emilk): there are more calls to `prevent_default` and `stop_propagation`
|
||||||
// than what is probably needed.
|
// than what is probably needed.
|
||||||
|
|
||||||
|
|
@ -570,10 +570,17 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
|
||||||
/// Returns true if the cursor is above the canvas, or if we're dragging something.
|
/// Returns true if the cursor is above the canvas, or if we're dragging something.
|
||||||
/// Pass in the position in browser viewport coordinates (usually event.clientX/Y).
|
/// Pass in the position in browser viewport coordinates (usually event.clientX/Y).
|
||||||
fn is_interested_in_pointer_event(runner: &AppRunner, pos: egui::Pos2) -> bool {
|
fn is_interested_in_pointer_event(runner: &AppRunner, pos: egui::Pos2) -> bool {
|
||||||
let document = web_sys::window().unwrap().document().unwrap();
|
let root_node = runner.canvas().get_root_node();
|
||||||
let is_hovering_canvas = document
|
|
||||||
.element_from_point(pos.x, pos.y)
|
let element_at_point = if let Some(document) = root_node.dyn_ref::<Document>() {
|
||||||
.is_some_and(|element| element.eq(runner.canvas()));
|
document.element_from_point(pos.x, pos.y)
|
||||||
|
} else if let Some(shadow) = root_node.dyn_ref::<ShadowRoot>() {
|
||||||
|
shadow.element_from_point(pos.x, pos.y)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_hovering_canvas = element_at_point.is_some_and(|element| element.eq(runner.canvas()));
|
||||||
let is_pointer_down = runner
|
let is_pointer_down = runner
|
||||||
.egui_ctx()
|
.egui_ctx()
|
||||||
.input(|i| i.pointer.any_down() || i.any_touches());
|
.input(|i| i.pointer.any_down() || i.any_touches());
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu;
|
||||||
pub use backend::*;
|
pub use backend::*;
|
||||||
|
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use web_sys::MediaQueryList;
|
use web_sys::{Document, MediaQueryList, Node};
|
||||||
|
|
||||||
use input::{
|
use input::{
|
||||||
button_from_mouse_event, modifiers_from_kb_event, modifiers_from_mouse_event,
|
button_from_mouse_event, modifiers_from_kb_event, modifiers_from_mouse_event,
|
||||||
|
|
@ -64,18 +64,22 @@ pub(crate) fn string_from_js_value(value: &JsValue) -> String {
|
||||||
/// - `<a>`/`<area>` with an `href` attribute
|
/// - `<a>`/`<area>` with an `href` attribute
|
||||||
/// - `<input>`/`<select>`/`<textarea>`/`<button>` which aren't `disabled`
|
/// - `<input>`/`<select>`/`<textarea>`/`<button>` which aren't `disabled`
|
||||||
/// - any other element with a `tabindex` attribute
|
/// - any other element with a `tabindex` attribute
|
||||||
pub(crate) fn focused_element() -> Option<web_sys::Element> {
|
pub(crate) fn focused_element(root: &Node) -> Option<web_sys::Element> {
|
||||||
web_sys::window()?
|
if let Some(document) = root.dyn_ref::<Document>() {
|
||||||
.document()?
|
document.active_element()
|
||||||
.active_element()?
|
} else if let Some(shadow) = root.dyn_ref::<web_sys::ShadowRoot>() {
|
||||||
.dyn_into()
|
shadow.active_element()
|
||||||
.ok()
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn has_focus<T: JsCast>(element: &T) -> bool {
|
pub(crate) fn has_focus<T: JsCast>(element: &T) -> bool {
|
||||||
fn try_has_focus<T: JsCast>(element: &T) -> Option<bool> {
|
fn try_has_focus<T: JsCast>(element: &T) -> Option<bool> {
|
||||||
let element = element.dyn_ref::<web_sys::Element>()?;
|
let element = element.dyn_ref::<web_sys::Element>()?;
|
||||||
let focused_element = focused_element()?;
|
let root = element.get_root_node();
|
||||||
|
|
||||||
|
let focused_element = focused_element(&root)?;
|
||||||
Some(element == &focused_element)
|
Some(element == &focused_element)
|
||||||
}
|
}
|
||||||
try_has_focus(element).unwrap_or(false)
|
try_has_focus(element).unwrap_or(false)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
|
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
use web_sys::{Document, Node};
|
||||||
|
|
||||||
use super::{AppRunner, WebRunner};
|
use super::{AppRunner, WebRunner};
|
||||||
|
|
||||||
|
|
@ -14,7 +15,7 @@ pub struct TextAgent {
|
||||||
|
|
||||||
impl TextAgent {
|
impl TextAgent {
|
||||||
/// Attach the agent to the document.
|
/// Attach the agent to the document.
|
||||||
pub fn attach(runner_ref: &WebRunner) -> Result<Self, JsValue> {
|
pub fn attach(runner_ref: &WebRunner, root: Node) -> Result<Self, JsValue> {
|
||||||
let document = web_sys::window().unwrap().document().unwrap();
|
let document = web_sys::window().unwrap().document().unwrap();
|
||||||
|
|
||||||
// create an `<input>` element
|
// create an `<input>` element
|
||||||
|
|
@ -37,7 +38,17 @@ impl TextAgent {
|
||||||
style.set_property("position", "absolute")?;
|
style.set_property("position", "absolute")?;
|
||||||
style.set_property("top", "0")?;
|
style.set_property("top", "0")?;
|
||||||
style.set_property("left", "0")?;
|
style.set_property("left", "0")?;
|
||||||
document.body().unwrap().append_child(&input)?;
|
|
||||||
|
if root.has_type::<Document>() {
|
||||||
|
// root object is a document, append to its body
|
||||||
|
root.dyn_into::<Document>()?
|
||||||
|
.body()
|
||||||
|
.unwrap()
|
||||||
|
.append_child(&input)?;
|
||||||
|
} else {
|
||||||
|
// append input into root directly
|
||||||
|
root.append_child(&input)?;
|
||||||
|
}
|
||||||
|
|
||||||
// attach event listeners
|
// attach event listeners
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ impl WebRunner {
|
||||||
|
|
||||||
{
|
{
|
||||||
// First set up the app runner:
|
// First set up the app runner:
|
||||||
let text_agent = TextAgent::attach(self)?;
|
let text_agent = TextAgent::attach(self, canvas.get_root_node())?;
|
||||||
let app_runner =
|
let app_runner =
|
||||||
AppRunner::new(canvas.clone(), web_options, app_creator, text_agent).await?;
|
AppRunner::new(canvas.clone(), web_options, app_creator, text_agent).await?;
|
||||||
self.app_runner.replace(Some(app_runner));
|
self.app_runner.replace(Some(app_runner));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue