implement the IfNeeded part of scrollIntoViewIfNeeded

This commit is contained in:
Karl Seguin
2026-06-07 12:32:39 +08:00
parent b77d0f8ed1
commit 1ffc84d947
4 changed files with 64 additions and 15 deletions

View File

@@ -125,14 +125,21 @@
const targetY = deep.getBoundingClientRect().y;
testing.expectTrue(targetY > 0);
window.scrollTo(0, 0);
testing.expectEqual(0, window.scrollY);
// scrollIntoView() always brings the element's top to the scroll offset.
window.scrollTo(0, 999999);
deep.scrollIntoView();
testing.expectEqual(targetY, window.scrollY);
// scrollIntoViewIfNeeded behaves the same here (brings the element in view).
window.scrollTo(0, 0);
// scrollIntoViewIfNeeded() is a no-op when the element is already within the
// viewport band [scrollY, scrollY + innerHeight]. Sit just below the
// element's top so a stray scroll would visibly move scrollY.
const inView = Math.max(0, targetY - 1);
window.scrollTo(0, inView);
deep.scrollIntoViewIfNeeded();
testing.expectEqual(inView, window.scrollY);
// ...but it does scroll when the element is outside the band.
window.scrollTo(0, targetY + window.innerHeight + 100);
deep.scrollIntoViewIfNeeded();
testing.expectEqual(targetY, window.scrollY);
}

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<!-- Chrome don't scroll if the body isn't big enough. -->
<body style=height:4000px;width:4000px></body>
<script id=scroll_dedup type=module>
{
const state = await testing.async();
let scrollevt = 0;
document.addEventListener("scroll", () => {
scrollevt++;
});
// First move to a fresh position dispatches a scroll event (async).
window.scrollTo(100, 200);
// Scrolling to the same position must NOT dispatch a second scroll event.
window.scrollTo(100, 200);
window.setTimeout(() => {
state.resolve();
}, 100);
await state.done(() => {
testing.expectEqual(1, scrollevt);
});
}
</script>

View File

@@ -1475,6 +1475,12 @@ pub fn clone(self: *Element, deep: bool, frame: *Frame) !*Node {
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);
}

View File

@@ -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;
@@ -700,6 +704,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,
@@ -711,17 +719,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
@@ -1002,7 +1010,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, .{});