Skip to content

Commit

Permalink
[carousel] Pin the selected scroll-marker for targeted scrolls
Browse files Browse the repository at this point in the history
The CSS working group resolved[1] that when a scroll operation is aimed
at an element, i.e. Element.scrollIntoView, the associated
scroll-marker-group should select the active scroll-marker based on the
element which the operation is intending to scroll to. In such cases,
the scroll-marker that should be selected is the scroll-marker
associated with the first scroll target (a scroll target is an element
which generates a scroll-marker) found through a search starting from
the target of the scrollIntoView itself and backwards in tree-order.
As soon as some other type of scroll occurs, e.g. Element.scrollTo, or
a user gesture scroll, the scroll-marker-group should no longer
consider its active marker pinned, i.e. it should be based on the
scroll position.

This patch implements this for elements in general, but not for
::column pseudo elements which may also act as scroll targets since
::column::scroll-marker is allowed. The ::column case needs to be
handled specially as ::column pseudos are not parents of the elements
which are flowed into them in the DOM tree. This will be done in a
follow-up patch.

[1] w3c/csswg-drafts#10738 (comment)

Bug: 380062280
Change-Id: I363e0f055f9791ead0b35f4bbe037db91f299624
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6089232
Reviewed-by: Steve Kobes <skobes@chromium.org>
Commit-Queue: David Awogbemila <awogbemila@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1399197}
  • Loading branch information
David Awogbemila authored and chromium-wpt-export-bot committed Dec 20, 2024
1 parent 3f44436 commit 9bbcdbf
Showing 1 changed file with 247 additions and 0 deletions.
247 changes: 247 additions & 0 deletions css/css-overflow/targeted-scroll-marker-selection.tentative.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<title>CSS Test: scroll tracking for ::scroll-markers whose orignatin elements cannot be scroll-aligned </title>
<link rel="help" href="https://drafts.csswg.org/css-overflow-5/#scroll-container-scroll">
<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>
<script src="/css/css-transitions/support/helper.js"></script>
<script src="/dom/events/scrolling/scroll_support.js"></script>
</head>

<body>
<style>
.wrapper {
display: grid;
justify-content: center;
}

.carousel {
display: grid;
grid-auto-flow: column;
width: 1600px;
height: 512px;
overflow-x: scroll;
scroll-snap-type: x mandatory;
list-style-type: none;
scroll-behavior: smooth;
border: solid 2px grey;
padding-top: 10%;
text-align: center;
counter-set: markeridx -1;

div>.item {
&>.itemchild {

}
scroll-snap-align: center;
height: 80%;
width: 318px;
border: 1px solid;
place-content: center;

&::scroll-marker {
content: counter(markeridx);
counter-increment: markeridx;
align-content: center;
text-align: center;
width: 35px;
height: 35px;
border: 3px solid gray;
border-radius: 50%;
margin: 3px;
background-color: red;
}

&::scroll-marker:target-current {
background-color: green;
}
&::scroll-marker:checked {
background-color: green;
}
}

scroll-marker-group: after;

&::scroll-marker-group {
height: 45px;
display: flex;
align-items: center;
justify-content: center;
border: solid 1px black;
border-radius: 30px;
}
}
</style>
<div class="wrapper" id="wrapper">
<div class="carousel" id="carousel">
<div>
<div class="item" id="item0" tabindex=0>
<div class="itemchild" id="itemchild0">0</div>
</div>
<div class="itemsibling" id="itemsibling0"></div>
</div>
<div>
<div class="item" id="item1" tabindex=0>
<div class="itemchild" id="itemchild1">1</div>
</div>
<div class="itemsibling" id="itemsibling1"></div>
</div>
<div>
<div class="item" id="item2" tabindex=0>
<div class="itemchild" id="itemchild2">2</div>
</div>
<div class="itemsibling" id="itemsibling2"></div>
</div>
<div>
<div class="item" id="item3" tabindex=0>
<div class="itemchild" id="itemchild3">3</div>
</div>
<div class="itemsibling" id="itemsibling3"></div>
</div>
<div>
<div class="item" id="item4" tabindex=0>
<div class="itemchild" id="itemchild4">4</div>
</div>
<div class="itemsibling" id="itemsibling4"></div>
</div>
<div>
<div class="item" id="item5" tabindex=0>
<div class="itemchild" id="itemchild5">5</div>
</div>
<div class="itemsibling" id="itemsibling5"></div>
</div>
<div>
<div class="item" id="item6" tabindex=0>
<div class="itemchild" id="itemchild6">6</div>
</div>
<div class="itemsibling" id="itemsibling6"></div>
</div>
<div>
<div class="item" id="item7" tabindex=0>
<div class="itemchild" id="itemchild7">7</div>
</div>
<div class="itemsibling" id="itemsibling7"></div>
</div>
<div>
<div class="item" id="item8" tabindex=0>
<div class="itemchild" id="itemchild8">8</div>
</div>
<div class="itemsibling" id="itemsibling8"></div>
</div>
<div>
<div class="item" id="item9" tabindex=0>
<div class="itemchild" id="itemchild9">9</div>
</div>
<div class="itemsibling" id="itemsibling9"></div>
</div>
<div>
<div class="item" id="item10" tabindex=0>
<div class="itemchild" id="itemchild10">10</div>
</div>
<div class="itemsibling" id="itemsibling10"></div>
</div>
<div>
<div class="item" id="item11" tabindex=0>
<div class="itemchild" id="itemchild11">11</div>
</div>
<div class="itemsibling" id="itemsibling11"></div>
</div>
<div>
<div class="item" id="item12" tabindex=0>
<div class="itemchild" id="itemchild12">12</div>
</div>
<div class="itemsibling" id="itemsibling12"></div>
</div>
<div>
<div class="item" id="item13" tabindex=0>
<div class="itemchild" id="itemchild13">13</div>
</div>
<div class="itemsibling" id="itemsibling13"></div>
</div>
<div>
<div class="item" id="item14" tabindex=0>
<div class="itemchild" id="itemchild14">14</div>
</div>
<div class="itemsibling" id="itemsibling14"></div>
</div>
<div>
<div class="item" id="item15" tabindex=0>
<div class="itemchild" id="itemchild15">15</div>
</div>
<div class="itemsibling" id="itemsibling15"></div>
</div>
</div>
</div>
<script>

