mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
Merge pull request #2659 from rohitsux/feat/element-scroll-into-view
feat(webapi): implement Element.scrollIntoView
This commit is contained in:
@@ -114,3 +114,33 @@
|
||||
testing.expectEqual(rect.height, test1.offsetHeight);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="scrollIntoView">
|
||||
{
|
||||
// Add an element deep in the document so its position is non-zero.
|
||||
const deep = document.createElement('div');
|
||||
deep.id = 'deep';
|
||||
document.body.appendChild(deep);
|
||||
|
||||
const targetY = deep.getBoundingClientRect().y;
|
||||
testing.expectTrue(targetY > 0);
|
||||
|
||||
// scrollIntoView() always brings the element's top to the scroll offset.
|
||||
window.scrollTo(0, 999999);
|
||||
deep.scrollIntoView();
|
||||
testing.expectEqual(targetY, window.scrollY);
|
||||
|
||||
// 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);
|
||||
}
|
||||
</script>
|
||||
|
||||
28
src/browser/tests/window/scroll_dedup.html
Normal file
28
src/browser/tests/window/scroll_dedup.html
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
Reference in New Issue
Block a user