From c9849e93932691e4aa013e19d722360e8da0e867 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 6 Jul 2026 18:17:19 -0700 Subject: [PATCH] Web: don't scroll host page when text agent or canvas grabs focus Focus the text agent and canvas with `preventScroll: true`, replace the `autofocus` attribute with an explicit focus call (the autofocus insertion path always scrolls the element into view and cannot be prevented), and park the hidden text agent input at the canvas origin instead of (0,0) of the page. Fixes https://github.com/emilk/egui/issues/8295 Co-Authored-By: Claude Fable 5 --- crates/eframe/Cargo.toml | 1 + crates/eframe/src/web/app_runner.rs | 2 +- crates/eframe/src/web/events.rs | 2 +- crates/eframe/src/web/mod.rs | 11 ++++++++++ crates/eframe/src/web/text_agent.rs | 32 ++++++++++++++++++++--------- crates/eframe/src/web/web_runner.rs | 2 +- 6 files changed, 37 insertions(+), 13 deletions(-) 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));