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 <noreply@anthropic.com>
This commit is contained in:
Emil Ernerfeldt
2026-07-06 18:17:19 -07:00
parent f005960bdc
commit c9849e9393
6 changed files with 37 additions and 13 deletions
+1
View File
@@ -232,6 +232,7 @@ web-sys = { workspace = true, features = [
"File",
"FileList",
"FocusEvent",
"FocusOptions",
"HtmlCanvasElement",
"HtmlElement",
"HtmlInputElement",
+1 -1
View File
@@ -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();
}
}
+1 -1
View File
@@ -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)
+11
View File
@@ -84,6 +84,17 @@ pub(crate) fn has_focus<T: JsCast>(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 <https://github.com/emilk/egui/issues/8295>.
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.
+22 -10
View File
@@ -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<Self, JsValue> {
pub fn attach(
runner_ref: &WebRunner,
canvas: &web_sys::HtmlCanvasElement,
) -> Result<Self, JsValue> {
let document = web_sys::window().unwrap().document().unwrap();
// create an `<input>` element
let input = document
.create_element("input")?
.dyn_into::<web_sys::HtmlElement>()?;
input.set_autofocus(true)?;
let input = input.dyn_into::<web_sys::HtmlInputElement>()?;
.dyn_into::<web_sys::HtmlInputElement>()?;
input.set_type("text");
input.set_attribute("autocapitalize", "off")?;
// append it to `<body>` 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::<Document>() {
// root object is a document, append to its body
root.dyn_into::<Document>()?
@@ -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));
}
}
+1 -1
View File
@@ -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));