|
24 | 24 | padding: 0; |
25 | 25 |
|
26 | 26 | & button[aria-pressed="true"] { |
27 | | - color: red; |
| 27 | + color: purple; |
28 | 28 | } |
29 | 29 | } |
30 | 30 |
|
|
35 | 35 | display: block; |
36 | 36 | } |
37 | 37 | } |
| 38 | + |
| 39 | + lazy-load .error { |
| 40 | + color: red; |
| 41 | + } |
38 | 42 | </style> |
39 | 43 |
|
40 | 44 | <void-component id="void"> |
@@ -104,11 +108,11 @@ <h1>Hello from Server</h1> |
104 | 108 | </greeting-configurator> |
105 | 109 | <lazy-load src="/test/mock/lazy-load.html" id="lazy-success"> |
106 | 110 | <p class="loading" role="status">Loading...</p> |
107 | | - <p class="error" role="alert" aria-live="polite"></p> |
| 111 | + <p class="error" role="alert" aria-live="polite" hidden></p> |
108 | 112 | </lazy-load> |
109 | 113 | <lazy-load src="/test/mock/404.html" id="lazy-error"> |
110 | 114 | <p class="loading" role="status">Loading...</p> |
111 | | - <p class="error" role="alert" aria-live="polite"></p> |
| 115 | + <p class="error" role="alert" aria-live="polite" hidden></p> |
112 | 116 | </lazy-load> |
113 | 117 | <tab-list> |
114 | 118 | <menu> |
@@ -265,41 +269,46 @@ <h2>Tab 3</h2> |
265 | 269 | class LazyLoad extends UIElement { |
266 | 270 | static states = { |
267 | 271 | content: '', |
268 | | - error: '' |
| 272 | + error: '', |
| 273 | + loaded: false |
269 | 274 | } |
270 | 275 |
|
271 | 276 | async connectedCallback() { |
272 | 277 | super.connectedCallback() |
273 | 278 |
|
274 | | - fetch(this.getAttribute('src')) // TODO ensure 'src' attribute is a valid URL from a trusted source |
275 | | - .then(async response => { |
276 | | - let content = '' |
277 | | - await wait(1500) // we wait a bit, otherwise Promise is already fulfilled when test runs |
278 | | - if (response.ok) content = await response.text() |
279 | | - else this.set('error', response.statusText) |
280 | | - this.set('content', content) |
281 | | - }) |
282 | | - .catch(error => this.set('error', error)) |
| 279 | + const url = this.getAttribute('src') // ⚠️ Ensure 'src' is from a trusted source |
| 280 | + if (url) { |
| 281 | + try { |
| 282 | + const response = await fetch(url) |
| 283 | + this.set('loaded', true) |
| 284 | + if (response.ok) { |
| 285 | + const text = await response.text() |
| 286 | + this.set('content', text) |
| 287 | + } else { |
| 288 | + this.set('error', response.statusText) |
| 289 | + } |
| 290 | + } catch (error) { |
| 291 | + this.set('error', error.message) |
| 292 | + } |
| 293 | + } else { |
| 294 | + this.set('error', 'Missing required attribute: src') |
| 295 | + } |
283 | 296 |
|
284 | | - const loadingEl = this.querySelector('.loading') |
285 | | - const errorEl = this.querySelector('.error') |
| 297 | + // Hide loading element when 'loaded' becomes true |
| 298 | + this.first('.loading').sync(toggleAttribute('hidden', 'loaded')) |
286 | 299 |
|
287 | | - effect(() => { |
288 | | - const error = this.get('error') |
289 | | - if (error) { |
290 | | - loadingEl.remove() // remove placeholder for pending state |
291 | | - errorEl.textContent = error // fill error message |
292 | | - } |
293 | | - }) |
| 300 | + // Set error text when 'error' is set |
| 301 | + this.first('.error').sync( |
| 302 | + toggleAttribute('hidden', () => !this.get('error')), |
| 303 | + setText('error') |
| 304 | + ) |
294 | 305 |
|
| 306 | + // Set innerHTML of Shadow Root when 'content' is set |
295 | 307 | effect(() => { |
296 | 308 | const content = this.get('content') |
297 | 309 | if (content) { |
298 | | - // console.log(content) |
299 | | - this.root = this.shadowRoot || this.attachShadow({ mode: 'open' }) // we use shadow DOM to enUIElementte styles |
300 | | - this.root.innerHTML = content // UNSAFE!, use only trusted sources in 'src' attribute |
301 | | - loadingEl.remove() // remove placeholder for pending state |
302 | | - errorEl.remove() // won't be needed anymore as request was successful |
| 310 | + this.root = this.shadowRoot || this.attachShadow({ mode: 'open' }) |
| 311 | + this.root.innerHTML = content |
303 | 312 | } |
304 | 313 | }) |
305 | 314 | } |
@@ -711,20 +720,21 @@ <h2>Tab 3</h2> |
711 | 720 |
|
712 | 721 | it('should display lazy loaded content', async function () { |
713 | 722 | const lazyComponent = document.getElementById('lazy-success') |
714 | | - await wait(1000) |
| 723 | + await wait(100) |
715 | 724 | const shadow = lazyComponent.shadowRoot |
716 | 725 | assert.instanceOf(shadow, DocumentFragment, 'Should have a shadow root') |
717 | | - // assert.equal(normalizeText(shadow.querySelector('p').textContent), 'Lazy loaded content', 'Should display lazy loaded content') |
718 | | - // assert.equal(lazyComponent.querySelector('.loading'), null, 'Should no longer have a loading status') |
719 | | - // assert.equal(lazyComponent.querySelector('.error'), null, 'Should no longer have an error container') |
| 726 | + assert.equal(normalizeText(shadow.querySelector('p').textContent), 'Lazy loaded content', 'Should display lazy loaded content') |
| 727 | + assert.equal(lazyComponent.querySelector('.error').hidden, true, 'Should hide error container') |
| 728 | + assert.equal(lazyComponent.querySelector('.loading').hidden, true, 'Should hide loading status') |
720 | 729 | }) |
721 | 730 |
|
722 | 731 | it('should display error message', async function () { |
723 | 732 | const lazyComponent = document.getElementById('lazy-error') |
724 | 733 | await wait(100) |
725 | 734 | assert.equal(normalizeText(lazyComponent.querySelector('.error').textContent), 'Not Found', 'Should display error message') |
726 | | - assert.equal(lazyComponent.querySelector('.loading'), null, 'Should no longer have a loading status') |
| 735 | + assert.equal(lazyComponent.querySelector('.loading').hidden, true, 'Should hide loading status') |
727 | 736 | assert.equal(lazyComponent.shadowRoot, null, 'Should not have a shadow root') |
| 737 | + |
728 | 738 | }) |
729 | 739 |
|
730 | 740 | }) |
|
0 commit comments