Bug 1449532 - Part II, Use Web Animation API to animate video control transition
Web Animation API should give us deterministic timing when the transition ends or aborts.
Additional clean-ups:
- Make sure hidden status is always set/get from the hidden property,
instead of the hidden attribute.
- Remove the unused isControlBarHidden property.
- controlsSpacer no longer has a background color (removed in
bug 1374007),
therefore it no longer needs a transition and there is no need to test
its state with the test added in
bug 1319301.
- Fix a logic error at hideByAdjustment property, revealed by the changed
transition timing, in which adjustControlSize() would show the controlBar
set hidden by the transition.
MozReview-Commit-ID: DB2cgQcUEXi
--- a/toolkit/content/moz.build
+++ b/toolkit/content/moz.build
@@ -177,18 +177,16 @@ with Files('tests/reftests/*videocontrol
BUG_COMPONENT = ('Toolkit', 'Video/Audio Controls')
with Files('tests/unit/**'):
BUG_COMPONENT = ('Toolkit', 'General')
with Files('tests/widgets/*audiocontrols*'):
BUG_COMPONENT = ('Toolkit', 'Video/Audio Controls')
-with Files('tests/widgets/*1319301*'):
- BUG_COMPONENT = ('Toolkit', 'Video/Audio Controls')
with Files('tests/widgets/*898940*'):
BUG_COMPONENT = ('Toolkit', 'Video/Audio Controls')
with Files('tests/widgets/*contextmenu*'):
BUG_COMPONENT = ('Firefox', 'Menus')
with Files('tests/widgets/*editor*'):
BUG_COMPONENT = ('Core', 'XP Toolkit/Widgets: XUL')
--- a/toolkit/content/tests/widgets/mochitest.ini
+++ b/toolkit/content/tests/widgets/mochitest.ini
@@ -36,13 +36,12 @@ skip-if = toolkit == 'android'
[test_videocontrols_audio_direction.html]
[test_videocontrols_jsdisabled.html]
skip-if = toolkit == 'android' # bug 1272646
[test_videocontrols_standalone.html]
skip-if = toolkit == 'android' # bug 1075573
[test_videocontrols_video_direction.html]
skip-if = os == 'win'
[test_videocontrols_video_noaudio.html]
-[test_bug1319301.html]
[test_bug898940.html]
[test_videocontrols_error.html]
[test_videocontrols_orientation.html]
run-if = toolkit == 'android'
deleted file mode 100644
--- a/toolkit/content/tests/widgets/test_bug1319301.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
- <title>Video controls test - bug 1319301</title>
- <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
- <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
- <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
- <script type="text/javascript" src="head.js"></script>
- <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
-</head>
-<body>
-<p id="display"></p>
-
-<div id="content">
- <video id="video" controls preload="auto"></video>
-</div>
-
-<pre id="test">
-<script clas="testbody" type="application/javascript">
- const video = document.getElementById("video");
- const controlsSpacer = getAnonElementWithinVideoByAttribute(video, "anonid", "controlsSpacer");
-
- add_task(async function setup() {
- await new Promise(resolve => window.addEventListener("load", resolve));
- await SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]});
- });
-
- add_task(async function play_fadeout() {
- isnot(controlsSpacer.getAttribute("fadeout"), "true", "controlsSpacer should not fadeout before playing");
-
- await new Promise(resolve => {
- video.addEventListener("canplaythrough", video.play);
- video.addEventListener("play", () => SimpleTest.executeSoon(resolve));
- video.src = "seek_with_sound.ogg";
- });
-
- is(controlsSpacer.getAttribute("fadeout"), "true", "controlsSpacer should fadeout once video starts playing");
- });
-</script>
-</pre>
-</body>
-</html>
--- a/toolkit/content/tests/widgets/test_videocontrols_vtt.html
+++ b/toolkit/content/tests/widgets/test_videocontrols_vtt.html
@@ -28,83 +28,83 @@
await SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]});
await new Promise(resolve => {
video.src = "seek_with_sound.ogg";
video.addEventListener("loadedmetadata", resolve);
});
});
add_task(async function check_inital_state() {
- is(ccBtn.getAttribute("hidden"), "true", "CC button should hide");
+ ok(ccBtn.hidden, "CC button should hide");
});
add_task(async function check_unsupported_type_added() {
video.addTextTrack("descriptions", "English", "en");
video.addTextTrack("chapters", "English", "en");
video.addTextTrack("metadata", "English", "en");
await new Promise(SimpleTest.executeSoon);
- is(ccBtn.getAttribute("hidden"), "true", "CC button should hide if no supported tracks provided");
+ ok(ccBtn.hidden, "CC button should hide if no supported tracks provided");
});
add_task(async function check_cc_button_present() {
const sub = video.addTextTrack("subtitles", "English", "en");
sub.mode = "disabled";
await new Promise(SimpleTest.executeSoon);
- is(ccBtn.hasAttribute("hidden"), false, "CC button should show");
+ ok(!ccBtn.hidden, "CC button should show");
is(ccBtn.hasAttribute("enabled"), false, "CC button should be disabled");
});
add_task(async function check_cc_button_be_enabled() {
const subtitle = video.addTextTrack("subtitles", "English", "en");
subtitle.mode = "showing";
await new Promise(SimpleTest.executeSoon);
- is(ccBtn.getAttribute("enabled"), "true", "CC button should be enabled");
+ ok(ccBtn.hasAttribute("enabled"), "CC button should be enabled");
subtitle.mode = "disabled";
});
add_task(async function check_cpations_type() {
const caption = video.addTextTrack("captions", "English", "en");
caption.mode = "showing";
await new Promise(SimpleTest.executeSoon);
- is(ccBtn.getAttribute("enabled"), "true", "CC button should be enabled");
+ ok(ccBtn.hasAttribute("enabled"), "CC button should be enabled");
});
add_task(async function check_track_ui_state() {
synthesizeMouseAtCenter(ccBtn, {});
await new Promise(SimpleTest.executeSoon);
- is(ttList.hasAttribute("hidden"), false, "Texttrack menu should show up");
- is(ttList.lastChild.getAttribute("on"), "true", "The last added item should be highlighted");
+ ok(!ttList.hidden, "Texttrack menu should show up");
+ ok(ttList.lastChild.hasAttribute("on"), "The last added item should be highlighted");
});
add_task(async function check_select_texttrack() {
const tt = ttList.children[1];
- isnot(tt.getAttribute("on"), "true", "Item should be off before click");
+ ok(!tt.hasAttribute("on"), "Item should be off before click");
synthesizeMouseAtCenter(tt, {});
await new Promise(SimpleTest.executeSoon);
- is(tt.getAttribute("on"), "true", "Selected item should be enabled");
- is(ttList.getAttribute("hidden"), "true", "Should hide texttrack menu once clicked on an item");
+ ok(tt.hasAttribute("on"), "Selected item should be enabled");
+ ok(ttList.hidden, "Should hide texttrack menu once clicked on an item");
});
add_task(async function check_change_texttrack_mode() {
const tts = [...video.textTracks];
tts.forEach(tt => tt.mode = "hidden");
await new Promise(SimpleTest.executeSoon);
- is(ccBtn.hasAttribute("enabled"), false, "CC button should be disabled");
+ ok(!ccBtn.hasAttribute("enabled"), "CC button should be disabled");
// enable the last text track.
tts[tts.length - 1].mode = "showing";
await new Promise(SimpleTest.executeSoon);
- is(ccBtn.hasAttribute("enabled"), true, "CC button should be enabled");
- is(ttList.lastChild.getAttribute("on"), "true", "The last item should be highlighted");
+ ok(ccBtn.hasAttribute("enabled"), "CC button should be enabled");
+ ok(ttList.lastChild.hasAttribute("on"), "The last item should be highlighted");
});
</script>
</pre>
</body>
</html>
--- a/toolkit/content/widgets/videocontrols.xml
+++ b/toolkit/content/widgets/videocontrols.xml
@@ -133,22 +133,16 @@
this.video.style.height = this.controlBarMinHeight + "px";
this.video.style.width = "66%";
} else {
this.video.style.removeProperty("height");
this.video.style.removeProperty("width");
}
},
- get isControlBarHidden() {
- return this.controlBar.hidden ||
- this.controlBar.hideByAdjustment ||
- this.controlBar.getAttribute("fadeout") === "true";
- },
-
suppressError: false,
setupStatusFader(immediate) {
// Since the play button will be showing, we don't want to
// show the throbber behind it. The throbber here will
// only show if needed after the play button has been pressed.
if (!this.clickToPlay.hidden) {
this.startFadeOut(this.statusOverlay, true);
@@ -293,31 +287,46 @@
modifier: {
value: "",
writable: true
},
isWanted: {
value: true,
writable: true
},
- hideByAdjustment: {
+ hidden: {
set: (v) => {
- if (v) {
- control.setAttribute("hidden", "true");
- } else {
- control.removeAttribute("hidden");
- }
-
+ control._isHiddenExplicitly = v;
+ control._updateHiddenAttribute();
+ },
+ get: () => control.hasAttribute("hidden")
+ },
+ hiddenByAdjustment: {
+ set: (v) => {
control._isHiddenByAdjustment = v;
+ control._updateHiddenAttribute();
},
get: () => control._isHiddenByAdjustment
},
_isHiddenByAdjustment: {
value: false,
writable: true
+ },
+ _isHiddenExplicitly: {
+ value: false,
+ writable: true
+ },
+ _updateHiddenAttribute: {
+ value: () => {
+ if (control._isHiddenExplicitly || control._isHiddenByAdjustment) {
+ control.setAttribute("hidden", "");
+ } else {
+ control.removeAttribute("hidden");
+ }
+ }
}
});
}
this.adjustControlSize();
// Can only update the volume controls once we've computed
// _volumeControlWidth, since the volume slider implementation
// depends on it.
@@ -337,17 +346,16 @@
// (Note: the |controls| attribute is already handled via layout/style/html.css)
var shouldShow = !this.dynamicControls ||
(this.video.paused &&
!this.video.autoplay);
// Hide the overlay if the video time is non-zero or if an error occurred to workaround bug 718107.
let shouldClickToPlayShow = shouldShow && !this.isAudioOnly &&
this.video.currentTime == 0 && !this.hasError();
this.startFade(this.clickToPlay, shouldClickToPlayShow, true);
- this.startFade(this.controlsSpacer, shouldClickToPlayShow, true);
this.startFade(this.controlBar, shouldShow, true);
},
get dynamicControls() {
// Don't fade controls for <audio> elements.
var enabled = !this.isAudioOnly;
// Allow tests to explicitly suppress the fading of controls.
@@ -393,18 +401,18 @@
* is completed.
*/
SHOW_THROBBER_TIMEOUT_MS: 250,
_showThrobberTimer: null,
_delayShowThrobberWhileResumingVideoDecoder() {
this._showThrobberTimer = setTimeout(() => {
this.statusIcon.setAttribute("type", "throbber");
// Show the throbber immediately since we have waited for SHOW_THROBBER_TIMEOUT_MS.
- // We don't want to wait for another transition-delay(750ms) and the
- // transition-duration(300ms).
+ // We don't want to wait for another animation delay(750ms) and the
+ // animation duration(300ms).
this.setupStatusFader(true);
}, this.SHOW_THROBBER_TIMEOUT_MS);
},
_cancelShowThrobberWhileResumingVideoDecoder() {
if (this._showThrobberTimer) {
clearTimeout(this._showThrobberTimer);
this._showThrobberTimer = null;
}
@@ -424,17 +432,16 @@
case "play":
this.setPlayButtonState(false);
this.setupStatusFader();
if (!this._triggeredByControls && this.dynamicControls && this.videocontrols.isTouchControls) {
this.startFadeOut(this.controlBar);
}
if (!this._triggeredByControls) {
this.clickToPlay.hidden = true;
- this.controlsSpacer.setAttribute("fadeout", "true");
}
this._triggeredByControls = false;
break;
case "pause":
// Little white lie: if we've internally paused the video
// while dragging the scrubber, don't change the button state.
if (!this.scrubber.isDragging) {
this.setPlayButtonState(true);
@@ -964,81 +971,131 @@
this.adjustControlSize();
// Keep the controls visible if the click-to-play is visible.
if (!this.clickToPlay.hidden) {
return;
}
this.startFadeOut(this.controlBar, false);
- this.textTrackList.setAttribute("hidden", "true");
+ this.textTrackList.hidden = true;
clearTimeout(this._showControlsTimeout);
Utils._controlsHiddenByTimeout = false;
}
},
startFadeIn(element, immediate) {
this.startFade(element, true, immediate);
},
startFadeOut(element, immediate) {
this.startFade(element, false, immediate);
},
- startFade(element, fadeIn, immediate) {
- if (element.classList.contains("controlBar") && fadeIn) {
- // Bug 493523, the scrubber doesn't call valueChanged while hidden,
- // so our dependent state (eg, timestamp in the thumb) will be stale.
- // As a workaround, update it manually when it first becomes unhidden.
- if (element.hidden) {
- this.scrubber.value = this.video.currentTime * 1000;
+ animationMap: new WeakMap(),
+
+ animationProps: {
+ clickToPlay: {
+ keyframes: [
+ { transform: "scale(3)", opacity: 0 },
+ { transform: "scale(1)", opacity: 0.55 }
+ ],
+ options: {
+ duration: 400
+ }
+ },
+ controlBar: {
+ keyframes: [
+ { opacity: 0 },
+ { opacity: 1 }
+ ],
+ options: {
+ duration: 200
+ }
+ },
+ statusOverlay: {
+ keyframes: [
+ { opacity: 0 },
+ { opacity: 0, offset: .72 }, // ~750ms into animation
+ { opacity: 1 }
+ ],
+ options: {
+ duration: 1050
}
}
+ },
- if (immediate) {
- element.setAttribute("immediate", true);
- } else {
- element.removeAttribute("immediate");
+ startFade(element, fadeIn, immediate = false) {
+ // Bug 493523, the scrubber doesn't call valueChanged while hidden,
+ // so our dependent state (eg, timestamp in the thumb) will be stale.
+ // As a workaround, update it manually when it first becomes unhidden.
+ if (element == this.controlBar && fadeIn && element.hidden) {
+ this.scrubber.value = this.video.currentTime * 1000;
+ }
+
+ let animationProp =
+ this.animationProps[element.getAttribute("anonid")];
+ if (!animationProp) {
+ throw new Error("Element " + element.getAttribute("anonid") +
+ " has no transition. Toggle the hidden property directly.");
+ }
+
+ let animation = this.animationMap.get(element);
+ if (!animation) {
+ animation = new Animation(new KeyframeEffect(
+ element, animationProp.keyframes, animationProp.options));
+
+ this.animationMap.set(element, animation);
}
if (fadeIn) {
// hidden state should be controlled by adjustControlSize
- if (!(element.isAdjustableControl && element.hideByAdjustment)) {
- element.hidden = false;
+ if (element.isAdjustableControl && element.hiddenByAdjustment) {
+ return;
+ }
+
+ // No need to fade in again if the element is visible and not fading out
+ if (!element.hidden && !element.classList.contains("fadeout")) {
+ return;
}
- // force style resolution, so that transition begins
- // when we remove the attribute.
- element.clientTop;
- element.removeAttribute("fadeout");
- if (element.classList.contains("controlBar")) {
- this.controlsSpacer.removeAttribute("hideCursor");
+
+ // Unhide
+ element.hidden = false;
+ } else {
+ // No need to fade out if the element is already no visible.
+ if (element.hidden) {
+ return;
}
- } else {
- element.setAttribute("fadeout", true);
- if (element.classList.contains("controlBar") && !this.hasError() &&
+
+ if (element == this.controlBar && !this.hasError() &&
document.mozFullScreenElement == this.video) {
this.controlsSpacer.setAttribute("hideCursor", true);
}
}
- },
- onTransitionEnd(event) {
- // Ignore events for things other than opacity changes.
- if (event.propertyName != "opacity") {
- return;
+ element.classList.toggle("fadeout", !fadeIn);
+ element.classList.toggle("fadein", fadeIn);
+ let finishedPromise;
+ if (!immediate) {
+ animation.playbackRate = fadeIn ? 1 : -1;
+ animation.play();
+ finishedPromise = animation.finished;
+ } else {
+ animation.cancel();
+ finishedPromise = Promise.resolve();
}
-
- var element = event.originalTarget;
-
- // Nothing to do when a fade *in* finishes.
- if (!element.hasAttribute("fadeout")) {
- return;
- }
-
- element.hidden = true;
+ finishedPromise.then(() => {
+ if (element == this.controlBar) {
+ this.onControlBarAnimationFinished();
+ }
+ element.classList.remove(fadeIn ? "fadein" : "fadeout");
+ if (!fadeIn) {
+ element.hidden = true;
+ }
+ }, () => { /* Do nothing on rejection */ });
},
_triggeredByControls: false,
startPlay() {
this._triggeredByControls = true;
this.hideClickToPlay();
this.video.play();
@@ -1186,25 +1243,19 @@
let videoWidth = this.video.clientWidth;
// The play button will animate to 3x its size. This
// shows the animation unless the video is too small
// to show 2/3 of the animation.
let animationScale = 2;
let animationMinSize = this.clickToPlay.minWidth * animationScale;
- if (animationMinSize > videoWidth ||
- animationMinSize > (videoHeight - this.controlBarMinHeight)) {
- this.clickToPlay.setAttribute("immediate", "true");
- this.clickToPlay.hidden = true;
- } else {
- this.clickToPlay.removeAttribute("immediate");
- }
- this.clickToPlay.setAttribute("fadeout", "true");
- this.controlsSpacer.setAttribute("fadeout", "true");
+ let immediate = (animationMinSize > videoWidth ||
+ animationMinSize > (videoHeight - this.controlBarMinHeight));
+ this.startFadeOut(this.clickToPlay, immediate);
},
setPlayButtonState(aPaused) {
if (aPaused) {
this.playButton.setAttribute("paused", "true");
} else {
this.playButton.removeAttribute("paused");
}
@@ -1481,34 +1532,34 @@
for (let tt of this.overlayableTextTracks) {
if (tt.index === index) {
tt.mode = "showing";
} else {
tt.mode = "disabled";
}
}
- this.textTrackList.setAttribute("hidden", "true");
+ this.textTrackList.hidden = true;
},
- onControlBarTransitioned() {
- this.textTrackList.setAttribute("hidden", "true");
+ onControlBarAnimationFinished() {
+ this.textTrackList.hidden = true;
this.video.dispatchEvent(new CustomEvent("controlbarchange"));
this.adjustControlSize();
},
toggleCasting() {
this.videocontrols.dispatchEvent(new CustomEvent("VideoBindingCast"));
},
toggleClosedCaption() {
- if (this.textTrackList.hasAttribute("hidden")) {
- this.textTrackList.removeAttribute("hidden");
+ if (this.textTrackList.hidden) {
+ this.textTrackList.hidden = false;
} else {
- this.textTrackList.setAttribute("hidden", "true");
+ this.textTrackList.hidden = true;
}
},
onTextTrackAdd(trackEvent) {
this.addNewTextTrack(trackEvent.track);
this.setClosedCaptionButtonState();
},
@@ -1600,24 +1651,24 @@
let videoHeight = this.isAudioOnly ? this.controlBarMinHeight : givenHeight;
let videocontrolsWidth = this.videocontrols.clientWidth;
let widthUsed = minControlBarPaddingWidth;
let preventAppendControl = false;
for (let control of this.prioritizedControls) {
if (!control.isWanted) {
- control.hideByAdjustment = true;
+ control.hiddenByAdjustment = true;
continue;
}
- control.hideByAdjustment = preventAppendControl ||
+ control.hiddenByAdjustment = preventAppendControl ||
widthUsed + control.minWidth > videoWidth;
- if (control.hideByAdjustment) {
+ if (control.hiddenByAdjustment) {
preventAppendControl = true;
} else {
widthUsed += control.minWidth;
}
}
// Use flexible spacer to separate controls when scrubber is hidden.
// As long as muteButton hidden, which means only play button presents,
@@ -1642,34 +1693,34 @@
this.controlBar.style.width = `${videoWidth}px`;
}
return;
}
if (videoHeight < this.controlBarMinHeight ||
widthUsed === minControlBarPaddingWidth) {
this.controlBar.setAttribute("size", "hidden");
- this.controlBar.hideByAdjustment = true;
+ this.controlBar.hiddenByAdjustment = true;
} else {
this.controlBar.removeAttribute("size");
- this.controlBar.hideByAdjustment = false;
+ this.controlBar.hiddenByAdjustment = false;
}
// Adjust clickToPlayButton size.
const minVideoSideLength = Math.min(videoWidth, videoHeight);
const clickToPlayViewRatio = 0.15;
const clickToPlayScaledSize = Math.max(
this.clickToPlay.minWidth, minVideoSideLength * clickToPlayViewRatio);
if (clickToPlayScaledSize >= videoWidth ||
(clickToPlayScaledSize + this.controlBarMinHeight / 2 >= videoHeight / 2 )) {
- this.clickToPlay.hideByAdjustment = true;
+ this.clickToPlay.hiddenByAdjustment = true;
} else {
if (this.clickToPlay.hidden && !this.video.played.length && this.video.paused) {
- this.clickToPlay.hideByAdjustment = false;
+ this.clickToPlay.hiddenByAdjustment = false;
}
this.clickToPlay.style.width = `${clickToPlayScaledSize}px`;
this.clickToPlay.style.height = `${clickToPlayScaledSize}px`;
}
},
init(binding) {
this.video = binding.parentNode;
@@ -1774,18 +1825,16 @@
// On touch videocontrols, tapping controlsSpacer should show/hide
// the control bar, instead of playing the video or toggle fullscreen.
if (!this.videocontrols.isTouchControls) {
addListener(this.controlsSpacer, "click", this.clickToPlayClickHandler);
addListener(this.controlsSpacer, "dblclick", this.toggleFullscreen);
}
addListener(this.videocontrols, "resizevideocontrols", this.adjustControlSize);
- addListener(this.videocontrols, "transitionend", this.onTransitionEnd);
- addListener(this.controlBar, "transitionend", this.onControlBarTransitioned);
// See comment at onFullscreenChange on bug 718107.
// addListener(this.video.ownerDocument, "fullscreenchange", this.onFullscreenChange);
addListener(this.video, "keypress", this.keyHandler, {capture: true});
// Prevent any click event within media controls from dispatching through to video.
addListener(this.videocontrols, "click", function(event) {
event.stopPropagation();
}, {mozSystemGroup: false});
addListener(this.videocontrols, "dragstart", function(event) {
@@ -1818,17 +1867,17 @@
controlsTimeout: 5000,
get Utils() {
return this.videocontrols.Utils;
},
get visible() {
return !this.Utils.controlBar.hasAttribute("fadeout") &&
- !(this.Utils.controlBar.getAttribute("hidden") == "true");
+ !(this.Utils.controlBar.hidden);
},
_firstShow: false,
get firstShow() { return this._firstShow; },
set firstShow(val) {
this._firstShow = val;
this.Utils.controlBar.setAttribute("firstshow", val);
},
--- a/toolkit/themes/shared/media/videocontrols.css
+++ b/toolkit/themes/shared/media/videocontrols.css
@@ -60,17 +60,17 @@ audio > xul|videocontrols {
.touch .controlBar {
/* Do not delete: these variables are accessed by JavaScript directly.
see videocontrols.xml and search for |-width|. */
--scrubberStack-width: 84px;
--volumeStack-width: 64px;
}
-.controlsContainer [hidden="true"],
+.controlsContainer [hidden],
.controlBar[hidden] {
display: none;
}
.controlBar[size="hidden"] {
display: none;
}
@@ -444,51 +444,16 @@ audio > xul|videocontrols {
.controlsSpacerStack:hover > .clickToPlay[fadeout] {
opacity: 0;
}
.controlBar[fullscreen-unavailable] .fullscreenButton {
display: none;
}
-/* CSS Transitions */
-.clickToPlay {
- transition-property: transform, opacity;
- transition-duration: 400ms, 400ms;
-}
-
-.controlsSpacer[fadeout] {
- opacity: 0;
-}
-
-.clickToPlay[fadeout] {
- transform: scale(3);
- opacity: 0;
-}
-
-.clickToPlay[fadeout][immediate] {
- transition-property: opacity, background-size;
- transition-duration: 0s, 0s;
-}
-.controlBar:not([immediate]) {
- transition-property: opacity;
- transition-duration: 200ms;
-}
-.controlBar[fadeout] {
- opacity: 0;
-}
-.volumeStack:not([immediate]) {
- transition-property: opacity, margin-top;
- transition-duration: 200ms, 200ms;
-}
-.statusOverlay:not([immediate]) {
- transition-property: opacity;
- transition-duration: 300ms;
- transition-delay: 750ms;
-}
.statusOverlay[fadeout],
.statusOverlay[error] + .controlsOverlay > .controlsSpacerStack {
opacity: 0;
}
/* Error description formatting */
.errorLabel {
padding: 0 10px;