const carousel = document.getElementById("carousel");
const items = document.querySelectorAll(".item");
const wrapper = document.getElementById("wrapper");

RED = "rgb(255, 0, 0)";
GREEN = "rgb(0, 128, 0)";

function verifySelectedMarker(selected_idx) {
for (let idx = 0; idx < items.length; idx++) {
const should_be_selected = idx == selected_idx
let expected_color = should_be_selected ? GREEN : RED;
const color =
getComputedStyle(items[idx], "::scroll-marker").backgroundColor;
assert_equals(color, expected_color,
`marker ${idx} should be ${should_be_selected ? "" : "un"}selected.`);
}
}

const max_scroll_offset = carousel.scrollWidth - carousel.clientWidth;
async function testTargetedHasActiveMarker(test, element, expected_idx) {
// Start from somewhere in the middle, ensuring that scrolling to the
// extremes generates scrollend events.
await waitForScrollReset(test, carousel, max_scroll_offset / 2);
const color =
getComputedStyle(items[expected_idx], "::scroll-marker").backgroundColor;
assert_not_equals(color, GREEN,
`Target item ${expected_idx} is not selected yet.`);
const scrollend_promise = waitForScrollendEventNoTimeout(carousel);
element.scrollIntoView({behavior: "smooth"});
await scrollend_promise;
verifySelectedMarker(expected_idx);
}

promise_test(async(t) => {
// Item 1 cannot be snap-aligned because there is not enough
// room for its scroll container to be aligned to its center.
const target_item = items[1];
await testTargetedHasActiveMarker(t, target_item, 1);
}, "scroll-marker of target (idx 1) of scrollIntoView is selected");

promise_test(async(t) => {
// Item 14 cannot be snap-aligned because there is not enough
// room for its scroll container to be aligned to its center.
const target_item = items[14];
await testTargetedHasActiveMarker(t, target_item, 14);
}, "scroll-marker of target (idx 14) of scrollIntoView is selected");

promise_test(async(t) => {
const item14 = items[14];
const target_item = item14.querySelector(".itemchild");
// Item 14 is the parent. We call scrollIntoView on its child.
await testTargetedHasActiveMarker(t, target_item, 14);
}, "scroll-marker of parent of target of scrollIntoView is selected");

promise_test(async(t) => {
const item14 = items[14];
const target_item = item14.nextElementSibling;
// Item 14 is before itemsibling14 in the DOM. We call scrollIntoView on
// itemsibling14.
await testTargetedHasActiveMarker(t, target_item, 14);
}, "scroll-marker of earlier sibling of target of scrollIntoView is selected");
</script>
</body>

</html>

0 comments on commit 9bbcdbf

Please sign in to comment.