diff --git a/src/browser/tests/element/position.html b/src/browser/tests/element/position.html index 178951dc..1f9f0319 100644 --- a/src/browser/tests/element/position.html +++ b/src/browser/tests/element/position.html @@ -114,3 +114,33 @@ testing.expectEqual(rect.height, test1.offsetHeight); } + + diff --git a/src/browser/tests/window/scroll_dedup.html b/src/browser/tests/window/scroll_dedup.html new file mode 100644 index 00000000..a9a1552c --- /dev/null +++ b/src/browser/tests/window/scroll_dedup.html @@ -0,0 +1,28 @@ + + + +
+ + diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 03573a88..6765475a 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -1478,16 +1478,28 @@ pub fn clone(self: *Element, deep: bool, frame: *Frame) !*Node { return node; } -pub fn scrollIntoViewIfNeeded(_: *const Element, center_if_needed: ?bool) void { +pub fn scrollIntoViewIfNeeded(self: *Element, center_if_needed: ?bool, frame: *Frame) void { _ = center_if_needed; + const y = calculateDocumentPosition(self.asNode()); + const scroll_y: f64 = @floatFromInt(frame.window.getScrollY()); + const viewport_height: f64 = @floatFromInt(frame.window.getInnerHeight()); + if (y >= scroll_y and y <= scroll_y + viewport_height) { + return; + } + self.scrollIntoView(null, frame); } const ScrollIntoViewOpts = union { align_to_top: bool, obj: js.Object, }; -pub fn scrollIntoView(_: *const Element, opts: ?ScrollIntoViewOpts) void { +pub fn scrollIntoView(self: *Element, opts: ?ScrollIntoViewOpts, frame: *Frame) void { _ = opts; + // Scroll the window so the element's top is brought into the viewport. + // Positions come from the faux-layout document position (top = preorder + // depth-scaled y), the same source getBoundingClientRect uses. + const y = calculateDocumentPosition(self.asNode()); + frame.window.scrollTo(.{ .x = 0 }, @intFromFloat(@max(0, y)), frame) catch {}; } pub fn format(self: *Element, writer: *std.Io.Writer) !void { diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index fb268b64..9071d313 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -54,6 +54,10 @@ const Notification = @import("../../Notification.zig"); const log = lp.log; const IS_DEBUG = builtin.mode == .Debug; +// Faux-layout viewport height. Exposed as window.innerHeight and used to decide +// whether an element is already within view (e.g. scrollIntoViewIfNeeded). +const DEFAULT_INNER_HEIGHT: u32 = 1080; + const Allocator = std.mem.Allocator; const Execution = js.Execution; @@ -701,6 +705,10 @@ pub fn getScrollY(self: *const Window) u32 { return self._scroll_pos.y; } +pub fn getInnerHeight(_: *const Window) u32 { + return DEFAULT_INNER_HEIGHT; +} + const ScrollToOpts = union(enum) { x: i32, opts: Opts, @@ -712,17 +720,17 @@ const ScrollToOpts = union(enum) { }; }; pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, frame: *Frame) !void { - switch (opts) { - .x => |x| { - self._scroll_pos.x = @intCast(@max(x, 0)); - self._scroll_pos.y = @intCast(@max(0, y orelse 0)); - }, - .opts => |o| { - self._scroll_pos.x = @intCast(@max(0, o.left)); - self._scroll_pos.y = @intCast(@max(0, o.top)); - }, + const new_x: u32, const new_y: u32 = switch (opts) { + .x => |x| .{ @intCast(@max(x, 0)), @intCast(@max(0, y orelse 0)) }, + .opts => |o| .{ @intCast(@max(0, o.left)), @intCast(@max(0, o.top)) }, + }; + + if (new_x == self._scroll_pos.x and new_y == self._scroll_pos.y) { + return; } + self._scroll_pos.x = new_x; + self._scroll_pos.y = new_y; self._scroll_pos.state = .scroll; // We dispatch scroll event asynchronously after 10ms. So we can throttle @@ -1003,7 +1011,7 @@ pub const JsApi = struct { // [Replaceable] (CSSOM-View): writable so assignment overwrites rather than throws. pub const innerWidth = bridge.property(1920, .{ .template = false, .readonly = false }); - pub const innerHeight = bridge.property(1080, .{ .template = false, .readonly = false }); + pub const innerHeight = bridge.property(DEFAULT_INNER_HEIGHT, .{ .template = false, .readonly = false }); pub const devicePixelRatio = bridge.property(1, .{ .template = false, .readonly = false }); pub const opener = bridge.accessor(Window.getOpener, null, .{});