吐司

此模式展示了如何构建可访问的自适应消息框组件。

完整文章 · YouTube 上的视频 · GitHub 上的来源

<button id="spells">
  Cast Spell
</button>
<button id="actions">
  Mock User Action
</button>

       
.gui-toast-group {
 
position: fixed;
 
z-index: 1;
 
inset-block-end: 0;
 
inset-inline: 0;
 
padding-block-end: 5vh;

 
display: grid;
 
justify-items: center;
 
justify-content: center;
 
gap: 1vh;

 
/* optimizations */
 
pointer-events: none;
}

.gui-toast {
 
--_duration: 3s;
 
--_bg-lightness: 90%;
 
--_travel-distance: 0;

 
font-family: system-ui, sans-serif;
 
color: black;
 
background: hsl(0 0% var(--_bg-lightness) / 90%);
 
 
max-inline-size: min(25ch, 90vw);
 
padding-block: .5ch;
 
padding-inline: 1ch;
 
border-radius: 3px;
 
font-size: 1rem;

 
will-change: transform;
 
animation:
    fade-in
.3s ease,
    slide-in
.3s ease,
    fade-out
.3s ease var(--_duration);

 
@media (--dark) {
   
color: white;
   
--_bg-lightness: 20%;
 
}

 
@media (--motionOK) {
   
--_travel-distance: 5vh;
 
}
}

@keyframes fade-in {
  from
{ opacity: 0 }
}

@keyframes fade-out {
  to
{ opacity: 0 }
}

@keyframes slide-in {
  from
{ transform: translateY(var(--_travel-distance, 10px)) }
}
       

       
const init = () => {
 
const node = document.createElement('section')
  node
.classList.add('gui-toast-group')

  document
.firstElementChild.insertBefore(node, document.body)
 
return node
}

const createToast = text => {
 
const node = document.createElement('output')
 
  node
.innerText = text
  node
.classList.add('gui-toast')
  node
.setAttribute('role', 'status')
  node
.setAttribute('aria-live', 'polite')

 
return node
}

const addToast = toast => {
 
const { matches:motionOK } = window.matchMedia(
   
'(prefers-reduced-motion: no-preference)'
 
)

 
Toaster.children.length && motionOK
   
? flipToast(toast)
   
: Toaster.appendChild(toast)
}

const Toast = text => {
  let toast
= createToast(text)
  addToast
(toast)

 
return new Promise(async (resolve, reject) => {
    await
Promise.allSettled(
      toast
.getAnimations().map(animation =>
        animation
.finished
     
)
   
)
   
Toaster.removeChild(toast)
    resolve
()
 
})
}

// https://aerotwist.com/blog/flip-your-animations/
const flipToast = toast => {
 
// FIRST
 
const first = Toaster.offsetHeight

 
// add new child to change container size
 
Toaster.appendChild(toast)

 
// LAST
 
const last = Toaster.offsetHeight

 
// INVERT
 
const invert = last - first

 
// PLAY
 
const animation = Toaster.animate([
   
{ transform: `translateY(${invert}px)` },
   
{ transform: 'translateY(0)' }
 
], {
    duration
: 150,
    easing
: 'ease-out',
 
})

  animation
.startTime = document.timeline.currentTime
}

const Toaster = init()
export default Toast