• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / blurts-server / #13117

pending completion
#13117

push

circleci

Vinnl
Pretend HTMLElement.shadowRoot is always set

We generally only tend to access it after having called
this.attachShadow({ mode: 'open' }), and the error message about it
potentially being undefined at every location we access
this.shadowRoot is more noisy than helpful.

See also
https://github.com/mozilla/blurts-server/pull/2959#discussion_r1154023113
and
https://github.com/mozilla/blurts-server/pull/2959#discussion_r1154960095

282 of 1629 branches covered (17.31%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

959 of 4374 relevant lines covered (21.93%)

1.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/client/js/components/toast-alert.js
1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
 * License, v. 2.0. If a copy of the MPL was not distributed with this
3
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4

5
/**
6
 * Toast alert
7
 *
8
 * Displays a short message towards the top of user's screen,
9
 * and auto removes itself after a period of time (default ~7s)
10
 *
11
 * Client JS examples:
12
 * ```
13
 * const toast = document.createElement('toast-alert')
14
 * toast.textContent = 'Alert message here'
15
 * document.body.append(toast)
16
 * ```
17
 *
18
 * ```
19
 * const toast = document.createElement('toast-alert')
20
 * toast.textContent = 'Another alert message here'
21
 * toast.ttl = 10 // seconds before fade-out (defaults to 7)
22
 * toast.type = 'error' // 'error' (default) or 'success'
23
 * document.body.append(toast)
24
 * ```
25
 *
26
 * SSR/HTML examples:
27
 * ```
28
 * <toast-alert>Alert message here</toast-alert>
29
 * <toast-alert ttl='10' type='error'>Another alert message here</toast-alert>
30
 * ```
31
 */
32

33
const html = `
×
34
<style>
35
  :host{
36
    contain: layout style;
37
    position: fixed;
38
    top: var(--padding-md);
39
    left: 0;
40
    width: 100%;
41
    text-align: center;
42
    font-size: .875rem;
43
    color: white;
44
    transform: translateY(var(--toast-y, 0));
45
    transition: transform 0.3s;
46
    animation: fade-out 0.6s var(--ttl, 7s) forwards;
47
    z-index: 2;
48
    pointer-events: none;
49
  }
50

51
  :host(:hover){
52
    animation-play-state: paused;
53
  }
54

55
  :host([hidden]) {
56
    display: none 
57
  }
58

59
  output{
60
    position: relative;
61
    display: inline-block;
62
    padding: var(--padding-sm) var(--padding-xl);
63
    border-radius: var(--border-radius);
64
    box-shadow: 0 0 6px -3px black;
65
    animation: fly-in 0.3s forwards;
66
    pointer-events: auto;
67
  }
68

69
  :host([type="error"]) output {
70
    background-color: var(--red-70);
71
  }
72

73
  :host([type="success"]) output {
74
    background-color: var(--green-80);
75
  }
76

77
  button{
78
    position: absolute;
79
    top: 0;
80
    right: 0;
81
    height: 100%;
82
    padding: 0 var(--padding-md);
83
    border: none;
84
    cursor: pointer;
85
    font: inherit;
86
    color: inherit;
87
    background-color: transparent;
88
  }
89

90
  button:hover{
91
    box-shadow: inset 0 0 64px #fc95;
92
  }
93

94
  @keyframes fly-in{
95
    from{
96
      opacity: 0;
97
      transform: translateY(-30%);
98
    }
99
    to{
100
      opacity: 1;
101
    }
102
  }
103

104
  @keyframes fade-out{
105
    from{
106
      opacity: 1;
107
    }
108
    to{
109
      opacity: 0;
110
    }
111
  }
112
</style>
113

114
<output>
115
  <slot></slot>
116
  <button aria-label="Close">✕</button>
117
</output>
118
`
119

120
const ToastTypes = /** @type {const} */ {
×
121
  Error: 'error',
122
  Success: 'success'
123
}
124

125
customElements.define('toast-alert', class extends HTMLElement {
×
126
  constructor () {
127
    super()
×
128
    this.attachShadow({ mode: 'open' })
×
129
    this.shadowRoot.innerHTML = html
×
130
  }
131

132
  get ttl () {
133
    return this.getAttribute('ttl')
×
134
  }
135

136
  /** @param {string | null} value */
137
  set ttl (value) {
138
    this.setAttribute('ttl', value ?? '')
×
139
    this.style.setProperty('--ttl', `${value}s`) // seconds before fade-out starts
×
140
  }
141

142
  get type () {
143
    return this.getAttribute('type')
×
144
  }
145

146
  /** @param {string | null} value */
147
  set type (value) {
148
    const isValidType = typeof value === 'string' && Object.values(ToastTypes).includes(value)
×
149
    if (!isValidType) {
×
150
      console.warn(`Unknown toast type ${value}.`)
×
151
      return
×
152
    }
153

154
    this.setAttribute('type', value)
×
155
  }
156

157
  connectedCallback () {
158
    const toasts = /** @type {HTMLElement[]} */ (Array.from(document.querySelectorAll('toast-alert')).reverse())
×
159

160
    for (let i = 1, y = 0; i < toasts.length; i++) {
×
161
      // start at index 1 to push old toasts down with aggregated toast heights plus 10px gap
162
      y += toasts[i - 1].getBoundingClientRect().height + 10
×
163
      toasts[i].style.setProperty('--toast-y', `${y}px`)
×
164
    }
165

166
    if (this.hasAttribute('ttl')) {
×
167
      this.ttl = this.getAttribute('ttl')
×
168
    }
169

170
    if (this.hasAttribute('type')) {
×
171
      this.type = this.getAttribute('type')
×
172
    } else {
173
      this.type = ToastTypes.Error
×
174
    }
175

176
    this.shadowRoot.addEventListener('click', this)
×
177
    this.addEventListener('animationend', this)
×
178
  }
179

180
  /** @param {Event} e */
181
  handleEvent (e) {
182
    switch (true) {
×
183
      case e.target instanceof HTMLElement && e.target.matches('button'):
×
184
        this.remove()
×
185
        window.gtag('event', 'toast_alert', { action: 'dismiss' })
×
186
        break
×
187
      case e instanceof AnimationEvent && e.animationName === 'fade-out':
×
188
        this.remove()
×
189
        window.gtag('event', 'toast_alert', { action: 'faded' })
×
190
        break
×
191
    }
192
  }
193

194
  disconnectedCallback () {
195
    this.shadowRoot.removeEventListener('click', this)
×
196
    this.removeEventListener('animationend', this)
×
197
  }
198
})
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc