mirror of
https://github.com/emilk/egui.git
synced 2026-07-11 09:08:53 +00:00
Web: don't scroll host page when text agent or canvas grabs focus (#8296)
* Closes <https://github.com/emilk/egui/issues/8295> * [x] I have followed the instructions in the PR template When an eframe app is embedded in a scrollable host page, the host page jumped to the top whenever the app booted or grabbed keyboard focus. Cause: the hidden text-agent `<input>` sat at (0,0) of `<body>` with the `autofocus` attribute, and all focus calls (text agent and canvas) used plain `focus()`, so the browser scrolled the focused element into view — i.e. to the top of the page. Changes: * All focus calls in `eframe` web (text agent, canvas, Gboard blur/refocus workaround) now go through a new `focus_without_scroll()` helper that uses `focus_with_options` with `preventScroll: true` (supported by all evergreen browsers). * The `autofocus` attribute is replaced with an explicit focus call after DOM insertion — the browser-internal autofocus path always scrolls the element into view and cannot be prevented by any focus option. Boot-focus behavior is preserved. * The text-agent input is initially parked at the canvas origin instead of (0,0) of the page, so any residual scroll-into-view would target the canvas rather than the top of the host page. (`move_to` keeps repositioning it for IME as before.) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -232,6 +232,7 @@ web-sys = { workspace = true, features = [
|
||||
"File",
|
||||
"FileList",
|
||||
"FocusEvent",
|
||||
"FocusOptions",
|
||||
"HtmlCanvasElement",
|
||||
"HtmlElement",
|
||||
"HtmlInputElement",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user