Last active
September 19, 2021 13:15
-
-
Save mherchel/ac37b3fd3f83018a6760724ec5806f07 to your computer and use it in GitHub Desktop.
Lullabot.com MP3 Player
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{{ attach_library('lullabotcom/mp3player') }} | |
{% for item in items %} | |
{# Default implentation of audio element is hidden from modern browsers and shown to IE11. #} | |
<audio class="u-noscript-visible u-ie11-visible mp3player--default" controls> | |
<source src="{{ item.content }}" type="audio/mpeg"> | |
</audio> | |
<div class="u-noscript-hidden u-ie11-hidden mp3player"> | |
<audio src="{{ item.content }}" preload="auto"></audio> | |
<button class="mp3player_play-pause u-no-style"> | |
<span class="visually-hidden">Play</span> | |
</button> | |
<div class="mp3player__location"> | |
<span class="mp3player__current--current">00:00</span> | |
/ | |
<span class="mp3player__current--end"></span> | |
</div> | |
<label class="visually-hidden" for="mp3player__scrubber">Playback Location</label> | |
<input type="range" id="mp3player__scrubber" class="mp3player__scrubber" min="0" max="3600" step="1" value="0" style="--max: 3600; --val: 0"> | |
<button type="button" class="mp3player__playback-rate"> | |
<span class="mp3player__playback-rate__label visually-hidden">Playback Rate:</span> | |
<span class="mp3player__playback-rate__value">1</span> | |
<span class="mp3player__playback-rate__x">x</span> | |
</button> | |
<a href="{{ item.content }}" class="mp3player__download" download> | |
<div class="mp3player__download__icon"> | |
{% include "@lullabotcom/svg_imports/icon-download.svg" %} | |
</div> | |
<span class="mp3player__download__text">Download</span> | |
</a> | |
</div> | |
{% endfor %} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div class="mp3extra"> | |
{{ content.field_audio_media }} | |
{% if node.field_transcript.value %} | |
<a href="#transcript" class="mp3extra__transcript"> | |
<div class="mp3extra__transcript__icon"> | |
{% include "@lullabotcom/svg_imports/icon-transcript.svg" %} | |
</div> | |
<span class="mp3extra__transcript__text">Transcript</span> | |
</a> | |
{% endif %} | |
<div class="mp3extra__image"> | |
<img src="{{ podcast_image_uri|image_style('thumbnail') }}" alt="" /> | |
</div> | |
<div class="mp3extra__subscribe"> | |
<button id="mp3extra__subscribe__button" class="mp3extra__subscribe__button u-no-style" aria-haspopup="true" aria-controls="mp3extra__subscribe-menu"> | |
<span class="mp3extra__subscribe__text">Subscribe</span> | |
</button> | |
<ul id="mp3extra__subscribe-menu" class="mp3extra__subscribe-menu"> | |
{% if parent.field_itunes_url.0.url %} | |
<li> | |
<a target="_blank" href="{{ parent.field_itunes_url.0.url }}"> | |
{% include "@lullabotcom/svg_imports/itunes.svg" %} | |
iTunes | |
</a> | |
</li> | |
{% endif %} | |
{% if parent.field_google_play_url.0.url %} | |
<li> | |
<a target="_blank" href="{{ parent.field_google_play_url.0.url }}"> | |
{% include "@lullabotcom/svg_imports/google-play.svg" %} | |
Google Play | |
</a> | |
</li> | |
{% endif %} | |
{% if parent.field_stitcher_url.0.url %} | |
<li> | |
<a target="_blank" href="{{ parent.field_stitcher_url.0.url }}"> | |
{% include "@lullabotcom/svg_imports/stitcher.svg" %} | |
Stitcher | |
</a> | |
</li> | |
{% endif %} | |
{% if parent.field_spotify_url.0.url %} | |
<li> | |
<a target="_blank" href="{{ parent.field_spotify_url.0.url }}"> | |
{% include "@lullabotcom/svg_imports/spotify.svg" %} | |
Spotify | |
</a> | |
</li> | |
{% endif %} | |
{% if parent.field_rss_feed_url.0.url %} | |
<li> | |
<a target="_blank" href="{{ parent.field_rss_feed_url.0.url }}"> | |
{% include "@lullabotcom/svg_imports/rss.svg" %} | |
RSS | |
</a> | |
</li> | |
{% endif %} | |
</ul> | |
</div> | |
</div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
(function () { | |
/* | |
* MP3 player(s) | |
* | |
* This handles the binding of the various HTML elements to the HTMLMediaElement object, | |
* as well as resuming the audio if the event where the user navigated away and returned. | |
*/ | |
Drupal.behaviors.mp3Player = { | |
'attach': function (context) { | |
context.querySelectorAll('.mp3player').forEach(mp3player => { | |
const playButton = mp3player.querySelector('.mp3player_play-pause'); | |
const playbackRateButton = mp3player.querySelector('.mp3player__playback-rate'); | |
const audio = mp3player.querySelector('audio'); | |
const currentTimeElement = mp3player.querySelector('.mp3player__current--current'); | |
const durationElement = mp3player.querySelector('.mp3player__current--end'); | |
const scrubber = mp3player.querySelector('.mp3player__scrubber'); | |
const audioStorageNameSpace = 'mp3Player.lastTimeStamp.' + audio.src; | |
const playbackRateStorageNameSpace = 'mp3Player.playbackRate.' + audio.src; | |
const lastTimeStamp = parseInt(localStorage.getItem(audioStorageNameSpace)) - 5; | |
function setupAudio() { | |
durationElement.textContent = formatTime(audio.duration); | |
scrubber.max = parseInt(audio.duration); | |
// If the user has listened to the episode before, resume 5 seconds before | |
// where they left off... unless they left off within 30 seconds of the end. | |
if (audioShouldResume()) { | |
console.info('Woot 🎉 We\'re resuming where you left off the last time you were here!'); | |
scrubber.value = lastTimeStamp; | |
currentTimeElement.textContent = formatTime(lastTimeStamp); | |
changePlaybackRate(getCurrentPlaybackRate()); | |
} | |
// set slider max CSS variable for playback progress styling purposes | |
// we only need to do this once when the player is setup | |
scrubber.style.setProperty('--max', parseInt(scrubber.max)); | |
updateCSSVar(); | |
} | |
function resumeAudio() { | |
if (audioShouldResume()) { | |
// HTMLMediaElement.fastSeek() is needed for FF to seek properly. | |
// @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/fastSeek | |
if (audio.fastSeek) { | |
audio.fastSeek(parseInt(lastTimeStamp)); | |
} | |
else { | |
audio.currentTime = lastTimeStamp; | |
} | |
scrubber.value = lastTimeStamp; | |
} | |
} | |
// Change the audio playback rate, update the DOM, and store the setting in localStorage. | |
function changePlaybackRate(rate) { | |
const playbackRateText = playbackRateButton.querySelector('.mp3player__playback-rate__value'); | |
playbackRateText.textContent = rate; | |
audio.playbackRate = rate; | |
localStorage.setItem(playbackRateStorageNameSpace, rate); | |
} | |
// Get the current playback rate. | |
function getCurrentPlaybackRate() { | |
return parseFloat(localStorage.getItem(playbackRateStorageNameSpace) || 1); | |
} | |
// Has the user listened to this audio before, and then navigated away? | |
function audioShouldResume() { | |
return lastTimeStamp > 5 && lastTimeStamp < audio.duration - 30; | |
} | |
function handlePlayPauseClick() { | |
if (audio.paused) { | |
playButton.setAttribute('aria-pressed', true); | |
playButton.querySelector('span').textContent = 'Click to Pause'; | |
audio.play(); | |
} | |
else { | |
playButton.setAttribute('aria-pressed', false); | |
playButton.querySelector('span').textContent = 'Click to Play'; | |
audio.pause(); | |
} | |
} | |
function handlePlaybackRateClick() { | |
const playbackRateValues = [0.5, 0.75, 1, 1.25, 1.5, 2,]; | |
const currentLocation = playbackRateValues.indexOf(getCurrentPlaybackRate()); | |
const newSpeed = (currentLocation !== playbackRateValues.length - 1) ? playbackRateValues[currentLocation + 1] : playbackRateValues[0]; | |
changePlaybackRate(newSpeed); | |
} | |
function handleScrubberInput() { | |
if (audio.fastSeek) { | |
audio.fastSeek(scrubber.value); | |
} | |
else { | |
audio.currentTime = scrubber.value; | |
} | |
currentTimeElement.textContent = formatTime(scrubber.value); | |
updateCSSVar(); | |
} | |
function handleProgress(e) { | |
currentTimeElement.textContent = formatTime(e.target.currentTime); | |
scrubber.value = parseInt(e.target.currentTime); | |
// Update localStorage with currentTime, in case we need to pick up where they left off. | |
localStorage.setItem(audioStorageNameSpace, Math.floor(e.target.currentTime)); | |
updateCSSVar(); | |
} | |
function updateCSSVar() { | |
// set current slider value CSS variable as a hook for styling playback progress in Webkit browsers | |
scrubber.style.setProperty('--val', parseInt(scrubber.value)); | |
} | |
// Chrome doesn't fire 'loadedmetadata' or 'loadedmetadata' event listeners as expected, | |
// so we execute this when the behavior fires. | |
setupAudio(); | |
playButton.addEventListener('click', handlePlayPauseClick); | |
playbackRateButton.addEventListener('click', handlePlaybackRateClick); | |
scrubber.addEventListener('input', handleScrubberInput); | |
audio.addEventListener('timeupdate', handleProgress); | |
// We have to bind to the 'playing' event for the audio to actually resume at the correct location | |
// in Mobile Safari 🤮. Note this behavior doesn't work in Chrome, so we also have to fire on | |
// the 'loadedmetadata' event 🤮. None of these work in desktop Safari, so we bind | |
// to the 'canplaythrough' event 🤮. | |
audio.addEventListener('loadedmetadata', setupAudio, {'once': true, 'passive': true,}); | |
audio.addEventListener('canplaythrough', setupAudio, {'once': true, 'passive': true,}); | |
audio.addEventListener('playing', resumeAudio, {'once': true, 'passive': true,}); | |
}); | |
function formatTime(duration) { | |
let minutes = parseInt(duration / 60); | |
let seconds = parseInt(duration) - (minutes * 60); | |
minutes = (minutes < 10) ? '0' + minutes : minutes; | |
seconds = (seconds < 10) ? '0' + seconds : seconds; | |
// Firefox does not execute the 'loadedmetadata' event listener, so we execute handleLoadMetaData() | |
// early. This strips out any NaNs that exist when handleLoadMetaData() fires early in Chrome, or Safari. | |
minutes = String(minutes).replace('NaN', '00'); | |
seconds = String(seconds).replace('NaN', '00'); | |
return minutes + ':' + seconds; | |
} | |
}, | |
}; | |
/* | |
* Sticky the MP3 player to the top and bottom of the screen when it'd normally be outside of the viewport. | |
*/ | |
Drupal.behaviors.stickyPlayer = { | |
'attach': function (context) { | |
const mp3Player = context.querySelector('.mp3extra'); | |
const headerHeight = context.querySelector('.site-header').clientHeight; | |
let offset; | |
let viewportHeight; | |
function handleResize() { | |
offset = context.querySelector('.mp3-placeholder').getBoundingClientRect(); | |
// Offset only calculates offset relative to the viewport. If the page is | |
// reloaded scrolled halfway down, we need to account for scroll position. | |
offset.topPage = offset.top + window.pageYOffset; | |
// Can trigger reflow, so only do when browser is resized. | |
viewportHeight = window.innerHeight; | |
} | |
function handleScroll() { | |
const scrollPos = window.pageYOffset; | |
if (scrollPos >= offset.topPage - headerHeight) { | |
mp3Player.classList.remove('fixed-bottom'); | |
mp3Player.classList.add('fixed', 'fixed-top'); | |
} | |
else if (scrollPos + viewportHeight <= offset.topPage + mp3Player.clientHeight) { | |
mp3Player.classList.remove('fixed-top'); | |
mp3Player.classList.add('fixed', 'fixed-bottom'); | |
} | |
else { | |
mp3Player.classList.remove('fixed', 'fixed-top', 'fixed-bottom'); | |
} | |
} | |
// Trigger resize and scroll events on pageload. | |
handleResize(); | |
handleScroll(); | |
Drupal.helper.optimizedScroll.add(handleScroll); | |
Drupal.helper.optimizedResize.add(handleResize); | |
}, | |
}; | |
})(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@import '../core/functions'; | |
@import '../core/mixins'; | |
@import '../core/variables'; | |
.mp3player { | |
@include vr-all-breakpoints(margin-top, 3); | |
@include vr-all-breakpoints(margin-bottom, 3); | |
display: flex; | |
justify-content: space-between; | |
height: vr(kilo, 2); | |
background: palette(grayscale-95); | |
font-size: font-size(1); | |
@include breakpoint($mp3-break-medium) { | |
height: vr(lima, 2); | |
} | |
// Hide from IE11 which does not support web audio API. | |
@include ie11 { | |
display: none; | |
} | |
// If embedded with subscribe, transcript, etc, do not add margin. | |
.mp3extra & { | |
flex-basis: 100%; | |
margin: 0; | |
} | |
> * { | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
} | |
// Hide invisible audio elements, which are treated as blocks within flex | |
// context in Firefox. | |
> audio:not([controls]) { | |
display: none; | |
} | |
} | |
// Default audio player. Shown to IE11 and non-JS browsers. | |
.mp3player--default { | |
display: none; | |
width: 80%; | |
margin: vr(lima, 2) auto; | |
@include ie11 { | |
display: block; | |
min-height: 60px; | |
margin: 0; | |
} | |
} | |
// Play / pause button. | |
.mp3player_play-pause { | |
position: relative; | |
width: vr(kilo, 2); // Equal to height. | |
height: auto; | |
flex-shrink: 0; | |
border: 0; | |
background-color: palette(blue-60); | |
overflow: hidden; | |
transition: all 0.2s; | |
@include breakpoint($mp3-break-medium) { | |
width: vr(lima, 2); | |
} | |
&:active { | |
background-color: palette(blue-50); | |
} | |
&:before { | |
content: ''; | |
position: absolute; | |
top: 50%; | |
height: 0; | |
width: 0; | |
left: calc(50% + 3px); | |
transform: translate(-50%, -50%) scale(0.7); | |
border-top: 15px solid transparent; | |
border-left: 25px solid palette(grayscale-100); | |
border-bottom: 15px solid transparent; | |
@include breakpoint($mp3-break-medium) { | |
transform: translate(-50%, -50%); | |
} | |
} | |
&[aria-pressed='true']:before { | |
height: 22px; | |
width: 18px; | |
left: 50%; | |
border-top: 0; | |
border-left: solid 6px palette(grayscale-100); | |
border-right: solid 6px palette(grayscale-100); | |
border-bottom: 0; | |
} | |
} | |
// Time start / remaining. | |
.mp3player__location { | |
flex-basis: 140px; | |
padding: 0 10px; | |
color: palette(grayscale-40); | |
font-size: font-size(-1); | |
@include breakpoint($mp3-break-medium) { | |
font-size: font-size(1); | |
} | |
} | |
.mp3player__playback-rate { | |
&[class][class][class] { | |
align-self: center; | |
height: vr(lima, 1); | |
width: vr(lima, 1); | |
background-color: transparent; | |
color: palette(grayscale-40); | |
@include breakpoint($mp3-break-small) { | |
margin-left: 10px; | |
} | |
&:hover, | |
&:focus { | |
background-color: palette(grayscale-80); | |
} | |
} | |
} | |
.mp3player__download { | |
display: none; | |
border: 0; | |
padding: 0 5px; | |
color: palette(blue-45); | |
font-style: italic; | |
font-size: font-size(0); | |
@include breakpoint($mp3-break-medium) { | |
padding: 0 15px; | |
} | |
@include breakpoint(400px) { | |
display: flex; | |
} | |
} | |
.mp3player__download__text { | |
margin-left: 15px; | |
@include breakpoint($mp3-break-large, 'max-width') { | |
@include element-invisible; | |
} | |
} | |
.mp3player__download__icon { | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
height: vr(lima, 1); | |
width: vr(lima, 1); | |
background-color: palette(blue-60); | |
svg { | |
height: 20px; | |
width: 20px; | |
} | |
} | |
// Stying the scrubber | |
// @see https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/ | |
.mp3player__scrubber { | |
--thumb-size: 20px; | |
--ratio: calc(var(--val)/var(--max)); | |
--x-pos: calc(0.5 * var(--thumb-size) + var(--ratio) * (100% - var(--thumb-size))); | |
flex-grow: 1; | |
-webkit-appearance: none; | |
background: transparent; | |
align-self: center; | |
cursor: pointer; | |
@include breakpoint($mp3-break-small, max-width) { | |
position: absolute; | |
left: 1%; | |
bottom: -20px; | |
width: 98%; | |
} | |
&:focus { | |
outline: none; | |
&::-webkit-slider-thumb { | |
background: palette(grayscale-100); | |
box-shadow: 0 0 0 4px palette(blue-60); | |
outline: 2px solid transparent; // Windows High Contrast mode does not show backgrounds or box-shadows. | |
} | |
&::-moz-range-thumb { | |
background: palette(grayscale-100); | |
box-shadow: 0 0 0 4px palette(blue-60); | |
outline: 2px solid transparent; // Windows High Contrast mode does not show backgrounds or box-shadows. | |
} | |
} | |
&::-webkit-slider-runnable-track { | |
width: 100%; | |
height: 2px; | |
background: linear-gradient(palette(blue-60), palette(blue-60)) 0/var(--x-pos) 100% no-repeat palette(grayscale-75); | |
} | |
&::-webkit-slider-thumb { | |
height: 20px; | |
width: 20px; | |
border: 0; | |
border-radius: 50%; | |
background: palette(blue-60); | |
-webkit-appearance: none; | |
margin-top: -9px; | |
transition: all 0.3s $cubic-bezier, outline 0s $cubic-bezier; | |
@media (pointer: fine) { | |
height: 12px; | |
width: 12px; | |
margin-top: -5px; | |
} | |
} | |
&::-moz-range-track { | |
width: 100%; | |
height: 2px; | |
background: palette(grayscale-75); | |
} | |
&::-moz-range-thumb { | |
height: 12px; | |
width: 12px; | |
border: 0; | |
border-radius: 50%; | |
background: palette(blue-60); | |
transition: all 0.3s $cubic-bezier; | |
} | |
&::-moz-range-progress { | |
height: 2px; | |
background: palette(blue-60); | |
} | |
&::-ms-track { | |
width: 100%; | |
height: 2px; | |
background: transparent; | |
border-color: transparent; | |
border-width: 16px 0; | |
color: transparent; | |
} | |
&::-ms-fill-lower { | |
background: palette(blue-60); | |
} | |
&::-ms-fill-upper { | |
background: palette(grayscale-75); | |
} | |
&::-ms-thumb { | |
height: 12px; | |
width: 12px; | |
border: 0; | |
border-radius: 50%; | |
background: palette(blue-60); | |
margin-top: 0; | |
} | |
&:focus::-ms-fill-lower { | |
background: palette(blue-60); | |
} | |
&:focus::-ms-fill-upper { | |
background: palette(blue-60); | |
} | |
} | |
// The .mp3extra div transitions between normal position: static to position: fixed | |
// when it would normally reside outside the viewport. This .mp3-placeholder wrapping | |
// div ensures that the content does not collapse around it (and create a visual jump | |
// that the user would notice. | |
.mp3-placeholder { | |
@include vr-all-breakpoints(margin-top, 3); | |
@include vr-all-breakpoints(margin-bottom, 3); | |
height: vr(kilo, 2); | |
@include breakpoint($mp3-break-medium) { | |
height: vr(lima, 2); | |
} | |
} | |
.mp3extra { | |
position: relative; | |
display: flex; | |
justify-content: space-between; | |
height: vr(kilo, 2); | |
background: palette(grayscale-95); | |
font-size: font-size(1); | |
@include breakpoint($mp3-break-medium) { | |
height: vr(lima, 2); | |
border: solid 1px transparent; | |
} | |
// Only fix the headers if the browser height isn't too small. | |
@media (min-height: 600px) and (min-width: map-get($breakpoints, $mp3-break-medium)) { | |
&.fixed { | |
position: fixed; | |
width: calc(100% - #{2 * map-get($body-padding, kilo)}); | |
max-width: $site-max-width; | |
z-index: 5; | |
margin: 0; | |
border: solid 1px palette(grayscale-90); | |
} | |
&.fixed-top { | |
top: 72px; | |
filter: drop-shadow(0 10px 20px palette(grayscale-100)); | |
} | |
&.fixed-bottom { | |
bottom: 0; | |
filter: drop-shadow(0 -10px 20px palette(grayscale-100)); | |
} | |
} | |
} | |
.mp3extra__transcript { | |
display: none; | |
justify-content: center; | |
align-items: center; | |
border: 0; | |
color: palette(blue-45); | |
font-style: italic; | |
font-size: font-size(0); | |
@include breakpoint(570px) { | |
display: flex; | |
} | |
@include breakpoint($mp3-break-medium) { | |
padding: 0 15px 0 0; | |
} | |
@include breakpoint($mp3-break-large) { | |
padding: 0 15px; | |
} | |
} | |
.mp3extra__transcript__icon { | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
height: vr(lima, 1); | |
width: vr(lima, 1); | |
background-color: palette(blue-60); | |
svg { | |
max-height: 16px; | |
width: 14px; | |
} | |
} | |
.mp3extra__transcript__text { | |
margin-left: 15px; | |
@include breakpoint($mp3-break-large, 'max-width') { | |
@include element-invisible; | |
} | |
} | |
.mp3extra__image { | |
display: none; | |
flex-grow: 0; | |
flex-shrink: 0; | |
width: vr(lima, 2); | |
@include breakpoint($mp3-break-medium) { | |
display: block; | |
} | |
img { | |
height: 100%; | |
width: auto; | |
} | |
} | |
.mp3extra__subscribe { | |
position: relative; | |
display: flex; | |
justify-content: center; | |
white-space: nowrap; | |
} | |
.mp3extra__subscribe__text { | |
@media (min-width: map-get($breakpoints, $mp3-break-small)) and (max-width: map-get($breakpoints, $mp3-break-medium)) { | |
@include element-invisible; | |
} | |
} | |
.mp3extra__subscribe__button { | |
@include font-family(sans); | |
height: auto; | |
border: 0; | |
padding-right: 10px; | |
background: transparent; | |
appearance: none; | |
cursor: pointer; | |
text-transform: uppercase; | |
font-weight: bold; | |
font-size: font-size(-3); | |
color: palette(grayscale-50); | |
transition: all 0.2s; | |
@include breakpoint(350px) { | |
padding-right: 20px; | |
} | |
@include breakpoint($mp3-break-medium) { | |
padding: 0 20px; | |
font-size: font-size(-1); | |
} | |
&:hover, | |
&:focus, | |
&:active { | |
background-color: transparent; | |
outline: 0; | |
color: palette(blue-50); | |
&:after { | |
border-color: palette(blue-50); | |
} | |
} | |
&:after { | |
content: ''; | |
display: inline-block; | |
vertical-align: middle; | |
width: 8px; | |
height: 8px; | |
border-bottom: solid 2px palette(grayscale-50); | |
border-left: solid 2px palette(grayscale-50); | |
margin-top: -7px; | |
margin-left: 10px; | |
transform: rotate(-45deg); | |
} | |
} | |
.mp3extra__subscribe-menu { | |
display: none; // Hide by default | |
position: absolute; | |
top: 100%; | |
right: 0; | |
width: 160px; | |
margin: 0; | |
padding: 0; | |
list-style: none; | |
line-height: 1.1; | |
font-size: font-size(-1); | |
z-index: 1; // Ensure appears above blockquote below. | |
.mp3extra__subscribe:hover &, | |
.mp3extra__subscribe__button:focus + & { | |
display: block; | |
} | |
// This goes on its own line because in MSEdge (where it's unsupported) it will also break all accompanying selectors. | |
.mp3extra__subscribe:focus-within & { | |
display: block; | |
} | |
.fixed-bottom & { | |
top: auto; | |
bottom: 100%; | |
border-bottom: solid 1px palette(grayscale-85); | |
} | |
li { | |
margin: 0; | |
padding: 0; | |
} | |
a { | |
display: flex; | |
align-items: center; | |
padding: 10px 20px; | |
border-top: solid 1px palette(grayscale-85); | |
border-bottom: 0; | |
background: palette(grayscale-95); | |
transition: background 0.2s; | |
&:focus, | |
&:hover { | |
background: palette(grayscale-85); | |
} | |
} | |
path, | |
svg { | |
fill: palette(grayscale-50); | |
width: 20px; | |
margin-right: 10px; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment