[css-scroll-snap] Allow style changes to invalidate snap data

All snap container data was only updated and re-snapped after layout
changes. However, this invalidation needs to sometimes happen after
style changes too (such as changing 'scroll-snap-align').

Now, PaintLayerScrollableArea has flags for indicating that it needs
to update its snap container data, which are set during certain style
changes and layout changes. Then all snap container data that has the
flag set will be updated, and attempt to re-snap if any data changes.

This also prevents us from updating snap containers that haven't
changed.

The follow-up for this is to invalidate snap containers when changing
the transform property of their content.

Bug: 984794,1028316
Change-Id: I432500df98fdbe63e3b7eed3ba43e05465f9d873
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1962567
Commit-Queue: Yi Gu <yigu@chromium.org>
Reviewed-by: Majid Valipour <majidvp@chromium.org>
Reviewed-by: Yi Gu <yigu@chromium.org>
Reviewed-by: David Bokan <bokan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#729345}
diff --git a/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align.html b/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align.html
new file mode 100644
index 0000000..3be68fe
--- /dev/null
+++ b/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align.html
@@ -0,0 +1,121 @@
+<!DOCTYPE html>
+<title>
+  Updating the snap alignment of a snap container's content should make the snap
+  container resnap accordingly.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+  position: absolute;
+  margin: 0;
+}
+
+#scroller {
+  height: 200px;
+  width: 200px;
+  overflow: hidden;
+  scroll-snap-type: both mandatory;
+}
+
+#initial-target {
+  width: 300px;
+  height: 300px;
+  top: 100px;
+  left: 100px;
+  background-color: green;
+  scroll-snap-align: start;
+}
+
+#other-target {
+  width: 300px;
+  height: 300px;
+  top: 300px;
+  left: 300px;
+  background-color: red;
+  scroll-snap-align: start;
+}
+
+.area {
+  width: 2000px;
+  height: 2000px;
+}
+
+.snap-area {
+  scroll-snap-align: start !important;
+}
+</style>
+
+<div id="scroller">
+  <div class="area"></div>
+  <div id="initial-target"></div>
+  <div id="other-target"></div>
+</div>
+
+<script>
+const initial_target = document.getElementById("initial-target");
+const other_target = document.getElementById("other-target");
+const scroller = document.getElementById("scroller");
+
+function cleanup() {
+  initial_target.style.setProperty("scroll-snap-align", "start");
+  other_target.style.setProperty("scroll-snap-align", "start");
+  initial_target.removeAttribute("class");
+}
+
+test(t => {
+  t.add_cleanup(cleanup);
+  scroller.scrollTo(0,0);
+  assert_equals(scroller.scrollTop, 100);
+  assert_equals(scroller.scrollLeft, 100);
+
+  initial_target.style.setProperty("scroll-snap-align", "end");
+  // target.top + target.height - scroller.height
+  assert_equals(scroller.scrollTop, 200);
+  assert_equals(scroller.scrollLeft, 200);
+}, "Changing the current target's snap alignment should make the scroller"
++ " resnap to it even if another snap position is closer to the current offset");
+
+test(t => {
+  t.add_cleanup(cleanup);
+  scroller.scrollTo(0,0);
+  assert_equals(scroller.scrollTop, 100);
+  assert_equals(scroller.scrollLeft, 100);
+
+  initial_target.style.setProperty("scroll-snap-align", "none");
+  assert_equals(scroller.scrollTop, 300);
+  assert_equals(scroller.scrollLeft, 300);
+}, "Removing the current target's snap alignment should make the scroller"
++ " resnap to a new snap area.");
+
+test(t => {
+  t.add_cleanup(cleanup);
+  initial_target.style.setProperty("scroll-snap-align", "none");
+  other_target.style.setProperty("scroll-snap-align", "none");
+
+  scroller.scrollTo(0,0);
+  assert_equals(scroller.scrollTop, 0);
+  assert_equals(scroller.scrollLeft, 0);
+
+  initial_target.style.setProperty("scroll-snap-align", "start");
+  assert_equals(scroller.scrollTop, 100);
+  assert_equals(scroller.scrollLeft, 100);
+}, "Changing an element snap alignment from none to start should make the"
++ "scroller resnap.");
+
+test(t => {
+  t.add_cleanup(cleanup);
+  initial_target.style.setProperty("scroll-snap-align", "none");
+  other_target.style.setProperty("scroll-snap-align", "none");
+
+  scroller.scrollTo(0,0);
+  assert_equals(scroller.scrollTop, 0);
+  assert_equals(scroller.scrollLeft, 0);
+
+  initial_target.classList.add("snap-area");
+  assert_equals(scroller.scrollTop, 100);
+  assert_equals(scroller.scrollLeft, 100);
+}, "Changing an element snap alignment from none to start by adding a class"
++ " should make the scroller resnap.");
+</script>
diff --git a/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type.html b/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type.html
new file mode 100644
index 0000000..70774b3
--- /dev/null
+++ b/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<title>
+  Updating the scroll-snap-type of a snap container should make it resnap accordingly.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+  position: absolute;
+  margin: 0;
+}
+
+#scroller {
+  height: 500px;
+  width: 500px;
+  overflow: hidden;
+  scroll-snap-type: none;
+}
+
+#y-target {
+  width: 300px;
+  height: 300px;
+  top: 100px;
+  left: 0;
+  background-color: green;
+  scroll-snap-align: start none;
+}
+
+#x-target {
+  width: 300px;
+  height: 300px;
+  top: 0;
+  left: 100px;
+  background-color: red;
+  scroll-snap-align: none start;
+}
+
+.area {
+  width: 2000px;
+  height: 2000px;
+}
+</style>
+
+<div id="scroller">
+  <div class="area"></div>
+  <div id="x-target"></div>
+  <div id="y-target"></div>
+</div>
+
+<script>
+const x_target = document.getElementById("x_target");
+const y_target = document.getElementById("y_target");
+const scroller = document.getElementById("scroller");
+
+function cleanup() {
+  scroller.style.setProperty("scroll-snap-type", "none");
+}
+
+test(t => {
+  t.add_cleanup(cleanup);
+  scroller.scrollTo(0,0);
+  assert_equals(scroller.scrollTop, 0);
+  assert_equals(scroller.scrollLeft, 0);
+
+  scroller.style.setProperty("scroll-snap-type", "y mandatory");
+  assert_equals(scroller.scrollTop, 100);
+  assert_equals(scroller.scrollLeft, 0);
+}, "Changing the scroller's snap type to y should make it resnap on the y-axis.");
+
+test(t => {
+  t.add_cleanup(cleanup);
+  scroller.scrollTo(0,0);
+  assert_equals(scroller.scrollTop, 0);
+  assert_equals(scroller.scrollLeft, 0);
+
+  scroller.style.setProperty("scroll-snap-type", "x mandatory");
+  assert_equals(scroller.scrollLeft, 100);
+  assert_equals(scroller.scrollTop, 0);
+}, "Changing the scroller's snap type to x should make it resnap on the x-axis.");
+
+
+test(t => {
+  t.add_cleanup(cleanup);
+  scroller.scrollTo(0,0);
+  assert_equals(scroller.scrollTop, 0);
+  assert_equals(scroller.scrollLeft, 0);
+
+  scroller.style.setProperty("scroll-snap-type", "x mandatory");
+  assert_equals(scroller.scrollLeft, 100);
+  assert_equals(scroller.scrollTop, 0);
+
+  scroller.style.setProperty("scroll-snap-type", "y mandatory");
+  assert_equals(scroller.scrollTop, 100);
+}, "Changing the scroller's snap type axis should make it resnap.");
+</script>