layout: Clamp sticky positioning offset bounds by zero (#39443)

Sticky positioning tries to keep an element visible within the nearest
scrollport. However, the element can't be offset to go beyond its
containing block. We implement this as offset bounds.

The problem was that, if the element would already be overflowing its
containing block before applying the sticky positioning, then we were
forcing it to move inside the containing block. That was wrong, and is
solved by flooring or ceiling the offset bounds by zero.

Testing: Adding new tests

Signed-off-by: Oriol Brufau <obrufau@igalia.com>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Oriol Brufau 2025-09-23 09:22:10 +02:00 committed by GitHub
parent b73538a676
commit 16d2e030eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 189 additions and 4 deletions

View file

@ -1558,12 +1558,12 @@ impl BoxFragment {
// This is the minimum negative offset and then the maximum positive offset. We just // This is the minimum negative offset and then the maximum positive offset. We just
// specify every edge, but if the corresponding margin is None, that offset has no effect. // specify every edge, but if the corresponding margin is None, that offset has no effect.
let vertical_offset_bounds = wr::StickyOffsetBounds::new( let vertical_offset_bounds = wr::StickyOffsetBounds::new(
containing_block_rect.min.y - frame_rect.min.y, (containing_block_rect.min.y - frame_rect.min.y).min(0.),
containing_block_rect.max.y - frame_rect.max.y, (containing_block_rect.max.y - frame_rect.max.y).max(0.),
); );
let horizontal_offset_bounds = wr::StickyOffsetBounds::new( let horizontal_offset_bounds = wr::StickyOffsetBounds::new(
containing_block_rect.min.x - frame_rect.min.x, (containing_block_rect.min.x - frame_rect.min.x).min(0.),
containing_block_rect.max.x - frame_rect.max.x, (containing_block_rect.max.x - frame_rect.max.x).max(0.),
); );
let margins = SideOffsets2D::new( let margins = SideOffsets2D::new(

View file

@ -238242,6 +238242,58 @@
{} {}
] ]
], ],
"position-sticky-bottom-004.html": [
"7439c2795360da76b1448ab098942737cbcc8680",
[
null,
[
[
"/css/reference/ref-filled-green-100px-square.xht",
"=="
]
],
{}
]
],
"position-sticky-bottom-005.html": [
"3d226132a854b0d63c59298e19a1d1f8dbf03536",
[
null,
[
[
"/css/reference/ref-filled-green-100px-square.xht",
"=="
]
],
{}
]
],
"position-sticky-bottom-006.html": [
"675ade1fcbae7a7c5e5749ecfb3d2313f22ae9d1",
[
null,
[
[
"/css/reference/ref-filled-green-100px-square.xht",
"=="
]
],
{}
]
],
"position-sticky-bottom-007.html": [
"f2648f1e003ac16d29c49be5f61f27a2097fe1b9",
[
null,
[
[
"/css/reference/ref-filled-green-100px-square.xht",
"=="
]
],
{}
]
],
"position-sticky-change-top.html": [ "position-sticky-change-top.html": [
"6a327ccd567818c2267d808a890796724384726e", "6a327ccd567818c2267d808a890796724384726e",
[ [

View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<title>CSS Position Test: sticky element with bottom offset</title>
<link rel="author" title="Oriol Brufau" href="obrufau@igalia.com">
<link rel="help" href="https://drafts.csswg.org/css-position/#stickypos-insets">
<link rel="match" href="../../reference/ref-filled-green-100px-square.xht">
<meta name="assert" content="
The static position of #sticky is outside (below) of its containing block.
Sticky positioning doesn't force it to move up into the containing block.
">
<style>
#scroll-container {
overflow: hidden;
width: 100px;
height: 100px;
background: red;
}
#sticky {
position: sticky;
bottom: 0;
height: 100px;
background: green;
}
</style>
<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
<div id="scroll-container">
<div style="height: 100px; margin-top: -100px">
<div style="height: 100px"></div>
<div id="sticky"></div>
</div>
</div>

View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<title>CSS Position Test: sticky element with bottom offset</title>
<link rel="author" title="Oriol Brufau" href="obrufau@igalia.com">
<link rel="help" href="https://drafts.csswg.org/css-position/#stickypos-insets">
<link rel="match" href="../../reference/ref-filled-green-100px-square.xht">
<meta name="assert" content="
The static position of #sticky is outside (above) of its containing block.
Sticky positioning doesn't force it to move down into the containing block.
">
<style>
#scroll-container {
overflow: hidden;
width: 100px;
height: 100px;
background: red;
}
#sticky {
position: sticky;
bottom: 0;
height: 100px;
background: green;
}
</style>
<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
<div id="scroll-container">
<div style="padding-top: 200px">
<div style="margin-top: -200px"></div>
<div id="sticky"></div>
</div>
</div>

View file

@ -0,0 +1,31 @@
<!DOCTYPE html>
<title>CSS Position Test: sticky element with bottom offset</title>
<link rel="author" title="Oriol Brufau" href="obrufau@igalia.com">
<link rel="help" href="https://drafts.csswg.org/css-position/#stickypos-insets">
<link rel="match" href="../../reference/ref-filled-green-100px-square.xht">
<meta name="assert" content="
The static position of #sticky is outside (above) of its nearest scrollport.
Sticky positioning doesn't force it to move down into the scrollport.
">
<style>
#scroll-container {
overflow: hidden;
width: 100px;
height: 100px;
background: green;
}
#sticky {
position: sticky;
bottom: 0;
height: 100px;
background: red;
}
</style>
<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
<div id="scroll-container">
<div style="margin-top: -100px"></div>
<div id="sticky"></div>
</div>

View file

@ -0,0 +1,36 @@
<!DOCTYPE html>
<title>CSS Position Test: sticky element with bottom offset</title>
<link rel="author" title="Oriol Brufau" href="obrufau@igalia.com">
<link rel="help" href="https://drafts.csswg.org/css-position/#stickypos-insets">
<link rel="match" href="../../reference/ref-filled-green-100px-square.xht">
<meta name="assert" content="
When the scroll container is scrolled to the top, then #sticky is visible.
If we scroll 100px down, then #sticky should update its sticky offsets
to still be visible.
">
<style>
#scroll-container {
overflow: hidden;
width: 100px;
height: 100px;
background: red;
}
#sticky {
position: sticky;
bottom: 0;
height: 100px;
background: green;
}
</style>
<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
<div id="scroll-container">
<div style="height: 500px"></div>
<div id="sticky"></div>
</div>
<script>
scroller.scrollTop = 100;
</script>