script: Chain up keyboard scrolling to parent <iframe>s (#39469)

When an `<iframe>` cannot scroll because the size of the frame is
greater than or
equal to the size of page contents, chain up the keyboard scroll
operation to the parent frame.

Testing: A new Servo-only WPT tests is added, though needs to be
manually
run with `--product servodriver`.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Delan Azabani <dazabani@igalia.com>
This commit is contained in:
Martin Robinson 2025-09-25 13:16:41 +02:00 committed by GitHub
parent 75e32ba5a4
commit ffdb7d3663
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 406 additions and 132 deletions

1
Cargo.lock generated
View file

@ -1624,6 +1624,7 @@ dependencies = [
"http 1.3.1", "http 1.3.1",
"hyper_serde", "hyper_serde",
"ipc-channel", "ipc-channel",
"keyboard-types",
"log", "log",
"malloc_size_of_derive", "malloc_size_of_derive",
"net_traits", "net_traits",

View file

@ -1985,6 +1985,20 @@ where
warn!("No webdriver_input_command_reponse_sender"); warn!("No webdriver_input_command_reponse_sender");
} }
}, },
ScriptToConstellationMessage::ForwardKeyboardScroll(pipeline_id, scroll) => {
if let Some(pipeline) = self.pipelines.get(&pipeline_id) {
if let Err(error) =
pipeline
.event_loop
.send(ScriptThreadMessage::ForwardKeyboardScroll(
pipeline_id,
scroll,
))
{
warn!("Could not forward {scroll:?} to {pipeline_id}: {error:?}");
}
}
},
} }
} }

View file

@ -182,6 +182,7 @@ mod from_script {
Self::ReportMemory(..) => target!("ReportMemory"), Self::ReportMemory(..) => target!("ReportMemory"),
Self::WebDriverInputComplete(..) => target!("WebDriverInputComplete"), Self::WebDriverInputComplete(..) => target!("WebDriverInputComplete"),
Self::FinishJavaScriptEvaluation(..) => target!("FinishJavaScriptEvaluation"), Self::FinishJavaScriptEvaluation(..) => target!("FinishJavaScriptEvaluation"),
Self::ForwardKeyboardScroll(..) => target!("ForwardKeyboardScroll"),
} }
} }
} }

View file

@ -42,7 +42,7 @@ pub struct BoxTree {
root: BlockFormattingContext, root: BlockFormattingContext,
/// Whether or not the viewport should be sensitive to scrolling input events in two axes /// Whether or not the viewport should be sensitive to scrolling input events in two axes
viewport_scroll_sensitivity: AxesScrollSensitivity, pub(crate) viewport_overflow: AxesOverflow,
} }
impl BoxTree { impl BoxTree {
@ -65,10 +65,7 @@ impl BoxTree {
// From https://www.w3.org/TR/css-overflow-3/#overflow-propagation: // From https://www.w3.org/TR/css-overflow-3/#overflow-propagation:
// > If visible is applied to the viewport, it must be interpreted as auto. // > If visible is applied to the viewport, it must be interpreted as auto.
// > If clip is applied to the viewport, it must be interpreted as hidden. // > If clip is applied to the viewport, it must be interpreted as hidden.
viewport_scroll_sensitivity: AxesScrollSensitivity { viewport_overflow: viewport_overflow.to_scrollable(),
x: viewport_overflow.x.to_scrollable().into(),
y: viewport_overflow.y.to_scrollable().into(),
},
} }
} }
@ -266,11 +263,16 @@ impl BoxTree {
&mut root_fragments, &mut root_fragments,
); );
let viewport_scroll_sensitivity = AxesScrollSensitivity {
x: self.viewport_overflow.x.into(),
y: self.viewport_overflow.y.into(),
};
FragmentTree::new( FragmentTree::new(
layout_context, layout_context,
root_fragments, root_fragments,
physical_containing_block, physical_containing_block,
self.viewport_scroll_sensitivity, viewport_scroll_sensitivity,
) )
} }
} }

View file

