Bug 1483075 - Merge the two scrollbar color properties into scrollbar-color. r=heycam,hiro
authorXidorn Quan <me@upsuper.org>
Wed, 19 Sep 2018 05:33:12 +0000
changeset 437193 9c59c261b6a61020f3eec00e5a5cc5e2d94bef60
parent 437192 3c6e729baaa0fa37fd4514590a3c014a4ef3c44d
child 437194 83af3bf2f96713d4bd095b02b719214d1e7a8e3e
push id69548
push userxquan@mozilla.com
push dateWed, 19 Sep 2018 05:35:58 +0000
treeherderautoland@9c59c261b6a6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersheycam, hiro
bugs1483075
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1483075 - Merge the two scrollbar color properties into scrollbar-color. r=heycam,hiro Differential Revision: https://phabricator.services.mozilla.com/D6115
devtools/client/themes/dark-theme.css
devtools/server/actors/animation-type-longhand.js
devtools/shared/css/generated/properties-db.js
layout/reftests/css-scrollbars/viewport-scrollbar-color-change-ref.html
layout/reftests/css-scrollbars/viewport-scrollbar-color-change.html
layout/style/nsComputedDOMStyle.cpp
layout/style/nsComputedDOMStyle.h
layout/style/test/property_database.js
layout/style/test/test_bug1112014.html
layout/style/test/test_transitions_per_property.html
servo/components/style/properties/gecko.mako.rs
servo/components/style/properties/longhands/inherited_ui.mako.rs
servo/components/style/values/computed/ui.rs
servo/components/style/values/generics/ui.rs
servo/components/style/values/specified/ui.rs
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/css/css-scrollbars/support/viewport-scrollbar-body-frame.html
testing/web-platform/tests/css/css-scrollbars/support/viewport-scrollbar-frame.html
testing/web-platform/tests/css/css-scrollbars/viewport-scrollbar-ref.html
widget/tests/test_scrollbar_colors.html
--- a/devtools/client/themes/dark-theme.css
+++ b/devtools/client/themes/dark-theme.css
@@ -26,18 +26,17 @@ body {
   background-color: var(--theme-selection-background);
   color: var(--theme-selection-color);
 }
 
 :root[platform="win"],
 :root[platform="mac"] {
   /* Set colors for native scrollbars on Windows dark theme */
   /* Other platforms support for scrollbar theming is Bug 1460109 */
-  scrollbar-face-color: var(--theme-body-color-inactive);
-  scrollbar-track-color: var(--theme-splitter-color);
+  scrollbar-color: var(--theme-body-color-inactive) var(--theme-splitter-color);
 }
 
 .theme-selected,
 .CodeMirror-hint-active {
   background-color: var(--theme-selection-background);
   color: var(--theme-selection-color);
 }
 
