diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 3439610dc..2e21fcc19 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -232,6 +232,7 @@ web-sys = { workspace = true, features = [ "File", "FileList", "FocusEvent", + "FocusOptions", "HtmlCanvasElement", "HtmlElement", "HtmlInputElement", diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 8d3f6c9d7..3caa2428a 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -401,7 +401,7 @@ impl AppRunner { } else { // We are not editing text - give the focus to the canvas. self.text_agent.blur(); - self.canvas().focus().ok(); + super::focus_without_scroll(self.canvas()).ok(); } } diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index ec6bf2a07..f6ff078c5 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -599,7 +599,7 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), // not working when focusing on a text field in an egui app. // This attempts to fix that by forcing the focus on any // click on the canvas. - runner.canvas().focus().ok(); + super::focus_without_scroll(runner.canvas()).ok(); // In Safari we are only allowed to do certain things // (like playing audio, start a download, etc) diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index dc743ec49..67923987b 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -84,6 +84,17 @@ pub(crate) fn has_focus(element: &T) -> bool { try_has_focus(element).unwrap_or(false) } +/// Focus the given element without scrolling it into view. +/// +/// Scrolling the element into view would scroll the whole page when +/// the app is embedded in a larger scrollable page, +/// see . +pub(crate) fn focus_without_scroll(element: &web_sys::HtmlElement) -> Result<(), JsValue> { + let options = web_sys::FocusOptions::new(); + options.set_prevent_scroll(true); + element.focus_with_options(&options) +} + /// Current time in seconds (since undefined point in time). /// /// Monotonically increasing. diff --git a/crates/eframe/src/web/text_agent.rs b/crates/eframe/src/web/text_agent.rs index 60d617d81..73461e9e4 100644 --- a/crates/eframe/src/web/text_agent.rs +++ b/crates/eframe/src/web/text_agent.rs @@ -4,7 +4,7 @@ use std::cell::Cell; use wasm_bindgen::prelude::*; -use web_sys::{Document, Node}; +use web_sys::Document; use super::{AppRunner, WebRunner}; @@ -15,19 +15,23 @@ pub struct TextAgent { impl TextAgent { /// Attach the agent to the document. - pub fn attach(runner_ref: &WebRunner, root: Node) -> Result { + pub fn attach( + runner_ref: &WebRunner, + canvas: &web_sys::HtmlCanvasElement, + ) -> Result { let document = web_sys::window().unwrap().document().unwrap(); // create an `` element let input = document .create_element("input")? - .dyn_into::()?; - input.set_autofocus(true)?; - let input = input.dyn_into::()?; + .dyn_into::()?; input.set_type("text"); input.set_attribute("autocapitalize", "off")?; - // append it to `` and hide it outside of the viewport + // Hide the element, and park it over the canvas + // so that focusing it can never scroll some other part + // of the page into view. + let canvas_rect = super::canvas_content_rect(canvas); let style = input.style(); style.set_property("background-color", "transparent")?; style.set_property("border", "none")?; @@ -36,11 +40,12 @@ impl TextAgent { style.set_property("height", "1px")?; style.set_property("caret-color", "transparent")?; style.set_property("position", "absolute")?; - style.set_property("top", "0")?; - style.set_property("left", "0")?; + style.set_property("top", &format!("{}px", canvas_rect.min.y))?; + style.set_property("left", &format!("{}px", canvas_rect.min.x))?; // Prevent auto-zoom on mobile browsers (requires at least 16px). style.set_property("font-size", "16px")?; + let root = canvas.get_root_node(); if root.has_type::() { // root object is a document, append to its body root.dyn_into::()? @@ -52,6 +57,13 @@ impl TextAgent { root.append_child(&input)?; } + // Focus the app on startup, without scrolling the page. + // We do this instead of setting the `autofocus` attribute, + // since the browser scrolls the focused element into view when + // honoring `autofocus`, and there is no way to prevent that. + // See https://github.com/emilk/egui/issues/8295 + super::focus_without_scroll(&input).ok(); + // attach event listeners let on_input = { @@ -67,7 +79,7 @@ impl TextAgent { // between versions 14.7.09 and 17.0.12. if !event.is_composing() { input.blur().ok(); - input.focus().ok(); + super::focus_without_scroll(&input).ok(); } if event.is_composing() { @@ -221,7 +233,7 @@ impl TextAgent { log::trace!("Focusing text agent"); - if let Err(err) = self.input.focus() { + if let Err(err) = super::focus_without_scroll(&self.input) { log::error!("failed to set focus: {}", super::string_from_js_value(&err)); } } diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs index 99615c97f..2bf842ab0 100644 --- a/crates/eframe/src/web/web_runner.rs +++ b/crates/eframe/src/web/web_runner.rs @@ -73,7 +73,7 @@ impl WebRunner { { // First set up the app runner: - let text_agent = TextAgent::attach(self, canvas.get_root_node())?; + let text_agent = TextAgent::attach(self, &canvas)?; let app_runner = AppRunner::new(canvas.clone(), web_options, app_creator, text_agent).await?; self.app_runner.replace(Some(app_runner));