@ -333,11 +333,17 @@ impl Layout for LayoutThread {
#[servo_tracing::instrument(skip_all)] #[servo_tracing::instrument(skip_all)]
fn query_scroll_container( fn query_scroll_container(
&self, &self,
node: TrustedNodeAddress, node: Option<TrustedNodeAddress>,
flags: ScrollContainerQueryFlags, flags: ScrollContainerQueryFlags,
) -> Option<ScrollContainerResponse> { ) -> Option<ScrollContainerResponse> {
let node = unsafe { ServoLayoutNode::new(&node) }; let node = unsafe { node.as_ref().map(|node| ServoLayoutNode::new(node)) };
process_scroll_container_query(node, flags) let viewport_overflow = self
.box_tree
.borrow()
.as_ref()
.expect("Should have a BoxTree for all scroll container queries.")
.viewport_overflow;
process_scroll_container_query(node, flags, viewport_overflow)
} }
#[servo_tracing::instrument(skip_all)] #[servo_tracing::instrument(skip_all)]

View file

@ -12,7 +12,7 @@ use euclid::{SideOffsets2D, Size2D};
use itertools::Itertools; use itertools::Itertools;
use layout_api::wrapper_traits::{LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode}; use layout_api::wrapper_traits::{LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode};
use layout_api::{ use layout_api::{
BoxAreaType, LayoutElementType, LayoutNodeType, OffsetParentResponse, AxesOverflow, BoxAreaType, LayoutElementType, LayoutNodeType, OffsetParentResponse,
ScrollContainerQueryFlags, ScrollContainerResponse, ScrollContainerQueryFlags, ScrollContainerResponse,
}; };
use script::layout_dom::{ServoLayoutNode, ServoThreadSafeLayoutNode}; use script::layout_dom::{ServoLayoutNode, ServoThreadSafeLayoutNode};
@ -676,9 +676,14 @@ pub fn process_offset_parent_query(
/// ///
#[inline] #[inline]
pub(crate) fn process_scroll_container_query( pub(crate) fn process_scroll_container_query(
node: ServoLayoutNode<'_>, node: Option<ServoLayoutNode<'_>>,
query_flags: ScrollContainerQueryFlags, query_flags: ScrollContainerQueryFlags,
viewport_overflow: AxesOverflow,
) -> Option<ScrollContainerResponse> { ) -> Option<ScrollContainerResponse> {
let Some(node) = node else {
return Some(ScrollContainerResponse::Viewport(viewport_overflow));
};
let layout_data = node.to_threadsafe().inner_layout_data()?; let layout_data = node.to_threadsafe().inner_layout_data()?;
// 1. If any of the following holds true, return null and terminate this algorithm: // 1. If any of the following holds true, return null and terminate this algorithm:
@ -776,7 +781,7 @@ pub(crate) fn process_scroll_container_query(
match current_position_value { match current_position_value {
Position::Fixed => None, Position::Fixed => None,
_ => Some(ScrollContainerResponse::Viewport), _ => Some(ScrollContainerResponse::Viewport(viewport_overflow)),
} }
} }

View file

@ -933,6 +933,7 @@ malloc_size_of_is_stylo_malloc_size_of!(style::values::computed::FontWeight);
malloc_size_of_is_stylo_malloc_size_of!(style::values::computed::font::SingleFontFamily); malloc_size_of_is_stylo_malloc_size_of!(style::values::computed::font::SingleFontFamily);
malloc_size_of_is_stylo_malloc_size_of!(style::values::computed::JustifyContent); malloc_size_of_is_stylo_malloc_size_of!(style::values::computed::JustifyContent);
malloc_size_of_is_stylo_malloc_size_of!(style::values::specified::align::AlignFlags); malloc_size_of_is_stylo_malloc_size_of!(style::values::specified::align::AlignFlags);
malloc_size_of_is_stylo_malloc_size_of!(style::values::specified::box_::Overflow);
malloc_size_of_is_stylo_malloc_size_of!(style::values::specified::TextDecorationLine); malloc_size_of_is_stylo_malloc_size_of!(style::values::specified::TextDecorationLine);
malloc_size_of_is_stylo_malloc_size_of!(stylo_dom::ElementState); malloc_size_of_is_stylo_malloc_size_of!(stylo_dom::ElementState);

View file

@ -33,7 +33,10 @@ use euclid::default::{Rect, Size2D};
use html5ever::{LocalName, Namespace, QualName, local_name, ns}; use html5ever::{LocalName, Namespace, QualName, local_name, ns};
use hyper_serde::Serde; use hyper_serde::Serde;
use js::rust::{HandleObject, HandleValue, MutableHandleValue}; use js::rust::{HandleObject, HandleValue, MutableHandleValue};
use layout_api::{PendingRestyle, ReflowGoal, ReflowPhasesRun, RestyleReason, TrustedNodeAddress}; use layout_api::{
PendingRestyle, ReflowGoal, ReflowPhasesRun, RestyleReason, ScrollContainerQueryFlags,
TrustedNodeAddress,
};
use metrics::{InteractiveFlag, InteractiveWindow, ProgressiveWebMetrics}; use metrics::{InteractiveFlag, InteractiveWindow, ProgressiveWebMetrics};
use net_traits::CookieSource::NonHTTP; use net_traits::CookieSource::NonHTTP;
use net_traits::CoreResourceMsg::{GetCookiesForUrl, SetCookiesForUrl}; use net_traits::CoreResourceMsg::{GetCookiesForUrl, SetCookiesForUrl};
@ -173,7 +176,7 @@ use crate::dom::processinginstruction::ProcessingInstruction;
use crate::dom::promise::Promise; use crate::dom::promise::Promise;
use crate::dom::range::Range; use crate::dom::range::Range;
use crate::dom::resizeobserver::{ResizeObservationDepth, ResizeObserver}; use crate::dom::resizeobserver::{ResizeObservationDepth, ResizeObserver};
use crate::dom::scrolling_box::{ScrollingBox, ScrollingBoxSource}; use crate::dom::scrolling_box::ScrollingBox;
use crate::dom::selection::Selection; use crate::dom::selection::Selection;
use crate::dom::servoparser::ServoParser; use crate::dom::servoparser::ServoParser;
use crate::dom::shadowroot::ShadowRoot; use crate::dom::shadowroot::ShadowRoot;
@ -4447,8 +4450,10 @@ impl Document {
self.active_sandboxing_flag_set.set(flags) self.active_sandboxing_flag_set.set(flags)
} }
pub(crate) fn viewport_scrolling_box(&self) -> ScrollingBox { pub(crate) fn viewport_scrolling_box(&self, flags: ScrollContainerQueryFlags) -> ScrollingBox {
ScrollingBox::new(ScrollingBoxSource::Viewport(DomRoot::from_ref(self))) self.window()
.scrolling_box_query(None, flags)
.expect("We should always have a ScrollingBox for the Viewport")
} }
} }

View file