--- a/devtools/server/actors/animation-type-longhand.js
+++ b/devtools/server/actors/animation-type-longhand.js
@@ -234,18 +234,17 @@ exports.ANIMATION_TYPE_FOR_LONGHANDS = [
     "border-top-color",
     "caret-color",
     "color",
     "column-rule-color",
     "flood-color",
     "-moz-font-smoothing-background-color",
     "lighting-color",
     "outline-color",
-    "scrollbar-face-color",
-    "scrollbar-track-color",
+    "scrollbar-color",
     "stop-color",
     "text-decoration-color",
     "text-emphasis-color",
     "-webkit-text-fill-color",
     "-webkit-text-stroke-color",
   ])],
   ["custom", new Set([
     "background-position-x",
--- a/devtools/shared/css/generated/properties-db.js
+++ b/devtools/shared/css/generated/properties-db.js
@@ -3047,18 +3047,17 @@ exports.CSS_PROPERTIES = {
       "text-rendering",
       "-moz-control-character-visibility",
       "cursor",
       "pointer-events",
       "-moz-user-input",
       "-moz-user-modify",
       "-moz-user-focus",
       "caret-color",
-      "scrollbar-face-color",
-      "scrollbar-track-color",
+      "scrollbar-color",
       "text-anchor",
       "color-interpolation",
       "color-interpolation-filters",
       "fill",
       "fill-opacity",
       "fill-rule",
       "shape-rendering",
       "stroke",
@@ -9360,40 +9359,36 @@ exports.PREFERENCES = [
     "scale",
     "layout.css.individual-transform.enabled"
   ],
   [
     "scroll-snap-coordinate",
     "layout.css.scroll-snap.enabled"
   ],
   [
+    "scrollbar-color",
+    "layout.css.scrollbar-colors.enabled"
+  ],
+  [
     "shape-outside",
     "layout.css.shape-outside.enabled"
   ],
   [
     "translate",
     "layout.css.individual-transform.enabled"
   ],
   [
     "scroll-snap-points-x",
     "layout.css.scroll-snap.enabled"
   ],
   [
     "scroll-snap-points-y",
     "layout.css.scroll-snap.enabled"
   ],
   [
-    "scrollbar-face-color",
-    "layout.css.scrollbar-colors.enabled"
-  ],
-  [
-    "scrollbar-track-color",
-    "layout.css.scrollbar-colors.enabled"
-  ],
-  [
     "scroll-snap-destination",
     "layout.css.scroll-snap.enabled"
   ],
   [
     "shape-margin",
     "layout.css.shape-outside.enabled"
   ],
   [
--- a/layout/reftests/css-scrollbars/viewport-scrollbar-color-change-ref.html
+++ b/layout/reftests/css-scrollbars/viewport-scrollbar-color-change-ref.html
@@ -1,10 +1,9 @@
 <!DOCTYPE html>
 <html>
 <style>
   html {
     overflow: scroll;
-    scrollbar-face-color: green;
-    scrollbar-track-color: green;
+    scrollbar-color: green green;
   }
 </style>
 <div style="width: 200vw; height: 200vh"></div>
--- a/layout/reftests/css-scrollbars/viewport-scrollbar-color-change.html
+++ b/layout/reftests/css-scrollbars/viewport-scrollbar-color-change.html
@@ -1,19 +1,17 @@
 <!DOCTYPE html>
 <html class="reftest-wait">
 <style>
   html {
     overflow: scroll;
-    scrollbar-face-color: green;
-    scrollbar-track-color: green;
+    scrollbar-color: green green;
   }
   html.reftest-wait {
-    scrollbar-face-color: red;
-    scrollbar-track-color: red;
+    scrollbar-color: red red;
   }
 </style>
 <div style="width: 200vw; height: 200vh"></div>
 <script>
   window.addEventListener("MozAfterPaint", function() {
     document.documentElement.classList.remove("reftest-wait");
   });
 </script>
--- a/layout/style/nsComputedDOMStyle.cpp
+++ b/layout/style/nsComputedDOMStyle.cpp
@@ -2977,31 +2977,29 @@ nsComputedDOMStyle::DoGetScrollSnapCoord
       SetValueToPosition(sd->mScrollSnapCoordinate[i], itemList);
       valueList->AppendCSSValue(itemList.forget());
     }
     return valueList.forget();
   }
 }
 
 already_AddRefed<CSSValue>
-nsComputedDOMStyle::DoGetScrollbarFaceColor()
-{
-  RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
-  SetValueForWidgetColor(val, StyleUI()->mScrollbarFaceColor,
-                         StyleAppearance::ScrollbarthumbVertical);
-  return val.forget();
-}
-
-already_AddRefed<CSSValue>
-nsComputedDOMStyle::DoGetScrollbarTrackColor()
-{
-  RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
-  SetValueForWidgetColor(val, StyleUI()->mScrollbarTrackColor,
-                         StyleAppearance::ScrollbarVertical);
-  return val.forget();
+nsComputedDOMStyle::DoGetScrollbarColor()
+{
+  const nsStyleUI* ui = StyleUI();
+  RefPtr<nsDOMCSSValueList> list = GetROCSSValueList(false);
+  auto put = [this, &list](const StyleComplexColor& color,
+                           StyleAppearance type) {
+    RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
+    SetValueForWidgetColor(val, color, type);
+    list->AppendCSSValue(val.forget());
+  };
+  put(ui->mScrollbarFaceColor, StyleAppearance::ScrollbarthumbVertical);
+  put(ui->mScrollbarTrackColor, StyleAppearance::ScrollbarVertical);
+  return list.forget();
 }
 
 already_AddRefed<CSSValue>
 nsComputedDOMStyle::DoGetOutlineWidth()
 {
   RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
 
   const nsStyleOutline* outline = StyleOutline();
--- a/layout/style/nsComputedDOMStyle.h
+++ b/layout/style/nsComputedDOMStyle.h
@@ -407,18 +407,17 @@ private:
   already_AddRefed<CSSValue> DoGetOverscrollBehaviorX();
   already_AddRefed<CSSValue> DoGetOverscrollBehaviorY();
   already_AddRefed<CSSValue> DoGetScrollSnapTypeX();
   already_AddRefed<CSSValue> DoGetScrollSnapTypeY();
   already_AddRefed<CSSValue> DoGetScrollSnapPointsX();
   already_AddRefed<CSSValue> DoGetScrollSnapPointsY();
   already_AddRefed<CSSValue> DoGetScrollSnapDestination();
   already_AddRefed<CSSValue> DoGetScrollSnapCoordinate();
-  already_AddRefed<CSSValue> DoGetScrollbarFaceColor();
-  already_AddRefed<CSSValue> DoGetScrollbarTrackColor();
+  already_AddRefed<CSSValue> DoGetScrollbarColor();
 
   /* User interface properties */
   already_AddRefed<CSSValue> DoGetCaretColor();
   already_AddRefed<CSSValue> DoGetCursor();
   already_AddRefed<CSSValue> DoGetForceBrokenImageIcon();
 
   /* Column properties */
   already_AddRefed<CSSValue> DoGetColumnCount();
--- a/layout/style/test/property_database.js
+++ b/layout/style/test/property_database.js
@@ -8097,31 +8097,23 @@ if (IsCSSPropertyPrefEnabled("layout.css
   gCSSProperties["background-image"].invalid_values.push(
     "-moz-linear-gradient(unset, 10px 10px, from(blue))",
     "-moz-linear-gradient(unset, 10px 10px, blue 0)",
     "-moz-repeating-linear-gradient(unset, 10px 10px, blue 0)",
   );
 }
 
 if (IsCSSPropertyPrefEnabled("layout.css.scrollbar-colors.enabled")) {
-  gCSSProperties["scrollbar-face-color"] = {
-    domProp: "scrollbarFaceColor",
+  gCSSProperties["scrollbar-color"] = {
+    domProp: "scrollbarColor",
     inherited: true,
     type: CSS_TYPE_LONGHAND,
     initial_values: [ "auto" ],
-    other_values: [ "red", "blue", "#ffff00" ],
-    invalid_values: [ "ffff00" ]
-  };
-  gCSSProperties["scrollbar-track-color"] = {
-    domProp: "scrollbarTrackColor",
-    inherited: true,
-    type: CSS_TYPE_LONGHAND,
-    initial_values: [ "auto" ],
-    other_values: [ "red", "blue", "#ffff00" ],
-    invalid_values: [ "ffff00" ]
+    other_values: [ "red green", "blue yellow", "#ffff00 white" ],
+    invalid_values: [ "ffff00 red", "auto red", "red auto", "green" ]
   };
 }
 
 if (IsCSSPropertyPrefEnabled("layout.css.scrollbar-width.enabled")) {
   gCSSProperties["scrollbar-width"] = {
     domProp: "scrollbarWidth",
     inherited: false,
     type: CSS_TYPE_LONGHAND,
--- a/layout/style/test/test_bug1112014.html
+++ b/layout/style/test/test_bug1112014.html
@@ -26,16 +26,19 @@ https://bugzilla.mozilla.org/show_bug.cg
   // override values.
   let overrideValues = {
     "box-shadow": {
       TYPE_COLOR: testValues.TYPE_COLOR + " 2px 2px"
     },
     "-webkit-box-shadow": {
       TYPE_COLOR: testValues.TYPE_COLOR + " 2px 2px"
     },
+    "scrollbar-color": {
+      TYPE_COLOR: testValues.TYPE_COLOR + " " + testValues.TYPE_COLOR,
+    },
     "text-shadow": {
       TYPE_COLOR: testValues.TYPE_COLOR + " 2px 2px"
     },
   };
 
 
   // Ensure that all the TYPE_ constants have a representative
   // test value, to try to ensure that this test is updated
--- a/layout/style/test/test_transitions_per_property.html
+++ b/layout/style/test/test_transitions_per_property.html
@@ -324,25 +324,18 @@ if (IsCSSPropertyPrefEnabled("layout.css
 
 if (IsCSSPropertyPrefEnabled("layout.css.individual-transform.enabled")) {
   supported_properties["rotate"] = [ test_rotate_transition ];
   supported_properties["scale"] = [ test_scale_transition ];
   supported_properties["translate"] = [ test_translate_transition ];
 }
 
 if (IsCSSPropertyPrefEnabled("layout.css.scrollbar-colors.enabled")) {
-  supported_properties["scrollbar-face-color"] = [
-    test_color_transition,
-    test_currentcolor_transition,
-    test_auto_color_transition,
-  ];
-  supported_properties["scrollbar-track-color"] = [
-    test_color_transition,
-    test_currentcolor_transition,
-    test_auto_color_transition,
+  supported_properties["scrollbar-color"] = [
+    test_scrollbar_color_transition,
   ];
 }
 
 // Logical properties.
 for (const logical_side of ["inline-start", "inline-end", "block-start", "block-end"]) {
   supported_properties["border-" + logical_side + "-color"] = supported_properties["border-top-color"];
   supported_properties["border-" + logical_side + "-width"] = supported_properties["border-top-width"];
   supported_properties["margin-" + logical_side] = supported_properties["margin-top"];
@@ -1482,130 +1475,177 @@ function test_angle_transition(prop) {
      `angle property ${prop}: computed value before transition`);
   div.style.transitionProperty = prop;
   div.style[prop] = '145deg';
   is(cs[prop], '70deg',
      `angle property ${prop}: interpolation of angles`);
   check_distance(prop, '45deg', '70deg', '145deg');
 }
 
-function test_color_transition(prop, get_color=(x => x), is_shorthand=false) {
+function get_color_options(options) {
+  let {
+    get_color = x => x,
+    set_color = x => x,
+    is_shorthand = false,
+  } = options;
+  return { get_color, set_color, is_shorthand };
+}
+
+function test_color_transition(prop, options={}) {
+  let { get_color, set_color, is_shorthand } = get_color_options(options);
+
   div.style.setProperty("transition-property", "none", "");
-  div.style.setProperty(prop, "rgb(255, 28, 0)", "");
+  div.style.setProperty(prop, set_color("rgb(255, 28, 0)"), "");
   is(get_color(cs.getPropertyValue(prop)), "rgb(255, 28, 0)",
      "color-valued property " + prop + ": computed value before transition");
   div.style.setProperty("transition-property", prop, "");
-  div.style.setProperty(prop, "rgb(75, 84, 128)", "");
+  div.style.setProperty(prop, set_color("rgb(75, 84, 128)"), "");
   is(get_color(cs.getPropertyValue(prop)), "rgb(210, 42, 32)",
      "color-valued property " + prop + ": interpolation of colors");
 
   if (!is_shorthand) {
-    check_distance(prop, "rgb(255, 28, 0)", "rgb(210, 42, 32)",
-                         "rgb(75, 84, 128)");
+    check_distance(prop, set_color("rgb(255, 28, 0)"),
+                   set_color("rgb(210, 42, 32)"),
+                   set_color("rgb(75, 84, 128)"));
   }
 
   div.style.setProperty("transition-timing-function", FUNC_NEGATIVE, "");
   div.style.setProperty("transition-property", "none", "");
-  div.style.setProperty(prop, "rgb(0, 255, 0)", "");
-  var vals = cs.getPropertyValue(prop).match(/rgb\(([^, ]*), ([^, ]*), ([^, ]*)\)/);
+  div.style.setProperty(prop, set_color("rgb(0, 255, 0)"), "");
+  var color = get_color(cs.getPropertyValue(prop));
+  var vals = color.match(/rgb\(([^, ]*), ([^, ]*), ([^, ]*)\)/);
   is(vals.length, 4,
      "color-valued property " + prop + ": flush before clamping test (length)");
   is(vals[1], "0",
      "color-valued property " + prop + ": flush before clamping test (red)");
   is(vals[2], "255",
      "color-valued property " + prop + ": flush before clamping test (green)");
   is(vals[3], "0",
      "color-valued property " + prop + ": flush before clamping test (blue)");
   div.style.setProperty("transition-property", prop, "");
-  div.style.setProperty(prop, "rgb(255, 0, 128)", "");
+  div.style.setProperty(prop, set_color("rgb(255, 0, 128)"), "");
   // FIXME: Once we support non-sRGB colors, these tests will need fixing.
-  vals = cs.getPropertyValue(prop).match(/rgb\(([^, ]*), ([^, ]*), ([^, ]*)\)/);
+  color = get_color(cs.getPropertyValue(prop));
+  vals = color.match(/rgb\(([^, ]*), ([^, ]*), ([^, ]*)\)/);
   is(vals.length, 4,
      "color-valued property " + prop + ": clamping of negatives (length)");
   is(vals[1], "0",
      "color-valued property " + prop + ": clamping of negatives (red)");
   is(vals[2], "255",
      "color-valued property " + prop + ": clamping of above-range (green)");
   is(vals[3], "0",
      "color-valued property " + prop + ": clamping of negatives (blue)");
   div.style.setProperty("transition-timing-function", "linear", "");
 }
 
-function test_currentcolor_transition(prop, get_color=(x => x), is_shorthand=false) {
+function test_currentcolor_transition(prop, options={}) {
+  let { get_color, set_color } = get_color_options(options);
+
   const msg_prefix = `color-valued property ${prop}: `;
   div.style.setProperty("transition-property", "none", "");
   (prop == "color" ? div.parentNode : div).style.
     setProperty("color", "rgb(128, 0, 0)", "");
-  div.style.setProperty(prop, "rgb(0, 0, 128)", "");
+  div.style.setProperty(prop, set_color("rgb(0, 0, 128)"), "");
   is(get_color(cs.getPropertyValue(prop)), "rgb(0, 0, 128)",
      msg_prefix + "computed value before transition");
   div.style.setProperty("transition-property", prop, "");
-  div.style.setProperty(prop, "currentcolor", "");
+  div.style.setProperty(prop, set_color("currentcolor"), "");
   is(get_color(cs.getPropertyValue(prop)), "rgb(32, 0, 96)",
      msg_prefix + "interpolation of rgb color and currentcolor");
 
   if (prop != "color") {
     div.style.setProperty("transition-property", "none", "");
     div.style.setProperty("color", "rgb(128, 0, 0)", "");
-    div.style.setProperty(prop, "rgb(0, 0, 128)", "");
+    div.style.setProperty(prop, set_color("rgb(0, 0, 128)"), "");
     is(get_color(cs.getPropertyValue(prop)), "rgb(0, 0, 128)",
        msg_prefix + "computed value before transition");
     div.style.setProperty("transition-property", `color, ${prop}`, "");
     div.style.setProperty("color", "rgb(0, 128, 0)", "");
-    div.style.setProperty(prop, "currentcolor", "");
+    div.style.setProperty(prop, set_color("currentcolor"), "");
     is(cs.getPropertyValue("color"), "rgb(96, 32, 0)",
        "interpolation of rgb color property");
     is(get_color(cs.getPropertyValue(prop)), "rgb(24, 8, 96)",
        msg_prefix + "interpolation of rgb color and interpolated currentcolor");
   }
 
   div.style.setProperty("transition-property", "none", "");
   (prop == "color" ? div.parentNode : div).style.
     setProperty("color", "rgba(128, 0, 0, 0.6)", "");
-  div.style.setProperty(prop, "rgba(0, 0, 128, 0.8)", "");
+  div.style.setProperty(prop, set_color("rgba(0, 0, 128, 0.8)"), "");
   is(get_color(cs.getPropertyValue(prop)), "rgba(0, 0, 128, 0.8)",
      msg_prefix + "computed value before transition");
   div.style.setProperty("transition-property", prop, "");
-  div.style.setProperty(prop, "currentcolor", "");
+  div.style.setProperty(prop, set_color("currentcolor"), "");
   is(get_color(cs.getPropertyValue(prop)), "rgba(26, 0, 102, 0.75)",
      msg_prefix + "interpolation of rgba color and currentcolor");
 
   // It is not possible to check distance, because there is a hidden
   // dimension for ratio of currentcolor.
 
   (prop == "color" ? div.parentNode : div).style.removeProperty("color");
 }
 
-function test_auto_color_transition(prop, get_color=(x => x), is_shorthand=false) {
+function test_auto_color_transition(prop, options={}) {
+  let { get_color, set_color } = get_color_options(options);
+
   const msg_prefix = `color-valued property ${prop}: `;
   const test_color = "rgb(51, 102, 153)";
   div.style.setProperty("transition-property", "none", "");
   div.style.setProperty(prop, "auto", "");
   let used_value_of_auto = get_color(cs.getPropertyValue(prop));
   isnot(used_value_of_auto, test_color,
         msg_prefix + "ensure used auto value is different than our test color");
 
   div.style.setProperty("transition-property", prop, "");
-  div.style.setProperty(prop, test_color, "");
+  div.style.setProperty(prop, set_color(test_color), "");
   is(get_color(cs.getPropertyValue(prop)), test_color,
      msg_prefix + "not interpolatable between auto and rgb color");
 }
 
 function get_color_from_shorthand_value(value) {
   var m = value.match(/rgba?\([^, ]*, [^, ]*, [^, ]*(?:, [^, ]*)?\)/);
   isnot(m, null, "shorthand property value should contain color");
   return m[0];
 }
 
 function test_color_shorthand_transition(prop) {
-  test_color_transition(prop, get_color_from_shorthand_value, true);
+  test_color_transition(prop, {
+    get_color: get_color_from_shorthand_value,
+    is_shorthand: true,
+  });
 }
 
 function test_currentcolor_shorthand_transition(prop) {
-  test_currentcolor_transition(prop, get_color_from_shorthand_value, true);
+  test_currentcolor_transition(prop, {
+    get_color: get_color_from_shorthand_value,
+    is_shorthand: true,
+  });
+}
+
+function test_scrollbar_color_transition(prop) {
+  function split_colors(value) {
+    const colors = value.match(/^(rgba?\(.+?\)) (rgba?\(.+?\))$/);
+    isnot(colors, null, "scrollbar-color should consist of two colors");
+    return { thumb: colors[1], track: colors[2] };
+  }
+  const TEST_FUNCS = [
+    test_color_transition,
+    test_currentcolor_transition,
+    test_auto_color_transition,
+  ];
+  for (let test_func of TEST_FUNCS) {
+    test_func(prop, {
+      get_color: value => split_colors(value).thumb,
+      set_color: value => value + " blue",
+    });
+    test_func(prop, {
+      get_color: value => split_colors(value).track,
+      set_color: value => "blue " + value,
+    });
+  }
 }
 
 function test_shape_or_url_equals(computedValStr, expected)
 {
   // Check simple case "none"
   if (computedValStr == "none" && computedValStr == expected[0]) {
     return true;
   }
--- a/servo/components/style/properties/gecko.mako.rs
+++ b/servo/components/style/properties/gecko.mako.rs
@@ -5299,17 +5299,17 @@ clip-path
 
     pub fn clone_color(&self) -> longhands::color::computed_value::T {
         let color = ${get_gecko_property("mColor")} as u32;
         convert_nscolor_to_rgba(color)
     }
 </%self:impl_trait>
 
 <%self:impl_trait style_struct_name="InheritedUI"
-                  skip_longhands="cursor">
+                  skip_longhands="cursor scrollbar-color">
     pub fn set_cursor(&mut self, v: longhands::cursor::computed_value::T) {
         use style_traits::cursor::CursorKind;
 
         self.gecko.mCursor = match v.keyword {
             CursorKind::Auto => structs::NS_STYLE_CURSOR_AUTO,
             CursorKind::None => structs::NS_STYLE_CURSOR_NONE,
             CursorKind::Default => structs::NS_STYLE_CURSOR_DEFAULT,
             CursorKind::Pointer => structs::NS_STYLE_CURSOR_POINTER,
@@ -5445,16 +5445,58 @@ clip-path
                     None
                 };
 
             CursorImage { url, hotspot }
         }).collect::<Vec<_>>().into_boxed_slice();
 
         longhands::cursor::computed_value::T { images, keyword }
     }
+
+    pub fn set_scrollbar_color(&mut self, v: longhands::scrollbar_color::computed_value::T) {
+        use gecko_bindings::structs::StyleComplexColor;
+        use values::generics::ui::ScrollbarColor;
+        match v {
+            ScrollbarColor::Auto => {
+                self.gecko.mScrollbarFaceColor = StyleComplexColor::auto();
+                self.gecko.mScrollbarTrackColor = StyleComplexColor::auto();
+            }
+            ScrollbarColor::Colors { thumb, track } => {
+                self.gecko.mScrollbarFaceColor = thumb.into();
+                self.gecko.mScrollbarTrackColor = track.into();
+            }
+        }
+    }
+
+    pub fn copy_scrollbar_color_from(&mut self, other: &Self) {
+        self.gecko.mScrollbarFaceColor = other.gecko.mScrollbarFaceColor;
+        self.gecko.mScrollbarTrackColor = other.gecko.mScrollbarTrackColor;
+    }
+
+    pub fn reset_scrollbar_color(&mut self, other: &Self) {
+        self.copy_scrollbar_color_from(other);
+    }
+
+    pub fn clone_scrollbar_color(&self) -> longhands::scrollbar_color::computed_value::T {
+        use gecko_bindings::structs::StyleComplexColor_Tag as Tag;
+        use values::generics::ui::ScrollbarColor;
+        debug_assert!(
+            (self.gecko.mScrollbarFaceColor.mTag == Tag::eAuto) ==
+            (self.gecko.mScrollbarTrackColor.mTag == Tag::eAuto),
+            "Whether the two colors are `auto` should match",
+        );
+        if self.gecko.mScrollbarFaceColor.mTag == Tag::eAuto {
+            ScrollbarColor::Auto
+        } else {
+            ScrollbarColor::Colors {
+                thumb: self.gecko.mScrollbarFaceColor.into(),
+                track: self.gecko.mScrollbarTrackColor.into(),
+            }
+        }
+    }
 </%self:impl_trait>
 
 <%self:impl_trait style_struct_name="Column"
                   skip_longhands="column-count column-rule-width">
 
     #[allow(unused_unsafe)]
     pub fn set_column_count(&mut self, v: longhands::column_count::computed_value::T) {
         use gecko_bindings::structs::{nsStyleColumn_kColumnCountAuto, nsStyleColumn_kMaxColumnCount};
--- a/servo/components/style/properties/longhands/inherited_ui.mako.rs
+++ b/servo/components/style/properties/longhands/inherited_ui.mako.rs
@@ -62,29 +62,20 @@
     "ColorOrAuto",
     "Either::Second(Auto)",
     spec="https://drafts.csswg.org/css-ui/#caret-color",
     animation_value_type="AnimatedCaretColor",
     ignored_when_colors_disabled=True,
     products="gecko",
 )}
 
-// Only scrollbar-face-color and scrollbar-track-color are added here.
-// These two are the only common parts of scrollbar among Windows and
-// macOS. We may or may not want to provide other properties to allow
-// finer-grain control.
-//
-// NOTE The syntax in spec is currently `normal | <color>`, but I think
-//      reusing `auto | <color>` as `caret-color` makes more sense. See
-//      https://github.com/w3c/csswg-drafts/issues/2660
-% for part in ["face", "track"]:
 ${helpers.predefined_type(
-    "scrollbar-%s-color" % part,
-    "ColorOrAuto",
-    "Either::Second(Auto)",
-    spec="https://drafts.csswg.org/css-scrollbars-1/#scrollbar-color-properties",
+    "scrollbar-color",
+    "ui::ScrollbarColor",
+    "Default::default()",
+    spec="https://drafts.csswg.org/css-scrollbars-1/#scrollbar-color",
     gecko_pref="layout.css.scrollbar-colors.enabled",
-    animation_value_type="ColorOrAuto",
+    animation_value_type="ScrollbarColor",
+    boxed=True,
     ignored_when_colors_disabled=True,
     enabled_in="chrome",
     products="gecko",
 )}
-% endfor
--- a/servo/components/style/values/computed/ui.rs
+++ b/servo/components/style/values/computed/ui.rs
@@ -15,8 +15,11 @@ pub use values::specified::ui::MozForceB
 /// auto | <color>
 pub type ColorOrAuto = Either<Color, Auto>;
 
 /// A computed value for the `cursor` property.
 pub type Cursor = generics::Cursor<CursorImage>;
 
 /// A computed value for item of `image cursors`.
 pub type CursorImage = generics::CursorImage<ComputedImageUrl, Number>;
+
+/// A computed value for `scrollbar-color` property.
+pub type ScrollbarColor = generics::ScrollbarColor<Color>;
--- a/servo/components/style/values/generics/ui.rs
+++ b/servo/components/style/values/generics/ui.rs
@@ -62,8 +62,32 @@ impl<ImageUrl: ToCss, Number: ToCss> ToC
             dest.write_str(" ")?;
             x.to_css(dest)?;
             dest.write_str(" ")?;
             y.to_css(dest)?;
         }
         Ok(())
     }
 }
+
+/// A generic value for `scrollbar-color` property.
+///
+/// https://drafts.csswg.org/css-scrollbars-1/#scrollbar-color
+#[derive(Animate, Clone, ComputeSquaredDistance, Copy, Debug, MallocSizeOf, PartialEq,
+         SpecifiedValueInfo, ToAnimatedValue, ToAnimatedZero, ToComputedValue, ToCss)]
+pub enum ScrollbarColor<Color> {
+    /// `auto`
+    Auto,
+    /// `<color>{2}`
+    Colors {
+        /// First `<color>`, for color of the scrollbar thumb.
+        thumb: Color,
+        /// Second `<color>`, for color of the scrollbar track.
+        track: Color,
+    }
+}
+
+impl<Color> Default for ScrollbarColor<Color> {
+    #[inline]
+    fn default() -> Self {
+        ScrollbarColor::Auto
+    }
+}
--- a/servo/components/style/values/specified/ui.rs
+++ b/servo/components/style/values/specified/ui.rs
@@ -117,8 +117,26 @@ impl From<MozForceBrokenImageIcon> for u
     fn from(v: MozForceBrokenImageIcon) -> u8 {
         if v.0 {
             1
         } else {
             0
         }
     }
 }
+
+/// A specified value for `scrollbar-color` property
+pub type ScrollbarColor = generics::ScrollbarColor<Color>;
+
+impl Parse for ScrollbarColor {
+    fn parse<'i, 't>(
+        context: &ParserContext,
+        input: &mut Parser<'i, 't>,
+    ) -> Result<Self, ParseError<'i>> {
+        if input.try(|i| i.expect_ident_matching("auto")).is_ok() {
+            return Ok(generics::ScrollbarColor::Auto);
+        }
+        Ok(generics::ScrollbarColor::Colors {
+            thumb: Color::parse(context, input)?,
+            track: Color::parse(context, input)?,
+        })
+    }
+}
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -553626,21 +553626,21 @@
    "9209d5d1f4174d1e22258c0571db89b51f4f3a59",
    "support"
   ],
   "css/css-scrollbars/scrollbar-width-keywords.html": [
    "94ccd6ef6d550c83c97ba9525135cc309cc35842",
    "testharness"
   ],
   "css/css-scrollbars/support/viewport-scrollbar-body-frame.html": [
-   "74c02f994e3103daabfaf40a7fb84d3169451d2c",
+   "8dacffefb62a8d348963a51c67e73887ca9b977b",
    "support"
   ],
   "css/css-scrollbars/support/viewport-scrollbar-frame.html": [
-   "95101024b4f27e93eb2c61c52df70845ae5842bf",
+   "04a4a2fa2d507a0715feaa1e6ba222c85648b77a",
    "support"
   ],
   "css/css-scrollbars/textarea-scrollbar-width-none-ref.html": [
    "9f505dd7f1d121c0bd0af0131b51f536225326b1",
    "support"
   ],
   "css/css-scrollbars/textarea-scrollbar-width-none.html": [
    "dcfaf5b6270ee0e0092dc795d33bb01af0b9a695",
@@ -553650,17 +553650,17 @@
    "4e07903e2ca1e5d0378845c8cc4754dc82b1ebf8",
    "support"
   ],
   "css/css-scrollbars/viewport-scrollbar-body.html": [
    "a1b3aee871a918dc4e3f92e669d6631c0944cd18",
    "reftest"
   ],
   "css/css-scrollbars/viewport-scrollbar-ref.html": [
-   "42463d309b994760c4e91d07fd9e4cb551964617",
+   "d6df3b49e56d6cfab633dcd5264c79811cfaf0f7",
    "support"
   ],
   "css/css-scrollbars/viewport-scrollbar.html": [
    "500da7a9f9584428f27624d1e877085c382c0429",
    "reftest"
   ],
   "css/css-shadow-parts/META.yml": [
    "6cce42664a64778f548edb53ec86ae61531812f0",
--- a/testing/web-platform/tests/css/css-scrollbars/support/viewport-scrollbar-body-frame.html
+++ b/testing/web-platform/tests/css/css-scrollbars/support/viewport-scrollbar-body-frame.html
@@ -1,14 +1,13 @@
 <!DOCTYPE html>
 <meta charset="UTF-8">
 <style>
 body {
-  scrollbar-track-color: blue;
-  scrollbar-face-color: yellow;
+  scrollbar-color: yellow blue;
 }
 html, body {
   margin: 0;
   padding: 0;
 }
 #inner {
   width: 400px;
   height: 400px;
--- a/testing/web-platform/tests/css/css-scrollbars/support/viewport-scrollbar-frame.html
+++ b/testing/web-platform/tests/css/css-scrollbars/support/viewport-scrollbar-frame.html
@@ -1,14 +1,13 @@
 <!DOCTYPE html>
 <meta charset="UTF-8">
 <style>
 html {
-  scrollbar-track-color: blue;
-  scrollbar-face-color: yellow;
+  scrollbar-color: yellow blue;
 }
 html, body {
   margin: 0;
   padding: 0;
 }
 #inner {
   width: 400px;
   height: 400px;
--- a/testing/web-platform/tests/css/css-scrollbars/viewport-scrollbar-ref.html
+++ b/testing/web-platform/tests/css/css-scrollbars/viewport-scrollbar-ref.html
@@ -4,18 +4,17 @@
 <link rel="author" title="Xidorn Quan" href="https://www.upsuper.org">
 <link rel="author" title="Mozilla" href="https://www.mozilla.org">
 <link rel="help" href="https://drafts.csswg.org/css-scrollbars-1/#scrollbar-color-properties">
 <style>
 #outer {
   border: 1px solid black;
   width: 200px; height: 200px;
   overflow: scroll;
-  scrollbar-track-color: blue;
-  scrollbar-face-color: yellow;
+  scrollbar-color: yellow blue;
 }
 #inner {
   width: 400px; height: 400px;
 }
 </style>
 <p>Test passes if the scrollbars in the following box are styled:</p>
 <div id="outer">
   <div id="inner">
--- a/widget/tests/test_scrollbar_colors.html
+++ b/widget/tests/test_scrollbar_colors.html
@@ -61,20 +61,17 @@ if (outerRect.width == outer.clientWidth
     outerRect.height == outer.clientHeight) {
   ok(true, "Using overlay scrollbar, skip this test");
 } else {
   SimpleTest.waitForExplicitFinish();
   SpecialPowers.pushPrefEnv({
     "set": [["layout.css.scrollbar-colors.enabled", true]],
   }, function() {
     document.querySelector('#style').textContent = `
-      .outer {
-        scrollbar-face-color: blue;
-        scrollbar-track-color: cyan;
-      }
+      .outer { scrollbar-color: blue cyan; }
     `;
 
     let canvas = snapshotRect(window, outerRect);
     let stats = countPixels(canvas);
     let references;
     if (navigator.platform.startsWith("Win")) {
       references = WIN_REFERENCES;
     } else if (navigator.platform.startsWith("Mac")) {