mirror of
https://github.com/emilk/egui.git
synced 2026-07-14 18:48:54 +00:00
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:
@@ -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