@ -10,7 +10,7 @@ use std::rc::Rc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use base::generic_channel; use base::generic_channel;
use constellation_traits::ScriptToConstellationMessage; use constellation_traits::{KeyboardScroll, ScriptToConstellationMessage};
use embedder_traits::{ use embedder_traits::{
Cursor, EditingActionEvent, EmbedderMsg, GamepadEvent as EmbedderGamepadEvent, Cursor, EditingActionEvent, EmbedderMsg, GamepadEvent as EmbedderGamepadEvent,
GamepadSupportedHapticEffects, GamepadUpdateType, ImeEvent, InputEvent, GamepadSupportedHapticEffects, GamepadUpdateType, ImeEvent, InputEvent,
@ -20,6 +20,7 @@ use embedder_traits::{
}; };
use euclid::{Point2D, Vector2D}; use euclid::{Point2D, Vector2D};
use ipc_channel::ipc; use ipc_channel::ipc;
use js::jsapi::JSAutoRealm;
use keyboard_types::{Code, Key, KeyState, Modifiers, NamedKey}; use keyboard_types::{Code, Key, KeyState, Modifiers, NamedKey};
use layout_api::{ScrollContainerQueryFlags, node_id_from_scroll_id}; use layout_api::{ScrollContainerQueryFlags, node_id_from_scroll_id};
use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods; use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods;
@ -31,6 +32,7 @@ use script_bindings::codegen::GenericBindings::TouchBinding::TouchMethods;
use script_bindings::codegen::GenericBindings::WindowBinding::{ScrollBehavior, WindowMethods}; use script_bindings::codegen::GenericBindings::WindowBinding::{ScrollBehavior, WindowMethods};
use script_bindings::inheritance::Castable; use script_bindings::inheritance::Castable;
use script_bindings::num::Finite; use script_bindings::num::Finite;
use script_bindings::reflector::DomObject;
use script_bindings::root::{Dom, DomRoot, DomSlice}; use script_bindings::root::{Dom, DomRoot, DomSlice};
use script_bindings::script_runtime::CanGc; use script_bindings::script_runtime::CanGc;
use script_bindings::str::DOMString; use script_bindings::str::DOMString;
@ -48,7 +50,7 @@ use crate::dom::event::{EventBubbles, EventCancelable, EventComposed, EventDefau
use crate::dom::gamepad::gamepad::{Gamepad, contains_user_gesture}; use crate::dom::gamepad::gamepad::{Gamepad, contains_user_gesture};
use crate::dom::gamepad::gamepadevent::GamepadEventType; use crate::dom::gamepad::gamepadevent::GamepadEventType;
use crate::dom::inputevent::HitTestResult; use crate::dom::inputevent::HitTestResult;
use crate::dom::node::{self, Node, ShadowIncluding}; use crate::dom::node::{self, Node, NodeTraits, ShadowIncluding};
use crate::dom::pointerevent::PointerId; use crate::dom::pointerevent::PointerId;
use crate::dom::scrolling_box::ScrollingBoxAxis; use crate::dom::scrolling_box::ScrollingBoxAxis;
use crate::dom::types::{ use crate::dom::types::{
@ -1486,60 +1488,94 @@ impl DocumentEventHandler {
if !event.modifiers().is_empty() { if !event.modifiers().is_empty() {
return; return;
} }
let scroll = match event.key() {
let scroll_axis = match event.key() { Key::Named(NamedKey::ArrowDown) => KeyboardScroll::Down,
Key::Named( Key::Named(NamedKey::ArrowLeft) => KeyboardScroll::Left,
NamedKey::Home | Key::Named(NamedKey::ArrowRight) => KeyboardScroll::Right,
NamedKey::End | Key::Named(NamedKey::ArrowUp) => KeyboardScroll::Up,
NamedKey::PageDown | Key::Named(NamedKey::End) => KeyboardScroll::End,
NamedKey::PageUp | Key::Named(NamedKey::Home) => KeyboardScroll::Home,
NamedKey::ArrowUp | Key::Named(NamedKey::PageDown) => KeyboardScroll::PageDown,
NamedKey::ArrowDown, Key::Named(NamedKey::PageUp) => KeyboardScroll::PageUp,
) => ScrollingBoxAxis::Y,
Key::Named(NamedKey::ArrowLeft | NamedKey::ArrowRight) => ScrollingBoxAxis::X,
_ => return, _ => return,
}; };
self.do_keyboard_scroll(scroll);
}
pub(crate) fn do_keyboard_scroll(&self, scroll: KeyboardScroll) {
let scroll_axis = match scroll {
KeyboardScroll::Left | KeyboardScroll::Right => ScrollingBoxAxis::X,
_ => ScrollingBoxAxis::Y,
};
let document = self.window.Document(); let document = self.window.Document();
let mut scrolling_box = document let mut scrolling_box = document
.get_focused_element() .get_focused_element()
.or(self.most_recently_clicked_element.get()) .or(self.most_recently_clicked_element.get())
.and_then(|element| element.scrolling_box(ScrollContainerQueryFlags::Inclusive)) .and_then(|element| element.scrolling_box(ScrollContainerQueryFlags::Inclusive))
.unwrap_or_else(|| document.viewport_scrolling_box()); .unwrap_or_else(|| {
document.viewport_scrolling_box(ScrollContainerQueryFlags::Inclusive)
});
while !scrolling_box.can_keyboard_scroll_in_axis(scroll_axis) { while !scrolling_box.can_keyboard_scroll_in_axis(scroll_axis) {
// Always fall back to trying to scroll the entire document. // Always fall back to trying to scroll the entire document.
if scrolling_box.is_viewport() { if scrolling_box.is_viewport() {
break; break;
} }
let parent = scrolling_box let parent = scrolling_box.parent().unwrap_or_else(|| {
.parent() document.viewport_scrolling_box(ScrollContainerQueryFlags::Inclusive)
.unwrap_or_else(|| document.viewport_scrolling_box()); });
scrolling_box = parent; scrolling_box = parent;
} }
// If this is the viewport and we cannot scroll, try to ask a parent viewport to scroll,
// if we are inside an `<iframe>`.
if !scrolling_box.can_keyboard_scroll_in_axis(scroll_axis) {
assert!(scrolling_box.is_viewport());
let window_proxy = document.window().window_proxy();
if let Some(iframe) = window_proxy.frame_element() {
// When the `<iframe>` is local (in this ScriptThread), we can
// synchronously chain up the keyboard scrolling event.
let cx = GlobalScope::get_cx();
let iframe_window = iframe.owner_window();
let _ac = JSAutoRealm::new(*cx, iframe_window.reflector().get_jsobject().get());
iframe_window
.Document()
.event_handler()
.do_keyboard_scroll(scroll);
} else if let Some(parent_pipeline) = self.window.parent_info() {
// Otherwise, if we have a parent (presumably from a different origin)
// asynchronously ask the Constellation to forward the event to the parent
// pipeline, if we have one.
document.window().send_to_constellation(
ScriptToConstellationMessage::ForwardKeyboardScroll(parent_pipeline, scroll),
);
};
return;
}
const LINE_HEIGHT: f32 = 76.0; const LINE_HEIGHT: f32 = 76.0;
const LINE_WIDTH: f32 = 76.0; const LINE_WIDTH: f32 = 76.0;
let current_scroll_offset = scrolling_box.scroll_position(); let current_scroll_offset = scrolling_box.scroll_position();
let delta = match event.key() { let delta = match scroll {
Key::Named(NamedKey::Home) => Vector2D::new(0.0, -current_scroll_offset.y), KeyboardScroll::Home => Vector2D::new(0.0, -current_scroll_offset.y),
Key::Named(NamedKey::End) => Vector2D::new( KeyboardScroll::End => Vector2D::new(
0.0, 0.0,
-current_scroll_offset.y + scrolling_box.content_size().height - -current_scroll_offset.y + scrolling_box.content_size().height -
scrolling_box.size().height, scrolling_box.size().height,
), ),
Key::Named(NamedKey::PageDown) => { KeyboardScroll::PageDown => {
Vector2D::new(0.0, scrolling_box.size().height - 2.0 * LINE_HEIGHT) Vector2D::new(0.0, scrolling_box.size().height - 2.0 * LINE_HEIGHT)
}, },
Key::Named(NamedKey::PageUp) => { KeyboardScroll::PageUp => {
Vector2D::new(0.0, 2.0 * LINE_HEIGHT - scrolling_box.size().height) Vector2D::new(0.0, 2.0 * LINE_HEIGHT - scrolling_box.size().height)
}, },
Key::Named(NamedKey::ArrowUp) => Vector2D::new(0.0, -LINE_HEIGHT), KeyboardScroll::Up => Vector2D::new(0.0, -LINE_HEIGHT),
Key::Named(NamedKey::ArrowDown) => Vector2D::new(0.0, LINE_HEIGHT), KeyboardScroll::Down => Vector2D::new(0.0, LINE_HEIGHT),
Key::Named(NamedKey::ArrowLeft) => Vector2D::new(-LINE_WIDTH, 0.0), KeyboardScroll::Left => Vector2D::new(-LINE_WIDTH, 0.0),
Key::Named(NamedKey::ArrowRight) => Vector2D::new(LINE_WIDTH, 0.0), KeyboardScroll::Right => Vector2D::new(LINE_WIDTH, 0.0),
_ => return,
}; };
scrolling_box.scroll_to(delta + current_scroll_offset, ScrollBehavior::Auto); scrolling_box.scroll_to(delta + current_scroll_offset, ScrollBehavior::Auto);

View file

@ -25,7 +25,7 @@ use html5ever::{LocalName, Namespace, Prefix, QualName, local_name, namespace_pr
use js::jsapi::Heap; use js::jsapi::Heap;
use js::jsval::JSVal; use js::jsval::JSVal;
use js::rust::HandleObject; use js::rust::HandleObject;
use layout_api::{LayoutDamage, ScrollContainerQueryFlags, ScrollContainerResponse}; use layout_api::{LayoutDamage, ScrollContainerQueryFlags};
use net_traits::ReferrerPolicy; use net_traits::ReferrerPolicy;
use net_traits::request::CorsSettings; use net_traits::request::CorsSettings;
use selectors::Element as SelectorsElement; use selectors::Element as SelectorsElement;
@ -161,7 +161,7 @@ use crate::dom::mutationobserver::{Mutation, MutationObserver};
use crate::dom::namednodemap::NamedNodeMap; use crate::dom::namednodemap::NamedNodeMap;
use crate::dom::node::{ use crate::dom::node::{
BindContext, ChildrenMutation, CloneChildrenFlag, LayoutNodeHelpers, Node, NodeDamage, BindContext, ChildrenMutation, CloneChildrenFlag, LayoutNodeHelpers, Node, NodeDamage,
NodeFlags, NodeTraits, ShadowIncluding, UnbindContext, from_untrusted_node_address, NodeFlags, NodeTraits, ShadowIncluding, UnbindContext,
}; };
use crate::dom::nodelist::NodeList; use crate::dom::nodelist::NodeList;
use crate::dom::promise::Promise; use crate::dom::promise::Promise;
@ -822,22 +822,11 @@ impl Element {
.retain(|reg_obs| *reg_obs.observer != *observer) .retain(|reg_obs| *reg_obs.observer != *observer)
} }
#[allow(unsafe_code)] /// Get the [`ScrollingBox`] that contains this element, if one does. `position:
/// fixed` elements do not have a containing [`ScrollingBox`].
pub(crate) fn scrolling_box(&self, flags: ScrollContainerQueryFlags) -> Option<ScrollingBox> { pub(crate) fn scrolling_box(&self, flags: ScrollContainerQueryFlags) -> Option<ScrollingBox> {
self.owner_window() self.owner_window()
.scroll_container_query(self.upcast(), flags) .scrolling_box_query(Some(self.upcast()), flags)
.and_then(|response| match response {
ScrollContainerResponse::Viewport => Some(ScrollingBox::new(
ScrollingBoxSource::Viewport(self.owner_document()),
)),
ScrollContainerResponse::Element(parent_node_address, axes_overflow) => {
let node = unsafe { from_untrusted_node_address(parent_node_address) };
Some(ScrollingBox::new(ScrollingBoxSource::Element(
DomRoot::downcast(node)?,
axes_overflow,
)))
},
})
} }
/// <https://drafts.csswg.org/cssom-view/#scroll-a-target-into-view> /// <https://drafts.csswg.org/cssom-view/#scroll-a-target-into-view>
@ -927,7 +916,7 @@ impl Element {
let (adjusted_element_top_left, adjusted_element_bottom_right) = let (adjusted_element_top_left, adjusted_element_bottom_right) =
match scrolling_box.target() { match scrolling_box.target() {
ScrollingBoxSource::Viewport(_) => (target_top_left, target_bottom_right), ScrollingBoxSource::Viewport(_) => (target_top_left, target_bottom_right),
ScrollingBoxSource::Element(scrolling_element, _) => { ScrollingBoxSource::Element(scrolling_element) => {
let scrolling_padding_rect_top_left = scrolling_element let scrolling_padding_rect_top_left = scrolling_element
.upcast::<Node>() .upcast::<Node>()
.padding_box() .padding_box()

View file

@ -450,9 +450,12 @@ impl HTMLElementMethods<crate::DomTypeHolder> for HTMLElement {
#[allow(unsafe_code)] #[allow(unsafe_code)]
fn GetScrollParent(&self) -> Option<DomRoot<Element>> { fn GetScrollParent(&self) -> Option<DomRoot<Element>> {
self.owner_window() self.owner_window()
.scroll_container_query(self.upcast(), ScrollContainerQueryFlags::ForScrollParent) .scroll_container_query(
Some(self.upcast()),
ScrollContainerQueryFlags::ForScrollParent,
)
.and_then(|response| match response { .and_then(|response| match response {
ScrollContainerResponse::Viewport => self.owner_document().GetScrollingElement(), ScrollContainerResponse::Viewport(_) => self.owner_document().GetScrollingElement(),
ScrollContainerResponse::Element(parent_node_address, _) => { ScrollContainerResponse::Element(parent_node_address, _) => {
let node = unsafe { from_untrusted_node_address(parent_node_address) }; let node = unsafe { from_untrusted_node_address(parent_node_address) };
DomRoot::downcast(node) DomRoot::downcast(node)

View file

@ -16,6 +16,7 @@ use crate::dom::types::{Document, Element};
pub(crate) struct ScrollingBox { pub(crate) struct ScrollingBox {
target: ScrollingBoxSource, target: ScrollingBoxSource,
overflow: AxesOverflow,
cached_content_size: Cell<Option<LayoutSize>>, cached_content_size: Cell<Option<LayoutSize>>,
cached_size: Cell<Option<LayoutSize>>, cached_size: Cell<Option<LayoutSize>>,
} }
@ -23,7 +24,7 @@ pub(crate) struct ScrollingBox {
/// Represents a scrolling box that can be either an element or the viewport /// Represents a scrolling box that can be either an element or the viewport
/// <https://drafts.csswg.org/cssom-view/#scrolling-box> /// <https://drafts.csswg.org/cssom-view/#scrolling-box>
pub(crate) enum ScrollingBoxSource { pub(crate) enum ScrollingBoxSource {
Element(DomRoot<Element>, AxesOverflow), Element(DomRoot<Element>),
Viewport(DomRoot<Document>), Viewport(DomRoot<Document>),
} }
@ -34,9 +35,10 @@ pub(crate) enum ScrollingBoxAxis {
} }
impl ScrollingBox { impl ScrollingBox {
pub(crate) fn new(target: ScrollingBoxSource) -> Self { pub(crate) fn new(target: ScrollingBoxSource, overflow: AxesOverflow) -> Self {
Self { Self {
target, target,
overflow,
cached_content_size: Default::default(), cached_content_size: Default::default(),
cached_size: Default::default(), cached_size: Default::default(),
} }
@ -52,7 +54,7 @@ impl ScrollingBox {
pub(crate) fn scroll_position(&self) -> LayoutVector2D { pub(crate) fn scroll_position(&self) -> LayoutVector2D {
match &self.target { match &self.target {
ScrollingBoxSource::Element(element, _) => element ScrollingBoxSource::Element(element) => element
.owner_window() .owner_window()
.scroll_offset_query(element.upcast::<Node>()), .scroll_offset_query(element.upcast::<Node>()),
ScrollingBoxSource::Viewport(document) => document.window().scroll_offset(), ScrollingBoxSource::Viewport(document) => document.window().scroll_offset(),
@ -65,7 +67,7 @@ impl ScrollingBox {
} }
let (document, node_to_query) = match &self.target { let (document, node_to_query) = match &self.target {
ScrollingBoxSource::Element(element, _) => { ScrollingBoxSource::Element(element) => {
(element.owner_document(), Some(element.upcast())) (element.owner_document(), Some(element.upcast()))
}, },
ScrollingBoxSource::Viewport(document) => (document.clone(), None), ScrollingBoxSource::Viewport(document) => (document.clone(), None),
@ -87,9 +89,7 @@ impl ScrollingBox {
} }
let size = match &self.target { let size = match &self.target {
ScrollingBoxSource::Element(element, _) => { ScrollingBoxSource::Element(element) => element.client_rect().size.to_f32().cast_unit(),
element.client_rect().size.to_f32().cast_unit()
},
ScrollingBoxSource::Viewport(document) => { ScrollingBoxSource::Viewport(document) => {
document.window().viewport_details().size.cast_unit() document.window().viewport_details().size.cast_unit()
}, },
@ -100,7 +100,7 @@ impl ScrollingBox {
pub(crate) fn parent(&self) -> Option<ScrollingBox> { pub(crate) fn parent(&self) -> Option<ScrollingBox> {
match &self.target { match &self.target {
ScrollingBoxSource::Element(element, _) => { ScrollingBoxSource::Element(element) => {
element.scrolling_box(ScrollContainerQueryFlags::empty()) element.scrolling_box(ScrollContainerQueryFlags::empty())
}, },
ScrollingBoxSource::Viewport(_) => None, ScrollingBoxSource::Viewport(_) => None,
@ -109,14 +109,14 @@ impl ScrollingBox {
pub(crate) fn node(&self) -> &Node { pub(crate) fn node(&self) -> &Node {
match &self.target { match &self.target {
ScrollingBoxSource::Element(element, _) => element.upcast(), ScrollingBoxSource::Element(element) => element.upcast(),
ScrollingBoxSource::Viewport(document) => document.upcast(), ScrollingBoxSource::Viewport(document) => document.upcast(),
} }
} }
pub(crate) fn scroll_to(&self, position: LayoutVector2D, behavior: ScrollBehavior) { pub(crate) fn scroll_to(&self, position: LayoutVector2D, behavior: ScrollBehavior) {
match &self.target { match &self.target {
ScrollingBoxSource::Element(element, _) => { ScrollingBoxSource::Element(element) => {
element element
.owner_window() .owner_window()
.scroll_an_element(element, position.x, position.y, behavior); .scroll_an_element(element, position.x, position.y, behavior);
@ -128,15 +128,11 @@ impl ScrollingBox {
} }
pub(crate) fn can_keyboard_scroll_in_axis(&self, axis: ScrollingBoxAxis) -> bool { pub(crate) fn can_keyboard_scroll_in_axis(&self, axis: ScrollingBoxAxis) -> bool {
let axes_overflow = match &self.target {
ScrollingBoxSource::Element(_, axes_overflow) => axes_overflow,
ScrollingBoxSource::Viewport(_) => return true,
};
let overflow = match axis { let overflow = match axis {
ScrollingBoxAxis::X => axes_overflow.x, ScrollingBoxAxis::X => self.overflow.x,
ScrollingBoxAxis::Y => axes_overflow.x, ScrollingBoxAxis::Y => self.overflow.y,
}; };
if !overflow.is_scrollable() || overflow == Overflow::Hidden { if overflow == Overflow::Hidden {
return false; return false;
} }
match axis { match axis {

View file

@ -157,6 +157,7 @@ use crate::dom::promise::Promise;
use crate::dom::reportingendpoint::{ReportingEndpoint, SendReportsToEndpoints}; use crate::dom::reportingendpoint::{ReportingEndpoint, SendReportsToEndpoints};
use crate::dom::reportingobserver::ReportingObserver; use crate::dom::reportingobserver::ReportingObserver;
use crate::dom::screen::Screen; use crate::dom::screen::Screen;
use crate::dom::scrolling_box::{ScrollingBox, ScrollingBoxSource};
use crate::dom::selection::Selection; use crate::dom::selection::Selection;
use crate::dom::shadowroot::ShadowRoot; use crate::dom::shadowroot::ShadowRoot;
use crate::dom::storage::Storage; use crate::dom::storage::Storage;
@ -2628,13 +2629,37 @@ impl Window {
pub(crate) fn scroll_container_query( pub(crate) fn scroll_container_query(
&self, &self,
node: &Node, node: Option<&Node>,
flags: ScrollContainerQueryFlags, flags: ScrollContainerQueryFlags,
) -> Option<ScrollContainerResponse> { ) -> Option<ScrollContainerResponse> {
self.layout_reflow(QueryMsg::ScrollParentQuery); self.layout_reflow(QueryMsg::ScrollParentQuery);
self.layout self.layout
.borrow() .borrow()
.query_scroll_container(node.to_trusted_node_address(), flags) .query_scroll_container(node.map(Node::to_trusted_node_address), flags)
}
#[allow(unsafe_code)]
pub(crate) fn scrolling_box_query(
&self,
node: Option<&Node>,
flags: ScrollContainerQueryFlags,
) -> Option<ScrollingBox> {
self.scroll_container_query(node, flags)
.and_then(|response| {
Some(match response {
ScrollContainerResponse::Viewport(overflow) => {
(ScrollingBoxSource::Viewport(self.Document()), overflow)
},
ScrollContainerResponse::Element(parent_node_address, overflow) => {
let node = unsafe { from_untrusted_node_address(parent_node_address) };
(
ScrollingBoxSource::Element(DomRoot::downcast(node)?),
overflow,
)
},
})
})
.map(|(source, overflow)| ScrollingBox::new(source, overflow))
} }
pub(crate) fn text_index_query( pub(crate) fn text_index_query(

View file

@ -107,7 +107,9 @@ pub(crate) struct WindowProxy {
/// <https://html.spec.whatwg.org/multipage/#is-closing> /// <https://html.spec.whatwg.org/multipage/#is-closing>
is_closing: Cell<bool>, is_closing: Cell<bool>,
/// The containing iframe element, if this is a same-origin iframe /// If the containing `<iframe>` of this [`WindowProxy`] is from a same-origin page,
/// this will be the [`Element`] of the `<iframe>` element in the realm of the
/// parent page. Otherwise, it is `None`.
frame_element: Option<Dom<Element>>, frame_element: Option<Dom<Element>>,
/// The parent browsing context's window proxy, if this is a nested browsing context /// The parent browsing context's window proxy, if this is a nested browsing context
@ -630,6 +632,9 @@ impl WindowProxy {
self.webview_id self.webview_id
} }
/// If the containing `<iframe>` of this [`WindowProxy`] is from a same-origin page,
/// this will return an [`Element`] of the `<iframe>` element in the realm of the parent
/// page.
pub(crate) fn frame_element(&self) -> Option<&Element> { pub(crate) fn frame_element(&self) -> Option<&Element> {
self.frame_element.as_deref() self.frame_element.as_deref()
} }

View file

@ -101,6 +101,7 @@ impl MixedMessage {
ScriptThreadMessage::SendImageKeysBatch(..) => None, ScriptThreadMessage::SendImageKeysBatch(..) => None,
ScriptThreadMessage::PreferencesUpdated(..) => None, ScriptThreadMessage::PreferencesUpdated(..) => None,
ScriptThreadMessage::NoLongerWaitingOnAsychronousImageUpdates(_) => None, ScriptThreadMessage::NoLongerWaitingOnAsychronousImageUpdates(_) => None,
ScriptThreadMessage::ForwardKeyboardScroll(id, _) => Some(*id),
}, },
MixedMessage::FromScript(inner_msg) => match inner_msg { MixedMessage::FromScript(inner_msg) => match inner_msg {
MainThreadScriptMsg::Common(CommonScriptMsg::Task(_, _, pipeline_id, _)) => { MainThreadScriptMsg::Common(CommonScriptMsg::Task(_, _, pipeline_id, _)) => {

View file

@ -1911,6 +1911,11 @@ impl ScriptThread {
} }
prefs::set(current_preferences); prefs::set(current_preferences);
}, },
ScriptThreadMessage::ForwardKeyboardScroll(pipeline_id, scroll) => {
if let Some(document) = self.documents.borrow().find_document(pipeline_id) {
document.event_handler().do_keyboard_scroll(scroll);
}
},
} }
} }

View file

@ -26,6 +26,7 @@ fonts_traits = { workspace = true }
http = { workspace = true } http = { workspace = true }
hyper_serde = { workspace = true } hyper_serde = { workspace = true }
ipc-channel = { workspace = true } ipc-channel = { workspace = true }
keyboard-types = { workspace = true }
log = { workspace = true } log = { workspace = true }
malloc_size_of = { workspace = true } malloc_size_of = { workspace = true }
malloc_size_of_derive = { workspace = true } malloc_size_of_derive = { workspace = true }

View file

@ -480,6 +480,27 @@ pub struct IFrameSizeMsg {
pub type_: WindowSizeType, pub type_: WindowSizeType,
} }
/// An enum that describe a type of keyboard scroll.
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub enum KeyboardScroll {
/// Scroll the container one line up.
Up,
/// Scroll the container one line down.
Down,
/// Scroll the container one "line" left.
Left,
/// Scroll the container one "line" right.
Right,
/// Scroll the container one page up.
PageUp,
/// Scroll the container one page down.
PageDown,
/// Scroll the container to the vertical start.
Home,
/// Scroll the container to the vertical end.
End,
}
/// Messages from the script to the constellation. /// Messages from the script to the constellation.
#[derive(Deserialize, IntoStaticStr, Serialize)] #[derive(Deserialize, IntoStaticStr, Serialize)]
pub enum ScriptToConstellationMessage { pub enum ScriptToConstellationMessage {
@ -665,6 +686,8 @@ pub enum ScriptToConstellationMessage {
), ),
/// Notify the completion of a webdriver command. /// Notify the completion of a webdriver command.
WebDriverInputComplete(WebDriverMessageId), WebDriverInputComplete(WebDriverMessageId),
/// Forward a keyboard scroll operation from an `<iframe>` to a parent pipeline.
ForwardKeyboardScroll(PipelineId, KeyboardScroll),
} }
impl fmt::Debug for ScriptToConstellationMessage { impl fmt::Debug for ScriptToConstellationMessage {

View file

@ -306,9 +306,11 @@ pub trait Layout {
fn query_client_rect(&self, node: TrustedNodeAddress) -> Rect<i32>; fn query_client_rect(&self, node: TrustedNodeAddress) -> Rect<i32>;
fn query_element_inner_outer_text(&self, node: TrustedNodeAddress) -> String; fn query_element_inner_outer_text(&self, node: TrustedNodeAddress) -> String;
fn query_offset_parent(&self, node: TrustedNodeAddress) -> OffsetParentResponse; fn query_offset_parent(&self, node: TrustedNodeAddress) -> OffsetParentResponse;
/// Query the scroll container for the given node. If node is `None`, the scroll container for
/// the viewport is returned.
fn query_scroll_container( fn query_scroll_container(
&self, &self,
node: TrustedNodeAddress, node: Option<TrustedNodeAddress>,
flags: ScrollContainerQueryFlags, flags: ScrollContainerQueryFlags,
) -> Option<ScrollContainerResponse>; ) -> Option<ScrollContainerResponse>;
fn query_resolved_style( fn query_resolved_style(
@ -377,7 +379,7 @@ bitflags! {
} }
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, MallocSizeOf)]
pub struct AxesOverflow { pub struct AxesOverflow {
pub x: Overflow, pub x: Overflow,
pub y: Overflow, pub y: Overflow,
@ -401,9 +403,18 @@ impl From<&ComputedValues> for AxesOverflow {
} }
} }
impl AxesOverflow {
pub fn to_scrollable(&self) -> Self {
Self {
x: self.x.to_scrollable(),
y: self.y.to_scrollable(),
}
}
}
#[derive(Clone)] #[derive(Clone)]
pub enum ScrollContainerResponse { pub enum ScrollContainerResponse {
Viewport, Viewport(AxesOverflow),
Element(UntrustedNodeAddress, AxesOverflow), Element(UntrustedNodeAddress, AxesOverflow),
} }

View file

@ -21,8 +21,8 @@ use bluetooth_traits::BluetoothRequest;
use canvas_traits::webgl::WebGLPipeline; use canvas_traits::webgl::WebGLPipeline;
use compositing_traits::CrossProcessCompositorApi; use compositing_traits::CrossProcessCompositorApi;
use constellation_traits::{ use constellation_traits::{
LoadData, NavigationHistoryBehavior, ScriptToConstellationChan, StructuredSerializedData, KeyboardScroll, LoadData, NavigationHistoryBehavior, ScriptToConstellationChan,
WindowSizeType, StructuredSerializedData, WindowSizeType,
}; };
use crossbeam_channel::{RecvTimeoutError, Sender}; use crossbeam_channel::{RecvTimeoutError, Sender};
use devtools_traits::ScriptToDevtoolsControlMsg; use devtools_traits::ScriptToDevtoolsControlMsg;
@ -265,6 +265,8 @@ pub enum ScriptThreadMessage {
/// asynchronous image uploads for the given `Pipeline`. These are mainly used /// asynchronous image uploads for the given `Pipeline`. These are mainly used
/// by canvas to perform uploads while the display list is being built. /// by canvas to perform uploads while the display list is being built.
NoLongerWaitingOnAsychronousImageUpdates(PipelineId), NoLongerWaitingOnAsychronousImageUpdates(PipelineId),
/// Forward a keyboard scroll operation from an `<iframe>` to a parent pipeline.
ForwardKeyboardScroll(PipelineId, KeyboardScroll),
} }
impl fmt::Debug for ScriptThreadMessage { impl fmt::Debug for ScriptThreadMessage {

View file

@ -12757,15 +12757,6 @@
{} {}
] ]
], ],
"keyboard-scrolling.html": [
"2d9a0c40272d8af49f26de8dc49283df68b2d7b0",
[
null,
{
"testdriver": true
}
]
],
"matchMedia.html": [ "matchMedia.html": [
"45a7ea268b1ebdba69e947b79d675cc9221428d4", "45a7ea268b1ebdba69e947b79d675cc9221428d4",
[ [
@ -13811,6 +13802,24 @@
{} {}
] ]
], ],
"keyboard-scrolling-iframe.sub.html": [
"e002eb6f2a35ed4f73a079acb05b3d99eb04813b",
[
null,
{
"testdriver": true
}
]
],
"keyboard-scrolling.html": [
"45916c5d11f351f0ca10cde5e52d5ee60c11bd9d",
[
null,
{
"testdriver": true
}
]
],
"keyframe-infinite-percentage.html": [ "keyframe-infinite-percentage.html": [
"36ba83eeac401653356fa38edf30c94d38fd8542", "36ba83eeac401653356fa38edf30c94d38fd8542",
[ [

View file

@ -0,0 +1,165 @@
<!doctype html>
<meta charset="utf-8">
<title>A test to verify that keyboard scrolling works properly in Servo in IFRAMEs.</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<style>
body {
margin: 0;
}
iframe {
position: fixed;
width: 200px;
height: 200px;
outline: solid;
}
</style>
<!-- This is an IFRAME that should be scrollable via keyboard. -->
<iframe id="iframe" style="left: 100px; top: 100px;" srcdoc='
<!DOCTYPE html>
<body style="margin: 0;">
<div style="width: 600px; height: 600px; background: blue;">Lorem ipsum dolor sit amet,</div>
'></iframe>
<!-- This IFRAME does not scroll, so keyboard events should chain to the main frame. -->
<iframe id="iframeWithSmallContent" style="left: 300px; top: 100px;" src='iframe_child1.html'></iframe>
<!-- This IFRAME does not scroll and is also cross origin, so keyboard events should chain to the main frame. -->
<iframe id="iframeCrossOriginWithSmallContent" style="left: 100px; top: 300px;" src='//{{hosts[alt][]}}:{{ports[http][0]}}/_mozilla/mozilla/iframe_child1.html'></iframe>
<!-- This IFRAME does not scroll because the body has overflow: hidden, so keyboard events should chain to the main frame. -->
<iframe id="iframeWithOverflowHiddenBody" style="left: 300px; top: 300px;" srcdoc='
<!DOCTYPE html>
<body style="overflow:hidden;">
<div style="width: 600px; height: 600px; background: blue;">Lorem ipsum dolor sit amet,</div>
'></iframe>
<div style="width: 300vw; height: 300vh; background: green;">
Lorem ipsum dolor sit amet,
</div>
<script>
const end = "\uE010";
const home = "\uE011";
const arrowDown = "\uE015";
const arrowUp = "\uE013";
const arrowRight = "\uE014";
const arrowLeft = "\uE012";
const pageDown = "\uE00F";
const pageUp = "\uE00E";
const lineSize = 76;
const pageSize = scrollportHeight => scrollportHeight - 2 * lineSize;
const pressKeyAndAssert = async (key, element, [expectedX, expectedY], description) => {
await test_driver.send_keys(document.body, key);
const actualX =
element == null ? scrollX
: element.nodeName == "IFRAME" ? element.contentWindow.scrollX
: element.scrollLeft;
const actualY =
element == null ? scrollY
: element.nodeName == "IFRAME" ? element.contentWindow.scrollY
: element.scrollTop;
assert_array_equals([actualX, actualY], [expectedX, expectedY], description);
};
promise_test(async () => {
await test_driver.click(iframe);
let bottom = iframe.contentDocument.documentElement.scrollHeight - iframe.contentWindow.innerHeight;
await pressKeyAndAssert(end, iframe, [0, bottom], "End key scrolls #iframe to bottom");
await pressKeyAndAssert(home, iframe, [0, 0], "Home key scrolls #iframe to top");
await pressKeyAndAssert(arrowDown, iframe, [0, lineSize], "ArrowDown key scrolls #iframe down by a line");
await pressKeyAndAssert(arrowDown, iframe, [0, lineSize * 2], "ArrowDown key scrolls #iframe down by a line");
await pressKeyAndAssert(arrowUp, iframe, [0, lineSize], "ArrowUp key scrolls #iframe up by a line");
await pressKeyAndAssert(arrowUp, iframe, [0, 0], "ArrowUp key scrolls #iframe up by a line");
await pressKeyAndAssert(arrowRight, iframe, [lineSize, 0], "ArrowRight key scrolls #iframe right by a line");
await pressKeyAndAssert(arrowRight, iframe, [lineSize * 2, 0], "ArrowRight key scrolls #iframe right by a line");
await pressKeyAndAssert(arrowLeft, iframe, [lineSize, 0], "ArrowLeft key scrolls #iframe left by a line");
await pressKeyAndAssert(arrowLeft, iframe, [0, 0], "ArrowLeft key scrolls #iframe left by a line");
await pressKeyAndAssert(pageDown, iframe, [0, pageSize(iframe.contentWindow.innerHeight)], "PageDown key scrolls #iframe down by almost a screenful");
await pressKeyAndAssert(pageDown, iframe, [0, pageSize(iframe.contentWindow.innerHeight) * 2], "PageDown key scrolls #iframe down by almost a screenful");
await pressKeyAndAssert(pageUp, iframe, [0, pageSize(iframe.contentWindow.innerHeight)], "PageUp key scrolls #iframe up by almost a screenful");
await pressKeyAndAssert(pageUp, iframe, [0, 0], "PageUp key scrolls #iframe up by almost a screenful");
// At the bottom of the IFRAME, we should not chain up to scrolling the document.
await pressKeyAndAssert(end, iframe, [0, bottom], "End key scrolls #iframe to bottom");
await pressKeyAndAssert(arrowDown, iframe, [0, bottom], "ArrowDown should not move the iframe past the max Y position");
await pressKeyAndAssert(arrowDown, iframe, [0, bottom], "ArrowDown should not move the iframe past the max Y position");
await pressKeyAndAssert(arrowDown, iframe, [0, bottom], "ArrowDown should not move the iframe past the max Y position");
assert_array_equals([scrollX, scrollY], [0, 0], "Keyboard scroll on a div should not chain to body");
await pressKeyAndAssert(home, iframe, [0, 0], "Home key scrolls #iframe to top");
}, "Keyboard scrolling works in #iframe");
promise_test(async () => {
await test_driver.click(iframeWithOverflowHiddenBody);
await pressKeyAndAssert(arrowDown, iframeWithOverflowHiddenBody, [0, 0], "Arrow down key should not scroll #iframeWithOverflowHiddenBody");
assert_array_equals([scrollX, scrollY], [0, lineSize], "The body should scroll instead of #iframeWithOverflowHiddenBody");
await pressKeyAndAssert(arrowDown, iframeWithOverflowHiddenBody, [0, 0], "Arrow down key should not scroll #iframeWithOverflowHiddenBody");
assert_array_equals([scrollX, scrollY], [0, lineSize * 2], "The body should scroll instead of #iframeWithOverflowHiddenBody");
await pressKeyAndAssert(arrowUp, iframeWithOverflowHiddenBody, [0, 0], "Arrow up key should not scroll #iframeWithOverflowHiddenBody");
assert_array_equals([scrollX, scrollY], [0, lineSize], "The body should scroll instead of #iframeWithOverflowHiddenBody");
await pressKeyAndAssert(arrowUp, iframeWithOverflowHiddenBody, [0, 0], "Arrow up key should not scroll #iframeWithOverflowHiddenBody");
assert_array_equals([scrollX, scrollY], [0, 0], "The body should scroll instead of #iframeWithOverflowHiddenBody");
}, "Keyboard scrolling on iframe with a body that has `overflow:hidden` chains to main document");
promise_test(async () => {
await test_driver.click(iframeCrossOriginWithSmallContent);
function waitForScrollEvent() {
return new Promise((resolve) => {
addEventListener("scroll", () => {
resolve();
}, { "once": true })
}
)
}
// We must create the promise before actually triggering the event, as the event might
// be fired while we are awaiting the keyboard action.
let promise = waitForScrollEvent();
await test_driver.send_keys(document.body, arrowDown);
await promise;
assert_array_equals([scrollX, scrollY], [0, lineSize], "The body should scroll instead of #iframeCrossOriginWithSmallContent");
promise = waitForScrollEvent();
await test_driver.send_keys(document.body, arrowDown);
await promise;
assert_array_equals([scrollX, scrollY], [0, lineSize * 2], "The body should scroll instead of #iframeCrossOriginWithSmallContent");
promise = waitForScrollEvent();
await test_driver.send_keys(document.body, arrowUp);
await promise;
assert_array_equals([scrollX, scrollY], [0, lineSize], "The body should scroll instead of #iframeCrossOriginWithSmallContent");
promise = waitForScrollEvent();
await test_driver.send_keys(document.body, arrowUp);
await promise;
assert_array_equals([scrollX, scrollY], [0, 0], "The body should scroll instead of #iframeCrossOriginWithSmallContent");
}, "Keyboard scrolling on cross-origin iframe with small content chains to main document");
promise_test(async () => {
await test_driver.click(iframeWithSmallContent);
await pressKeyAndAssert(arrowDown, iframeWithSmallContent, [0, 0], "Arrow down key should not scroll #iframeWithSmallContent");
assert_array_equals([scrollX, scrollY], [0, lineSize], "The body should scroll instead of #iframeWithSmallContent");
await pressKeyAndAssert(arrowDown, iframeWithSmallContent, [0, 0], "Arrow down key should not scroll #iframeWithSmallContent");
assert_array_equals([scrollX, scrollY], [0, lineSize * 2], "The body should scroll instead of #iframeWithSmallContent");
await pressKeyAndAssert(arrowUp, iframeWithSmallContent, [0, 0], "Arrow up key should not scroll #iframeWithSmallContent");
assert_array_equals([scrollX, scrollY], [0, lineSize], "The body should scroll instead of #iframeWithSmallContent");
await pressKeyAndAssert(arrowUp, iframeWithSmallContent, [0, 0], "Arrow up key should not scroll #iframeWithSmallContent");
assert_array_equals([scrollX, scrollY], [0, 0], "The body should scroll instead of #iframeWithSmallContent");
}, "Keyboard scrolling on iframe with small content chains to main document");
</script>

View file

@ -1,6 +1,6 @@
<!doctype html> <!doctype html>
<meta charset="utf-8"> <meta charset="utf-8">
<title>CSS test: Calc expressions with numbers should still serialize as calc()</title> <title>A test to verify that keyboard scrolling works properly in Servo.</title>
<script src="/resources/testharness.js"></script> <script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script> <script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script> <script src="/resources/testdriver.js"></script>
@ -36,15 +36,8 @@
<div style="width: 100px; height: 100px; background: blue;">Lorem ipsum dolor sit amet,</div> <div style="width: 100px; height: 100px; background: blue;">Lorem ipsum dolor sit amet,</div>
</div> </div>
<!-- This is an IFRAME that should be scrollable via keyboard. -->
<iframe id="iframe" style="left: 300px; top: 300px;" srcdoc='
<!doctype html><meta charset="utf-8">
<body style="margin: 0;">
<div style="width: 600px; height: 600px; background: blue;">Lorem ipsum dolor sit amet,</div>
'></iframe>
<!-- This is a DIV with `overflow: hidden` that should not be keyboard scrollable as its content is smaller than the DIV. --> <!-- This is a DIV with `overflow: hidden` that should not be keyboard scrollable as its content is smaller than the DIV. -->
<div id="boxWithOverflowHidden" class="scroller" style="overflow: hidden; left: 500px; top: 300px;"> <div id="boxWithOverflowHidden" class="scroller" style="overflow: hidden; left: 300px; top: 300px;">
<div style="width: 600px; height: 600px; background: blue;">Lorem ipsum dolor sit amet,</div> <div style="width: 600px; height: 600px; background: blue;">Lorem ipsum dolor sit amet,</div>
</div> </div>
@ -66,13 +59,9 @@ const pageSize = scrollportHeight => scrollportHeight - 2 * lineSize;
const pressKeyAndAssert = async (key, element, [expectedX, expectedY], description) => { const pressKeyAndAssert = async (key, element, [expectedX, expectedY], description) => {
await test_driver.send_keys(document.body, key); await test_driver.send_keys(document.body, key);
const actualX = const actualX =
element == null ? scrollX element == null ? scrollX : element.scrollLeft;
: element.nodeName == "IFRAME" ? element.contentWindow.scrollX
: element.scrollLeft;
const actualY = const actualY =
element == null ? scrollY element == null ? scrollY : element.scrollTop;
: element.nodeName == "IFRAME" ? element.contentWindow.scrollY
: element.scrollTop;
assert_array_equals([actualX, actualY], [expectedX, expectedY], description); assert_array_equals([actualX, actualY], [expectedX, expectedY], description);
}; };
@ -156,27 +145,6 @@ promise_test(async () => {
await pressKeyAndAssert(home, null, [0, 0], "Home key scrolls viewport to top"); await pressKeyAndAssert(home, null, [0, 0], "Home key scrolls viewport to top");
}, "Keyboard scrolling chains past inactive overflow:scroll DIVs"); }, "Keyboard scrolling chains past inactive overflow:scroll DIVs");
promise_test(async () => {
await test_driver.click(iframe);
await pressKeyAndAssert(end, iframe, [0, iframe.contentDocument.documentElement.scrollHeight - iframe.contentWindow.innerHeight], "End key scrolls #iframe to bottom");
await pressKeyAndAssert(home, iframe, [0, 0], "Home key scrolls #iframe to top");
await pressKeyAndAssert(arrowDown, iframe, [0, lineSize], "ArrowDown key scrolls #iframe down by a line");
await pressKeyAndAssert(arrowDown, iframe, [0, lineSize * 2], "ArrowDown key scrolls #iframe down by a line");
await pressKeyAndAssert(arrowUp, iframe, [0, lineSize], "ArrowUp key scrolls #iframe up by a line");
await pressKeyAndAssert(arrowUp, iframe, [0, 0], "ArrowUp key scrolls #iframe up by a line");
await pressKeyAndAssert(arrowRight, iframe, [lineSize, 0], "ArrowRight key scrolls #iframe right by a line");
await pressKeyAndAssert(arrowRight, iframe, [lineSize * 2, 0], "ArrowRight key scrolls #iframe right by a line");
await pressKeyAndAssert(arrowLeft, iframe, [lineSize, 0], "ArrowLeft key scrolls #iframe left by a line");
await pressKeyAndAssert(arrowLeft, iframe, [0, 0], "ArrowLeft key scrolls #iframe left by a line");
await pressKeyAndAssert(pageDown, iframe, [0, pageSize(iframe.contentWindow.innerHeight)], "PageDown key scrolls #iframe down by almost a screenful");
await pressKeyAndAssert(pageDown, iframe, [0, pageSize(iframe.contentWindow.innerHeight) * 2], "PageDown key scrolls #iframe down by almost a screenful");
await pressKeyAndAssert(pageUp, iframe, [0, pageSize(iframe.contentWindow.innerHeight)], "PageUp key scrolls #iframe up by almost a screenful");
await pressKeyAndAssert(pageUp, iframe, [0, 0], "PageUp key scrolls #iframe up by almost a screenful");
// TODO: test that scrolls chain up from iframe when they fail.
}, "Keyboard scrolling works in #iframe");
promise_test(async () => { promise_test(async () => {
await test_driver.click(boxWithOverflowHidden); await test_driver.click(boxWithOverflowHidden);