mirror of
https://github.com/servo/servo.git
synced 2025-09-25 06:10:08 +01:00
script: Move keyboard scrolling to script (#39371)
Instead of having every single embedder implement keyboard scrolling, handle it in script in the default key event handler. This allows properly targeting the scroll events to their scroll containers as well as appropriately sizing "page up" and "page down" scroll deltas. This change means that when you use the keyboard to scroll, the focused or most recently clicked `<iframe>` or overflow scroll container is scrolled, rather than the main frame. In addition, when a particular scroll frame is larger than its content in the axis of the scroll, the scrolling operation is chained to the parent (as in other browsers). One exception is for `<iframe>`s, which will be implemented in a followup change. Testing: automated tests runnable locally with `mach test-wpt --product servodriver` Signed-off-by: Martin Robinson <mrobinson@igalia.com> Co-authored-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
parent
99fbd36b5d
commit
ac8895c3ae
19 changed files with 540 additions and 185 deletions
|
@ -8,7 +8,7 @@ use compositing_traits::display_list::AxesScrollSensitivity;
|
|||
use euclid::Rect;
|
||||
use euclid::default::Size2D as UntypedSize2D;
|
||||
use layout_api::wrapper_traits::{LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode};
|
||||
use layout_api::{LayoutElementType, LayoutNodeType};
|
||||
use layout_api::{AxesOverflow, LayoutElementType, LayoutNodeType};
|
||||
use malloc_size_of_derive::MallocSizeOf;
|
||||
use script::layout_dom::{ServoLayoutNode, ServoThreadSafeLayoutNode};
|
||||
use servo_arc::Arc;
|
||||
|
@ -31,7 +31,7 @@ use crate::fragment_tree::{FragmentFlags, FragmentTree};
|
|||
use crate::geom::{LogicalVec2, PhysicalSize};
|
||||
use crate::positioned::{AbsolutelyPositionedBox, PositioningContext};
|
||||
use crate::replaced::ReplacedContents;
|
||||
use crate::style_ext::{AxesOverflow, Display, DisplayGeneratingBox, DisplayInside};
|
||||
use crate::style_ext::{Display, DisplayGeneratingBox, DisplayInside};
|
||||
use crate::taffy::{TaffyItemBox, TaffyItemBoxInner};
|
||||
use crate::{DefiniteContainingBlock, PropagatedBoxTreeData};
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ use layout_api::{
|
|||
BoxAreaType, IFrameSizes, Layout, LayoutConfig, LayoutDamage, LayoutFactory,
|
||||
OffsetParentResponse, PropertyRegistration, QueryMsg, ReflowGoal, ReflowPhasesRun,
|
||||
ReflowRequest, ReflowRequestRestyle, ReflowResult, RegisterPropertyError,
|
||||
ScrollContainerQueryType, ScrollContainerResponse, TrustedNodeAddress,
|
||||
ScrollContainerQueryFlags, ScrollContainerResponse, TrustedNodeAddress,
|
||||
};
|
||||
use log::{debug, error, warn};
|
||||
use malloc_size_of::{MallocConditionalSizeOf, MallocSizeOf, MallocSizeOfOps};
|
||||
|
@ -334,10 +334,10 @@ impl Layout for LayoutThread {
|
|||
fn query_scroll_container(
|
||||
&self,
|
||||
node: TrustedNodeAddress,
|
||||
query_type: ScrollContainerQueryType,
|
||||
flags: ScrollContainerQueryFlags,
|
||||
) -> Option<ScrollContainerResponse> {
|
||||
let node = unsafe { ServoLayoutNode::new(&node) };
|
||||
process_scroll_container_query(node, query_type)
|
||||
process_scroll_container_query(node, flags)
|
||||
}
|
||||
|
||||
#[servo_tracing::instrument(skip_all)]
|
||||
|
|
|
@ -12,8 +12,8 @@ use euclid::{SideOffsets2D, Size2D};
|
|||
use itertools::Itertools;
|
||||
use layout_api::wrapper_traits::{LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode};
|
||||
use layout_api::{
|
||||
BoxAreaType, LayoutElementType, LayoutNodeType, OffsetParentResponse, ScrollContainerQueryType,
|
||||
ScrollContainerResponse,
|
||||
BoxAreaType, LayoutElementType, LayoutNodeType, OffsetParentResponse,
|
||||
ScrollContainerQueryFlags, ScrollContainerResponse,
|
||||
};
|
||||
use script::layout_dom::{ServoLayoutNode, ServoThreadSafeLayoutNode};
|
||||
use servo_arc::Arc as ServoArc;
|
||||
|
@ -677,7 +677,7 @@ pub fn process_offset_parent_query(
|
|||
#[inline]
|
||||
pub(crate) fn process_scroll_container_query(
|
||||
node: ServoLayoutNode<'_>,
|
||||
query_type: ScrollContainerQueryType,
|
||||
query_flags: ScrollContainerQueryFlags,
|
||||
) -> Option<ScrollContainerResponse> {
|
||||
let layout_data = node.to_threadsafe().inner_layout_data()?;
|
||||
|
||||
|
@ -686,15 +686,15 @@ pub(crate) fn process_scroll_container_query(
|
|||
let layout_box = layout_data.self_box.borrow();
|
||||
let layout_box = layout_box.as_ref()?;
|
||||
|
||||
let (mut current_position_value, flags) = layout_box
|
||||
.with_first_base(|base| (base.style.clone_position(), base.base_fragment_info.flags))?;
|
||||
let (style, flags) =
|
||||
layout_box.with_first_base(|base| (base.style.clone(), base.base_fragment_info.flags))?;
|
||||
|
||||
// - The element is the root element.
|
||||
// - The element is the body element.
|
||||
//
|
||||
// Note: We only do this for `scrollParent`, which needs to be null. But `scrollIntoView` on the
|
||||
// `<body>` or root element should still bring it into view by scrolling the viewport.
|
||||
if query_type == ScrollContainerQueryType::ForScrollParent &&
|
||||
if query_flags.contains(ScrollContainerQueryFlags::ForScrollParent) &&
|
||||
flags.intersects(
|
||||
FragmentFlags::IS_ROOT_ELEMENT | FragmentFlags::IS_BODY_ELEMENT_OF_HTML_ELEMENT_ROOT,
|
||||
)
|
||||
|
@ -702,6 +702,15 @@ pub(crate) fn process_scroll_container_query(
|
|||
return None;
|
||||
}
|
||||
|
||||
if query_flags.contains(ScrollContainerQueryFlags::Inclusive) &&
|
||||
style.establishes_scroll_container(flags)
|
||||
{
|
||||
return Some(ScrollContainerResponse::Element(
|
||||
node.opaque().into(),
|
||||
style.effective_overflow(flags),
|
||||
));
|
||||
}
|
||||
|
||||
// - The element’s computed value of the position property is fixed and no ancestor
|
||||
// establishes a fixed position containing block.
|
||||
//
|
||||
|
@ -721,6 +730,7 @@ pub(crate) fn process_scroll_container_query(
|
|||
// Notes: We don't follow the specification exactly below, but we follow the spirit.
|
||||
//
|
||||
// TODO: Handle the situation where the ancestor is "closed-shadow-hidden" from the element.
|
||||
let mut current_position_value = style.clone_position();
|
||||
let mut current_ancestor = node.as_element()?;
|
||||
while let Some(ancestor) = current_ancestor.traversal_parent() {
|
||||
current_ancestor = ancestor;
|
||||
|
@ -757,6 +767,7 @@ pub(crate) fn process_scroll_container_query(
|
|||
if ancestor_style.establishes_scroll_container(ancestor_flags) {
|
||||
return Some(ScrollContainerResponse::Element(
|
||||
ancestor.as_node().opaque().into(),
|
||||
ancestor_style.effective_overflow(ancestor_flags),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use app_units::Au;
|
||||
use layout_api::AxesOverflow;
|
||||
use malloc_size_of_derive::MallocSizeOf;
|
||||
use style::Zero;
|
||||
use style::color::AbsoluteColor;
|
||||
|
@ -58,30 +59,6 @@ pub(crate) enum DisplayGeneratingBox {
|
|||
/// <https://drafts.csswg.org/css-display-3/#layout-specific-display>
|
||||
LayoutInternal(DisplayLayoutInternal),
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct AxesOverflow {
|
||||
pub x: Overflow,
|
||||
pub y: Overflow,
|
||||
}
|
||||
|
||||
impl Default for AxesOverflow {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
x: Overflow::Visible,
|
||||
y: Overflow::Visible,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ComputedValues> for AxesOverflow {
|
||||
fn from(style: &ComputedValues) -> Self {
|
||||
Self {
|
||||
x: style.clone_overflow_x(),
|
||||
y: style.clone_overflow_y(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DisplayGeneratingBox {
|
||||
pub(crate) fn display_inside(&self) -> DisplayInside {
|
||||
match *self {
|
||||
|
|
|
@ -174,6 +174,7 @@ use crate::dom::processinginstruction::ProcessingInstruction;
|
|||
use crate::dom::promise::Promise;
|
||||
use crate::dom::range::Range;
|
||||
use crate::dom::resizeobserver::{ResizeObservationDepth, ResizeObserver};
|
||||
use crate::dom::scrolling_box::{ScrollingBox, ScrollingBoxSource};
|
||||
use crate::dom::selection::Selection;
|
||||
use crate::dom::servoparser::ServoParser;
|
||||
use crate::dom::shadowroot::ShadowRoot;
|
||||
|
@ -4447,6 +4448,10 @@ impl Document {
|
|||
pub(crate) fn set_active_sandboxing_flag_set(&self, flags: SandboxingFlagSet) {
|
||||
self.active_sandboxing_flag_set.set(flags)
|
||||
}
|
||||
|
||||
pub(crate) fn viewport_scrolling_box(&self) -> ScrollingBox {
|
||||
ScrollingBox::new(ScrollingBoxSource::Viewport(DomRoot::from_ref(self)))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
|
|
|
@ -18,17 +18,17 @@ use embedder_traits::{
|
|||
MouseLeftViewportEvent, ScrollEvent, TouchEvent as EmbedderTouchEvent, TouchEventType, TouchId,
|
||||
UntrustedNodeAddress, WheelEvent as EmbedderWheelEvent,
|
||||
};
|
||||
use euclid::Point2D;
|
||||
use euclid::{Point2D, Vector2D};
|
||||
use ipc_channel::ipc;
|
||||
use keyboard_types::{Code, Key, KeyState, Modifiers, NamedKey};
|
||||
use layout_api::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::EventBinding::EventMethods;
|
||||
use script_bindings::codegen::GenericBindings::NavigatorBinding::NavigatorMethods;
|
||||
use script_bindings::codegen::GenericBindings::NodeBinding::NodeMethods;
|
||||
use script_bindings::codegen::GenericBindings::PerformanceBinding::PerformanceMethods;
|
||||
use script_bindings::codegen::GenericBindings::TouchBinding::TouchMethods;
|
||||
use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
|
||||
use script_bindings::codegen::GenericBindings::WindowBinding::{ScrollBehavior, WindowMethods};
|
||||
use script_bindings::inheritance::Castable;
|
||||
use script_bindings::num::Finite;
|
||||
use script_bindings::root::{Dom, DomRoot, DomSlice};
|
||||
|
@ -50,6 +50,7 @@ use crate::dom::gamepad::gamepadevent::GamepadEventType;
|
|||
use crate::dom::inputevent::HitTestResult;
|
||||
use crate::dom::node::{self, Node, ShadowIncluding};
|
||||
use crate::dom::pointerevent::PointerId;
|
||||
use crate::dom::scrolling_box::ScrollingBoxAxis;
|
||||
use crate::dom::types::{
|
||||
ClipboardEvent, CompositionEvent, DataTransfer, Element, Event, EventTarget, GlobalScope,
|
||||
HTMLAnchorElement, KeyboardEvent, MouseEvent, PointerEvent, Touch, TouchEvent, TouchList,
|
||||
|
@ -78,6 +79,8 @@ pub(crate) struct DocumentEventHandler {
|
|||
last_click_info: DomRefCell<Option<(Instant, Point2D<f32, CSSPixel>)>>,
|
||||
/// The element that is currently hovered by the cursor.
|
||||
current_hover_target: MutNullableDom<Element>,
|
||||
/// The element that was most recently clicked.
|
||||
most_recently_clicked_element: MutNullableDom<Element>,
|
||||
/// The most recent mouse movement point, used for processing `mouseleave` events.
|
||||
#[no_trace]
|
||||
most_recent_mousemove_point: Cell<Option<Point2D<f32, CSSPixel>>>,
|
||||
|
@ -100,6 +103,7 @@ impl DocumentEventHandler {
|
|||
mouse_move_event_index: Default::default(),
|
||||
last_click_info: Default::default(),
|
||||
current_hover_target: Default::default(),
|
||||
most_recently_clicked_element: Default::default(),
|
||||
most_recent_mousemove_point: Default::default(),
|
||||
current_cursor: Default::default(),
|
||||
active_touch_points: Default::default(),
|
||||
|
@ -581,6 +585,8 @@ impl DocumentEventHandler {
|
|||
match event.action {
|
||||
// https://w3c.github.io/uievents/#handle-native-mouse-click
|
||||
MouseButtonAction::Click => {
|
||||
self.most_recently_clicked_element.set(Some(&el));
|
||||
|
||||
el.set_click_in_progress(true);
|
||||
dom_event.dispatch(node.upcast(), false, can_gc);
|
||||
el.set_click_in_progress(false);
|
||||
|
@ -1472,4 +1478,70 @@ impl DocumentEventHandler {
|
|||
document.handle_element_scroll_event(&element);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn run_default_keyboard_event_handler(&self, event: &KeyboardEvent) {
|
||||
if event.upcast::<Event>().type_() != atom!("keydown") {
|
||||
return;
|
||||
}
|
||||
if !event.modifiers().is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let scroll_axis = match event.key() {
|
||||
Key::Named(
|
||||
NamedKey::Home |
|
||||
NamedKey::End |
|
||||
NamedKey::PageDown |
|
||||
NamedKey::PageUp |
|
||||
NamedKey::ArrowUp |
|
||||
NamedKey::ArrowDown,
|
||||
) => ScrollingBoxAxis::Y,
|
||||
Key::Named(NamedKey::ArrowLeft | NamedKey::ArrowRight) => ScrollingBoxAxis::X,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let document = self.window.Document();
|
||||
let mut scrolling_box = document
|
||||
.get_focused_element()
|
||||
.or(self.most_recently_clicked_element.get())
|
||||
.and_then(|element| element.scrolling_box(ScrollContainerQueryFlags::Inclusive))
|
||||
.unwrap_or_else(|| document.viewport_scrolling_box());
|
||||
|
||||
while !scrolling_box.can_keyboard_scroll_in_axis(scroll_axis) {
|
||||
// Always fall back to trying to scroll the entire document.
|
||||
if scrolling_box.is_viewport() {
|
||||
break;
|
||||
}
|
||||
let parent = scrolling_box
|
||||
.parent()
|
||||
.unwrap_or_else(|| document.viewport_scrolling_box());
|
||||
scrolling_box = parent;
|
||||
}
|
||||
|
||||
const LINE_HEIGHT: f32 = 76.0;
|
||||
const LINE_WIDTH: f32 = 76.0;
|
||||
|
||||
let current_scroll_offset = scrolling_box.scroll_position();
|
||||
let delta = match event.key() {
|
||||
Key::Named(NamedKey::Home) => Vector2D::new(0.0, -current_scroll_offset.y),
|
||||
Key::Named(NamedKey::End) => Vector2D::new(
|
||||
0.0,
|
||||
-current_scroll_offset.y + scrolling_box.content_size().height -
|
||||
scrolling_box.size().height,
|
||||
),
|
||||
Key::Named(NamedKey::PageDown) => {
|
||||
Vector2D::new(0.0, scrolling_box.size().height - 2.0 * LINE_HEIGHT)
|
||||
},
|
||||
Key::Named(NamedKey::PageUp) => {
|
||||
Vector2D::new(0.0, 2.0 * LINE_HEIGHT - scrolling_box.size().height)
|
||||
},
|
||||
Key::Named(NamedKey::ArrowUp) => Vector2D::new(0.0, -LINE_HEIGHT),
|
||||
Key::Named(NamedKey::ArrowDown) => Vector2D::new(0.0, LINE_HEIGHT),
|
||||
Key::Named(NamedKey::ArrowLeft) => Vector2D::new(-LINE_WIDTH, 0.0),
|
||||
Key::Named(NamedKey::ArrowRight) => Vector2D::new(LINE_WIDTH, 0.0),
|
||||
_ => return,
|
||||
};
|
||||
|
||||
scrolling_box.scroll_to(delta + current_scroll_offset, ScrollBehavior::Auto);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ use html5ever::{LocalName, Namespace, Prefix, QualName, local_name, namespace_pr
|
|||
use js::jsapi::Heap;
|
||||
use js::jsval::JSVal;
|
||||
use js::rust::HandleObject;
|
||||
use layout_api::{LayoutDamage, ScrollContainerQueryType, ScrollContainerResponse};
|
||||
use layout_api::{LayoutDamage, ScrollContainerQueryFlags, ScrollContainerResponse};
|
||||
use net_traits::ReferrerPolicy;
|
||||
use net_traits::request::CorsSettings;
|
||||
use selectors::Element as SelectorsElement;
|
||||
|
@ -61,7 +61,7 @@ use style::values::{AtomIdent, AtomString, CSSFloat, computed, specified};
|
|||
use style::{ArcSlice, CaseSensitivityExt, dom_apis, thread_state};
|
||||
use stylo_atoms::Atom;
|
||||
use stylo_dom::ElementState;
|
||||
use webrender_api::units::{LayoutSize, LayoutVector2D};
|
||||
use webrender_api::units::LayoutVector2D;
|
||||
use xml5ever::serialize::TraversalScope::{
|
||||
ChildrenOnly as XmlChildrenOnly, IncludeNode as XmlIncludeNode,
|
||||
};
|
||||
|
@ -166,6 +166,7 @@ use crate::dom::node::{
|
|||
use crate::dom::nodelist::NodeList;
|
||||
use crate::dom::promise::Promise;
|
||||
use crate::dom::raredata::ElementRareData;
|
||||
use crate::dom::scrolling_box::{ScrollingBox, ScrollingBoxSource};
|
||||
use crate::dom::servoparser::ServoParser;
|
||||
use crate::dom::shadowroot::{IsUserAgentWidget, ShadowRoot};
|
||||
use crate::dom::text::Text;
|
||||
|
@ -274,60 +275,6 @@ impl FromStr for AdjacentPosition {
|
|||
}
|
||||
}
|
||||
|
||||
/// Represents a scrolling box that can be either an element or the viewport
|
||||
/// <https://drafts.csswg.org/cssom-view/#scrolling-box>
|
||||
enum ScrollingBox {
|
||||
Element(DomRoot<Element>),
|
||||
Viewport(DomRoot<Document>),
|
||||
}
|
||||
|
||||
impl ScrollingBox {
|
||||
fn scroll_position(&self) -> LayoutVector2D {
|
||||
match self {
|
||||
ScrollingBox::Element(element) => element
|
||||
.owner_window()
|
||||
.scroll_offset_query(element.upcast::<Node>()),
|
||||
ScrollingBox::Viewport(document) => document.window().scroll_offset(),
|
||||
}
|
||||
}
|
||||
|
||||
fn size(&self) -> LayoutSize {
|
||||
match self {
|
||||
ScrollingBox::Element(element) => element.client_rect().size.to_f32().cast_unit(),
|
||||
ScrollingBox::Viewport(document) => {
|
||||
document.window().viewport_details().size.cast_unit()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn parent(&self) -> Option<ScrollingBox> {
|
||||
match self {
|
||||
ScrollingBox::Element(element) => element.scrolling_box(),
|
||||
ScrollingBox::Viewport(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn node(&self) -> &Node {
|
||||
match self {
|
||||
ScrollingBox::Element(element) => element.upcast(),
|
||||
ScrollingBox::Viewport(document) => document.upcast(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn scroll_to(&self, position: LayoutVector2D, behavior: ScrollBehavior) {
|
||||
match self {
|
||||
ScrollingBox::Element(element) => {
|
||||
element
|
||||
.owner_window()
|
||||
.scroll_an_element(element, position.x, position.y, behavior);
|
||||
},
|
||||
ScrollingBox::Viewport(document) => {
|
||||
document.window().scroll(position.x, position.y, behavior);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Element methods
|
||||
//
|
||||
|
@ -889,16 +836,19 @@ impl Element {
|
|||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
fn scrolling_box(&self) -> Option<ScrollingBox> {
|
||||
pub(crate) fn scrolling_box(&self, flags: ScrollContainerQueryFlags) -> Option<ScrollingBox> {
|
||||
self.owner_window()
|
||||
.scroll_container_query(self.upcast(), ScrollContainerQueryType::ForScrollIntoView)
|
||||
.scroll_container_query(self.upcast(), flags)
|
||||
.and_then(|response| match response {
|
||||
ScrollContainerResponse::Viewport => {
|
||||
Some(ScrollingBox::Viewport(self.owner_document()))
|
||||
},
|
||||
ScrollContainerResponse::Element(parent_node_address) => {
|
||||
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::Element(DomRoot::downcast(node)?))
|
||||
Some(ScrollingBox::new(ScrollingBoxSource::Element(
|
||||
DomRoot::downcast(node)?,
|
||||
axes_overflow,
|
||||
)))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -913,7 +863,7 @@ impl Element {
|
|||
) {
|
||||
// Step 1: For each ancestor element or viewport that establishes a scrolling box `scrolling
|
||||
// box`, in order of innermost to outermost scrolling box, run these substeps:
|
||||
let mut parent_scrolling_box = self.scrolling_box();
|
||||
let mut parent_scrolling_box = self.scrolling_box(ScrollContainerQueryFlags::empty());
|
||||
while let Some(scrolling_box) = parent_scrolling_box {
|
||||
parent_scrolling_box = scrolling_box.parent();
|
||||
|
||||
|
@ -987,9 +937,10 @@ impl Element {
|
|||
// to follow it using our own geometry types.
|
||||
//
|
||||
// TODO: This makes the code below wrong for the purposes of writing modes.
|
||||
let (adjusted_element_top_left, adjusted_element_bottom_right) = match scrolling_box {
|
||||
ScrollingBox::Viewport(_) => (target_top_left, target_bottom_right),
|
||||
ScrollingBox::Element(scrolling_element) => {
|
||||
let (adjusted_element_top_left, adjusted_element_bottom_right) =
|
||||
match scrolling_box.target() {
|
||||
ScrollingBoxSource::Viewport(_) => (target_top_left, target_bottom_right),
|
||||
ScrollingBoxSource::Element(scrolling_element, _) => {
|
||||
let scrolling_padding_rect_top_left = scrolling_element
|
||||
.upcast::<Node>()
|
||||
.padding_box()
|
||||
|
@ -5062,7 +5013,7 @@ impl Element {
|
|||
.inspect(|states| states.for_each_state(callback));
|
||||
}
|
||||
|
||||
fn client_rect(&self) -> Rect<i32> {
|
||||
pub(crate) fn client_rect(&self) -> Rect<i32> {
|
||||
let doc = self.node.owner_doc();
|
||||
|
||||
if let Some(rect) = self
|
||||
|
|
|
@ -9,7 +9,7 @@ use std::rc::Rc;
|
|||
use dom_struct::dom_struct;
|
||||
use html5ever::{LocalName, Prefix, QualName, local_name, ns};
|
||||
use js::rust::HandleObject;
|
||||
use layout_api::{QueryMsg, ScrollContainerQueryType, ScrollContainerResponse};
|
||||
use layout_api::{QueryMsg, ScrollContainerQueryFlags, ScrollContainerResponse};
|
||||
use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods;
|
||||
use style::attr::AttrValue;
|
||||
use stylo_dom::ElementState;
|
||||
|
@ -450,10 +450,10 @@ impl HTMLElementMethods<crate::DomTypeHolder> for HTMLElement {
|
|||
#[allow(unsafe_code)]
|
||||
fn GetScrollParent(&self) -> Option<DomRoot<Element>> {
|
||||
self.owner_window()
|
||||
.scroll_container_query(self.upcast(), ScrollContainerQueryType::ForScrollParent)
|
||||
.scroll_container_query(self.upcast(), ScrollContainerQueryFlags::ForScrollParent)
|
||||
.and_then(|response| match response {
|
||||
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) };
|
||||
DomRoot::downcast(node)
|
||||
},
|
||||
|
|
|
@ -428,6 +428,7 @@ pub(crate) mod resizeobserverentry;
|
|||
pub(crate) mod resizeobserversize;
|
||||
pub(crate) mod response;
|
||||
pub(crate) mod screen;
|
||||
mod scrolling_box;
|
||||
pub(crate) mod securitypolicyviolationevent;
|
||||
pub(crate) mod selection;
|
||||
#[allow(dead_code)]
|
||||
|
|
|
@ -121,6 +121,7 @@ use crate::dom::shadowroot::{IsUserAgentWidget, LayoutShadowRootHelpers, ShadowR
|
|||
use crate::dom::stylesheetlist::StyleSheetListOwner;
|
||||
use crate::dom::svgsvgelement::{LayoutSVGSVGElementHelpers, SVGSVGElement};
|
||||
use crate::dom::text::Text;
|
||||
use crate::dom::types::KeyboardEvent;
|
||||
use crate::dom::virtualmethods::{VirtualMethods, vtable_for};
|
||||
use crate::dom::window::Window;
|
||||
use crate::script_runtime::CanGc;
|
||||
|
@ -4084,6 +4085,14 @@ impl VirtualMethods for Node {
|
|||
self.ranges().drain_to_parent(context, self);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_event(&self, event: &Event, _: CanGc) {
|
||||
if let Some(event) = event.downcast::<KeyboardEvent>() {
|
||||
self.owner_document()
|
||||
.event_handler()
|
||||
.run_default_keyboard_event_handler(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A summary of the changes that happened to a node.
|
||||
|
|
147
components/script/dom/scrolling_box.rs
Normal file
147
components/script/dom/scrolling_box.rs
Normal file
|
@ -0,0 +1,147 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use std::cell::Cell;
|
||||
|
||||
use layout_api::{AxesOverflow, ScrollContainerQueryFlags};
|
||||
use script_bindings::codegen::GenericBindings::WindowBinding::ScrollBehavior;
|
||||
use script_bindings::inheritance::Castable;
|
||||
use script_bindings::root::DomRoot;
|
||||
use style::values::computed::Overflow;
|
||||
use webrender_api::units::{LayoutSize, LayoutVector2D};
|
||||
|
||||
use crate::dom::node::{Node, NodeTraits};
|
||||
use crate::dom::types::{Document, Element};
|
||||
|
||||
pub(crate) struct ScrollingBox {
|
||||
target: ScrollingBoxSource,
|
||||
cached_content_size: Cell<Option<LayoutSize>>,
|
||||
cached_size: Cell<Option<LayoutSize>>,
|
||||
}
|
||||
|
||||
/// Represents a scrolling box that can be either an element or the viewport
|
||||
/// <https://drafts.csswg.org/cssom-view/#scrolling-box>
|
||||
pub(crate) enum ScrollingBoxSource {
|
||||
Element(DomRoot<Element>, AxesOverflow),
|
||||
Viewport(DomRoot<Document>),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) enum ScrollingBoxAxis {
|
||||
X,
|
||||
Y,
|
||||
}
|
||||
|
||||
impl ScrollingBox {
|
||||
pub(crate) fn new(target: ScrollingBoxSource) -> Self {
|
||||
Self {
|
||||
target,
|
||||
cached_content_size: Default::default(),
|
||||
cached_size: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn target(&self) -> &ScrollingBoxSource {
|
||||
&self.target
|
||||
}
|
||||
|
||||
pub(crate) fn is_viewport(&self) -> bool {
|
||||
matches!(self.target, ScrollingBoxSource::Viewport(..))
|
||||
}
|
||||
|
||||
pub(crate) fn scroll_position(&self) -> LayoutVector2D {
|
||||
match &self.target {
|
||||
ScrollingBoxSource::Element(element, _) => element
|
||||
.owner_window()
|
||||
.scroll_offset_query(element.upcast::<Node>()),
|
||||
ScrollingBoxSource::Viewport(document) => document.window().scroll_offset(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn content_size(&self) -> LayoutSize {
|
||||
if let Some(content_size) = self.cached_content_size.get() {
|
||||
return content_size;
|
||||
}
|
||||
|
||||
let (document, node_to_query) = match &self.target {
|
||||
ScrollingBoxSource::Element(element, _) => {
|
||||
(element.owner_document(), Some(element.upcast()))
|
||||
},
|
||||
ScrollingBoxSource::Viewport(document) => (document.clone(), None),
|
||||
};
|
||||
|
||||
let content_size = document
|
||||
.window()
|
||||
.scrolling_area_query(node_to_query)
|
||||
.size
|
||||
.to_f32()
|
||||
.cast_unit();
|
||||
self.cached_content_size.set(Some(content_size));
|
||||
content_size
|
||||
}
|
||||
|
||||
pub(crate) fn size(&self) -> LayoutSize {
|
||||
if let Some(size) = self.cached_size.get() {
|
||||
return size;
|
||||
}
|
||||
|
||||
let size = match &self.target {
|
||||
ScrollingBoxSource::Element(element, _) => {
|
||||
element.client_rect().size.to_f32().cast_unit()
|
||||
},
|
||||
ScrollingBoxSource::Viewport(document) => {
|
||||
document.window().viewport_details().size.cast_unit()
|
||||
},
|
||||
};
|
||||
self.cached_size.set(Some(size));
|
||||
size
|
||||
}
|
||||
|
||||
pub(crate) fn parent(&self) -> Option<ScrollingBox> {
|
||||
match &self.target {
|
||||
ScrollingBoxSource::Element(element, _) => {
|
||||
element.scrolling_box(ScrollContainerQueryFlags::empty())
|
||||
},
|
||||
ScrollingBoxSource::Viewport(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn node(&self) -> &Node {
|
||||
match &self.target {
|
||||
ScrollingBoxSource::Element(element, _) => element.upcast(),
|
||||
ScrollingBoxSource::Viewport(document) => document.upcast(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn scroll_to(&self, position: LayoutVector2D, behavior: ScrollBehavior) {
|
||||
match &self.target {
|
||||
ScrollingBoxSource::Element(element, _) => {
|
||||
element
|
||||
.owner_window()
|
||||
.scroll_an_element(element, position.x, position.y, behavior);
|
||||
},
|
||||
ScrollingBoxSource::Viewport(document) => {
|
||||
document.window().scroll(position.x, position.y, behavior);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
ScrollingBoxAxis::X => axes_overflow.x,
|
||||
ScrollingBoxAxis::Y => axes_overflow.x,
|
||||
};
|
||||
if !overflow.is_scrollable() || overflow == Overflow::Hidden {
|
||||
return false;
|
||||
}
|
||||
match axis {
|
||||
ScrollingBoxAxis::X => self.content_size().width > self.size().width,
|
||||
ScrollingBoxAxis::Y => self.content_size().height > self.size().height,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -57,7 +57,7 @@ use layout_api::{
|
|||
BoxAreaType, ElementsFromPointFlags, ElementsFromPointResult, FragmentType, Layout,
|
||||
LayoutImageDestination, PendingImage, PendingImageState, PendingRasterizationImage, QueryMsg,
|
||||
ReflowGoal, ReflowPhasesRun, ReflowRequest, ReflowRequestRestyle, RestyleReason,
|
||||
ScrollContainerQueryType, ScrollContainerResponse, TrustedNodeAddress,
|
||||
ScrollContainerQueryFlags, ScrollContainerResponse, TrustedNodeAddress,
|
||||
combine_id_with_fragment_type,
|
||||
};
|
||||
use malloc_size_of::MallocSizeOf;
|
||||
|
@ -2629,12 +2629,12 @@ impl Window {
|
|||
pub(crate) fn scroll_container_query(
|
||||
&self,
|
||||
node: &Node,
|
||||
query_type: ScrollContainerQueryType,
|
||||
flags: ScrollContainerQueryFlags,
|
||||
) -> Option<ScrollContainerResponse> {
|
||||
self.layout_reflow(QueryMsg::ScrollParentQuery);
|
||||
self.layout
|
||||
.borrow()
|
||||
.query_scroll_container(node.to_trusted_node_address(), query_type)
|
||||
.query_scroll_container(node.to_trusted_node_address(), flags)
|
||||
}
|
||||
|
||||
pub(crate) fn text_index_query(
|
||||
|
|
|
@ -50,10 +50,11 @@ use style::data::ElementData;
|
|||
use style::dom::OpaqueNode;
|
||||
use style::invalidation::element::restyle_hints::RestyleHint;
|
||||
use style::media_queries::Device;
|
||||
use style::properties::PropertyId;
|
||||
use style::properties::style_structs::Font;
|
||||
use style::properties::{ComputedValues, PropertyId};
|
||||
use style::selector_parser::{PseudoElement, RestyleDamage, Snapshot};
|
||||
use style::stylesheets::{Stylesheet, UrlExtraData};
|
||||
use style::values::computed::Overflow;
|
||||
use style_traits::CSSPixel;
|
||||
use webrender_api::units::{DeviceIntSize, LayoutPoint, LayoutVector2D};
|
||||
use webrender_api::{ExternalScrollId, ImageKey};
|
||||
|
@ -308,7 +309,7 @@ pub trait Layout {
|
|||
fn query_scroll_container(
|
||||
&self,
|
||||
node: TrustedNodeAddress,
|
||||
query_type: ScrollContainerQueryType,
|
||||
flags: ScrollContainerQueryFlags,
|
||||
) -> Option<ScrollContainerResponse>;
|
||||
fn query_resolved_style(
|
||||
&self,
|
||||
|
@ -366,16 +367,44 @@ pub struct OffsetParentResponse {
|
|||
pub rect: Rect<Au>,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(PartialEq)]
|
||||
pub enum ScrollContainerQueryType {
|
||||
ForScrollParent,
|
||||
ForScrollIntoView,
|
||||
pub struct ScrollContainerQueryFlags: u8 {
|
||||
/// Whether or not this query is for the purposes of a `scrollParent` layout query.
|
||||
const ForScrollParent = 1 << 0;
|
||||
/// Whether or not to consider the original element's scroll box for the return value.
|
||||
const Inclusive = 1 << 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct AxesOverflow {
|
||||
pub x: Overflow,
|
||||
pub y: Overflow,
|
||||
}
|
||||
|
||||
impl Default for AxesOverflow {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
x: Overflow::Visible,
|
||||
y: Overflow::Visible,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ComputedValues> for AxesOverflow {
|
||||
fn from(style: &ComputedValues) -> Self {
|
||||
Self {
|
||||
x: style.clone_overflow_x(),
|
||||
y: style.clone_overflow_y(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ScrollContainerResponse {
|
||||
Viewport,
|
||||
Element(UntrustedNodeAddress),
|
||||
Element(UntrustedNodeAddress, AxesOverflow),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
|
|
|
@ -11,14 +11,12 @@ use std::rc::Rc;
|
|||
|
||||
use crossbeam_channel::Receiver;
|
||||
use embedder_traits::webdriver::WebDriverSenders;
|
||||
use euclid::Vector2D;
|
||||
use keyboard_types::{Key, Modifiers, NamedKey, ShortcutMatcher};
|
||||
use keyboard_types::ShortcutMatcher;
|
||||
use log::{error, info};
|
||||
use servo::base::generic_channel::GenericSender;
|
||||
use servo::base::id::WebViewId;
|
||||
use servo::config::pref;
|
||||
use servo::ipc_channel::ipc::IpcSender;
|
||||
use servo::webrender_api::ScrollLocation;
|
||||
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize};
|
||||
use servo::{
|
||||
AllowOrDenyRequest, AuthenticationRequest, FilterPattern, FormControl, GamepadHapticEffectType,
|
||||
|
@ -32,7 +30,7 @@ use super::app::PumpResult;
|
|||
use super::dialog::Dialog;
|
||||
use super::gamepad::GamepadSupport;
|
||||
use super::keyutils::CMD_OR_CONTROL;
|
||||
use super::window_trait::{LINE_HEIGHT, LINE_WIDTH, WindowPortsMethods};
|
||||
use super::window_trait::WindowPortsMethods;
|
||||
use crate::output_image::save_output_image_if_necessary;
|
||||
use crate::prefs::ServoShellPreferences;
|
||||
|
||||
|
@ -414,7 +412,6 @@ impl RunningAppState {
|
|||
|
||||
/// Handle servoshell key bindings that may have been prevented by the page in the focused webview.
|
||||
fn handle_overridable_key_bindings(&self, webview: ::servo::WebView, event: KeyboardEvent) {
|
||||
let origin = webview.rect().min.ceil().to_i32();
|
||||
ShortcutMatcher::from_event(event.event)
|
||||
.shortcut(CMD_OR_CONTROL, '=', || {
|
||||
webview.set_zoom(1.1);
|
||||
|
@ -427,42 +424,6 @@ impl RunningAppState {
|
|||
})
|
||||
.shortcut(CMD_OR_CONTROL, '0', || {
|
||||
webview.reset_zoom();
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::Named(NamedKey::PageDown), || {
|
||||
let scroll_location = ScrollLocation::Delta(Vector2D::new(
|
||||
0.0,
|
||||
self.inner().window.page_height() - 2.0 * LINE_HEIGHT,
|
||||
));
|
||||
webview.notify_scroll_event(scroll_location, origin);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::Named(NamedKey::PageUp), || {
|
||||
let scroll_location = ScrollLocation::Delta(Vector2D::new(
|
||||
0.0,
|
||||
-self.inner().window.page_height() + 2.0 * LINE_HEIGHT,
|
||||
));
|
||||
webview.notify_scroll_event(scroll_location, origin);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::Named(NamedKey::Home), || {
|
||||
webview.notify_scroll_event(ScrollLocation::Start, origin);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::Named(NamedKey::End), || {
|
||||
webview.notify_scroll_event(ScrollLocation::End, origin);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowUp), || {
|
||||
let location = ScrollLocation::Delta(Vector2D::new(0.0, -LINE_HEIGHT));
|
||||
webview.notify_scroll_event(location, origin);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowDown), || {
|
||||
let location = ScrollLocation::Delta(Vector2D::new(0.0, LINE_HEIGHT));
|
||||
webview.notify_scroll_event(location, origin);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowLeft), || {
|
||||
let location = ScrollLocation::Delta(Vector2D::new(-LINE_WIDTH, 0.0));
|
||||
webview.notify_scroll_event(location, origin);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowRight), || {
|
||||
let location = ScrollLocation::Delta(Vector2D::new(LINE_WIDTH, 0.0));
|
||||
webview.notify_scroll_event(location, origin);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -532,12 +532,6 @@ impl WindowPortsMethods for Window {
|
|||
.unwrap_or_else(|| self.device_hidpi_scale_factor())
|
||||
}
|
||||
|
||||
fn page_height(&self) -> f32 {
|
||||
let dpr = self.hidpi_scale_factor();
|
||||
let size = self.winit_window.inner_size();
|
||||
size.height as f32 * dpr.get()
|
||||
}
|
||||
|
||||
fn set_title(&self, title: &str) {
|
||||
self.winit_window.set_title(title);
|
||||
}
|
||||
|
|
|
@ -118,12 +118,6 @@ impl WindowPortsMethods for Window {
|
|||
.unwrap_or_else(|| self.device_hidpi_scale_factor())
|
||||
}
|
||||
|
||||
fn page_height(&self) -> f32 {
|
||||
let height = self.inner_size.get().height;
|
||||
let dpr = self.hidpi_scale_factor();
|
||||
height as f32 * dpr.get()
|
||||
}
|
||||
|
||||
fn set_fullscreen(&self, state: bool) {
|
||||
self.fullscreen.set(state);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ use super::app_state::RunningAppState;
|
|||
// This should vary by zoom level and maybe actual text size (focused or under cursor)
|
||||
pub(crate) const LINE_HEIGHT: f32 = 76.0;
|
||||
pub(crate) const LINE_WIDTH: f32 = 76.0;
|
||||
|
||||
// MouseScrollDelta::PixelDelta is default for MacOS, which is high precision and very slow
|
||||
// in winit. Therefore we use a factor of 4.0 to make it more usable.
|
||||
// See https://github.com/servo/servo/pull/34063#discussion_r2197729507
|
||||
|
@ -31,7 +32,6 @@ pub trait WindowPortsMethods {
|
|||
fn screen_geometry(&self) -> ScreenGeometry;
|
||||
fn device_hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel>;
|
||||
fn hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel>;
|
||||
fn page_height(&self) -> f32;
|
||||
fn get_fullscreen(&self) -> bool;
|
||||
fn handle_winit_event(&self, state: Rc<RunningAppState>, event: winit::event::WindowEvent);
|
||||
fn set_title(&self, _title: &str) {}
|
||||
|
|
9
tests/wpt/mozilla/meta/MANIFEST.json
vendored
9
tests/wpt/mozilla/meta/MANIFEST.json
vendored
|
@ -12748,6 +12748,15 @@
|
|||
{}
|
||||
]
|
||||
],
|
||||
"keyboard-scrolling.html": [
|
||||
"2d9a0c40272d8af49f26de8dc49283df68b2d7b0",
|
||||
[
|
||||
null,
|
||||
{
|
||||
"testdriver": true
|
||||
}
|
||||
]
|
||||
],
|
||||
"matchMedia.html": [
|
||||
"45a7ea268b1ebdba69e947b79d675cc9221428d4",
|
||||
[
|
||||
|
|
195
tests/wpt/mozilla/tests/css/keyboard-scrolling.html
vendored
Normal file
195
tests/wpt/mozilla/tests/css/keyboard-scrolling.html
vendored
Normal file
|
@ -0,0 +1,195 @@
|
|||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>CSS test: Calc expressions with numbers should still serialize as calc()</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;
|
||||
}
|
||||
.scroller, #iframe {
|
||||
position: fixed;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
outline: solid;
|
||||
}
|
||||
.scroller {
|
||||
overflow: scroll;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- This is a DIV with `overflow: scroll` that is not focusable -->
|
||||
<div id="box" class="scroller" style="left: 100px; top: 100px;">
|
||||
<div style="width: 600px; height: 600px; background: blue;">Lorem ipsum dolor sit amet,</div>
|
||||
</div>
|
||||
|
||||
<!-- This is a DIV with `overflow: scroll` that is is focusable due to tabindex. -->
|
||||
<div id="focusableBox" class="scroller" tabindex="1" style="left: 300px; top: 100px;">
|
||||
<div style="width: 600px; height: 600px; background: blue;">Lorem ipsum dolor sit amet,</div>
|
||||
</div>
|
||||
|
||||
<!-- This is a DIV with `overflow: scroll` that should not be keyboard scrollable as its content is smaller than the DIV. -->
|
||||
<div id="boxWithSmallContent" class="scroller" tabindex="1" style="left: 100px; top: 300px;">
|
||||
<div style="width: 100px; height: 100px; background: blue;">Lorem ipsum dolor sit amet,</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. -->
|
||||
<div id="boxWithOverflowHidden" class="scroller" style="overflow: hidden; left: 500px; top: 300px;">
|
||||
<div style="width: 600px; height: 600px; background: blue;">Lorem ipsum dolor sit amet,</div>
|
||||
</div>
|
||||
|
||||
<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 pressKeyAndAssert(end, null, [0, document.documentElement.scrollHeight - innerHeight], "End key scrolls viewport to bottom");
|
||||
await pressKeyAndAssert(home, null, [0, 0], "Home key scrolls viewport to top");
|
||||
await pressKeyAndAssert(arrowDown, null, [0, lineSize], "ArrowDown key scrolls viewport down by a line");
|
||||
await pressKeyAndAssert(arrowDown, null, [0, lineSize * 2], "ArrowDown key scrolls viewport down by a line");
|
||||
await pressKeyAndAssert(arrowUp, null, [0, lineSize], "ArrowUp key scrolls viewport up by a line");
|
||||
await pressKeyAndAssert(arrowUp, null, [0, 0], "ArrowUp key scrolls viewport up by a line");
|
||||
await pressKeyAndAssert(arrowRight, null, [lineSize, 0], "ArrowRight key scrolls viewport right by a line");
|
||||
await pressKeyAndAssert(arrowRight, null, [lineSize * 2, 0], "ArrowRight key scrolls viewport right by a line");
|
||||
await pressKeyAndAssert(arrowLeft, null, [lineSize, 0], "ArrowLeft key scrolls viewport left by a line");
|
||||
await pressKeyAndAssert(arrowLeft, null, [0, 0], "ArrowLeft key scrolls viewport left by a line");
|
||||
await pressKeyAndAssert(pageDown, null, [0, pageSize(innerHeight)], "PageDown key scrolls viewport down by almost a screenful");
|
||||
await pressKeyAndAssert(pageDown, null, [0, pageSize(innerHeight) * 2], "PageDown key scrolls viewport down by almost a screenful");
|
||||
await pressKeyAndAssert(pageUp, null, [0, pageSize(innerHeight)], "PageUp key scrolls viewport up by almost a screenful");
|
||||
await pressKeyAndAssert(pageUp, null, [0, 0], "PageUp key scrolls viewport up by almost a screenful");
|
||||
}, "Keyboard scrolling works in the viewport");
|
||||
|
||||
promise_test(async () => {
|
||||
await test_driver.click(box);
|
||||
|
||||
await pressKeyAndAssert(end, box, [0, box.scrollHeight - box.clientHeight], "End key scrolls #box to bottom");
|
||||
await pressKeyAndAssert(home, box, [0, 0], "Home key scrolls #box to top");
|
||||
await pressKeyAndAssert(arrowDown, box, [0, lineSize], "ArrowDown key scrolls #box down by a line");
|
||||
await pressKeyAndAssert(arrowDown, box, [0, lineSize * 2], "ArrowDown key scrolls #box down by a line");
|
||||
await pressKeyAndAssert(arrowUp, box, [0, lineSize], "ArrowUp key scrolls #box up by a line");
|
||||
await pressKeyAndAssert(arrowUp, box, [0, 0], "ArrowUp key scrolls #box up by a line");
|
||||
await pressKeyAndAssert(arrowRight, box, [lineSize, 0], "ArrowRight key scrolls #box right by a line");
|
||||
await pressKeyAndAssert(arrowRight, box, [lineSize * 2, 0], "ArrowRight key scrolls #box right by a line");
|
||||
await pressKeyAndAssert(arrowLeft, box, [lineSize, 0], "ArrowLeft key scrolls #box left by a line");
|
||||
await pressKeyAndAssert(arrowLeft, box, [0, 0], "ArrowLeft key scrolls #box left by a line");
|
||||
await pressKeyAndAssert(pageDown, box, [0, pageSize(box.clientHeight)], "PageDown key scrolls #box down by almost a screenful");
|
||||
await pressKeyAndAssert(pageDown, box, [0, pageSize(box.clientHeight) * 2], "PageDown key scrolls #box down by almost a screenful");
|
||||
await pressKeyAndAssert(pageUp, box, [0, pageSize(box.clientHeight)], "PageUp key scrolls #box up by almost a screenful");
|
||||
await pressKeyAndAssert(pageUp, box, [0, 0], "PageUp key scrolls #box up by almost a screenful");
|
||||
|
||||
// At the bottom of the DIV, we should not chain up to scrolling the document.
|
||||
let bottom = box.scrollHeight - box.clientHeight;
|
||||
await pressKeyAndAssert(end, box, [0, bottom], "End key scrolls #box to bottom");
|
||||
await pressKeyAndAssert(arrowDown, box, [0, bottom], "ArrowDown should not move the box past the max Y position");
|
||||
await pressKeyAndAssert(arrowDown, box, [0, bottom], "ArrowDown should not move the box past the max Y position");
|
||||
await pressKeyAndAssert(arrowDown, box, [0, bottom], "ArrowDown should not move the box 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, box, [0, 0], "Home key scrolls #box to top");
|
||||
}, "Keyboard scrolling works in #box");
|
||||
|
||||
promise_test(async () => {
|
||||
focusableBox.focus();
|
||||
await pressKeyAndAssert(end, focusableBox, [0, box.scrollHeight - box.clientHeight], "End key scrolls #focusableBox to bottom");
|
||||
await pressKeyAndAssert(home, focusableBox, [0, 0], "Home key scrolls #focusableBox to top");
|
||||
await pressKeyAndAssert(arrowDown, focusableBox, [0, lineSize], "ArrowDown key scrolls #focusableBox down by a line");
|
||||
await pressKeyAndAssert(arrowDown, focusableBox, [0, lineSize * 2], "ArrowDown key scrolls #focusableBox down by a line");
|
||||
await pressKeyAndAssert(arrowUp, focusableBox, [0, lineSize], "ArrowUp key scrolls #focusableBox up by a line");
|
||||
await pressKeyAndAssert(arrowUp, focusableBox, [0, 0], "ArrowUp key scrolls #focusableBox up by a line");
|
||||
await pressKeyAndAssert(arrowRight, focusableBox, [lineSize, 0], "ArrowRight key scrolls #focusableBox right by a line");
|
||||
await pressKeyAndAssert(arrowRight, focusableBox, [lineSize * 2, 0], "ArrowRight key scrolls #focusableBox right by a line");
|
||||
await pressKeyAndAssert(arrowLeft, focusableBox, [lineSize, 0], "ArrowLeft key scrolls #focusableBox left by a line");
|
||||
await pressKeyAndAssert(arrowLeft, focusableBox, [0, 0], "ArrowLeft key scrolls #focusableBox left by a line");
|
||||
await pressKeyAndAssert(pageDown, focusableBox, [0, pageSize(box.clientHeight)], "PageDown key scrolls #focusableBox down by almost a screenful");
|
||||
await pressKeyAndAssert(pageDown, focusableBox, [0, pageSize(box.clientHeight) * 2], "PageDown key scrolls #focusableBox down by almost a screenful");
|
||||
await pressKeyAndAssert(pageUp, focusableBox, [0, pageSize(box.clientHeight)], "PageUp key scrolls #focusableBox up by almost a screenful");
|
||||
await pressKeyAndAssert(pageUp, focusableBox, [0, 0], "PageUp key scrolls #focusableBox up by almost a screenful");
|
||||
focusableBox.blur();
|
||||
}, "Keyboard scrolling works in #focusableBox");
|
||||
|
||||
promise_test(async () => {
|
||||
await test_driver.click(boxWithSmallContent);
|
||||
|
||||
await pressKeyAndAssert(arrowDown, boxWithSmallContent, [0, 0], "Arrow down key should not scroll #boxWithSmallContent");
|
||||
assert_array_equals([scrollX, scrollY], [0, lineSize], "The body should scroll instead of boxWithSmallContent");
|
||||
await pressKeyAndAssert(arrowDown, boxWithSmallContent, [0, 0], "Arrow down key should not scroll #boxWithSmallContent");
|
||||
assert_array_equals([scrollX, scrollY], [0, lineSize * 2], "The body should scroll instead of boxWithSmallContent");
|
||||
|
||||
await pressKeyAndAssert(arrowUp, boxWithSmallContent, [0, 0], "Arrow up key should not scroll #boxWithSmallContent");
|
||||
assert_array_equals([scrollX, scrollY], [0, lineSize], "The body should scroll instead of boxWithSmallContent");
|
||||
await pressKeyAndAssert(arrowUp, boxWithSmallContent, [0, 0], "Arrow up key should not scroll #boxWithSmallContent");
|
||||
assert_array_equals([scrollX, scrollY], [0, 0], "The body should scroll instead of boxWithSmallContent");
|
||||
|
||||
await pressKeyAndAssert(home, null, [0, 0], "Home key scrolls viewport to top");
|
||||
}, "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 () => {
|
||||
await test_driver.click(boxWithOverflowHidden);
|
||||
|
||||
await pressKeyAndAssert(arrowDown, boxWithOverflowHidden, [0, 0], "Arrow down key should not scroll #boxWithOverflowHidden");
|
||||
assert_array_equals([scrollX, scrollY], [0, lineSize], "The body should scroll instead of boxWithOverflowHidden");
|
||||
await pressKeyAndAssert(arrowDown, boxWithOverflowHidden, [0, 0], "Arrow down key should not scroll #boxWithOverflowHidden");
|
||||
assert_array_equals([scrollX, scrollY], [0, lineSize * 2], "The body should scroll instead of boxWithOverflowHidden");
|
||||
|
||||
await pressKeyAndAssert(arrowUp, boxWithOverflowHidden, [0, 0], "Arrow up key should not scroll #boxWithOverflowHidden");
|
||||
assert_array_equals([scrollX, scrollY], [0, lineSize], "The body should scroll instead of boxWithOverflowHidden");
|
||||
await pressKeyAndAssert(arrowUp, boxWithOverflowHidden, [0, 0], "Arrow up key should not scroll #boxWithOverflowHidden");
|
||||
assert_array_equals([scrollX, scrollY], [0, 0], "The body should scroll instead of boxWithOverflowHidden");
|
||||
|
||||
await pressKeyAndAssert(home, null, [0, 0], "Home key scrolls viewport to top");
|
||||
}, "Keyboard scrolling chains past overflow:hidden DIVs");
|
||||
</script>
|
Loading…
Add table
Add a link
Reference in a new issue