diff --git a/src/capture-eye.ts b/src/capture-eye.ts index 729e1a2..4dfbd59 100644 --- a/src/capture-eye.ts +++ b/src/capture-eye.ts @@ -89,6 +89,11 @@ export class CaptureEye extends LitElement { console.debug(CaptureEyeModal.name); // The line ensures CaptureEyeModal is included in compilation. } + override disconnectedCallback() { + super.disconnectedCallback(); + ModalManager.getInstance().removeModal(); + } + get isOpened() { return !ModalManager.getInstance().modalHidden; } @@ -99,7 +104,7 @@ export class CaptureEye extends LitElement { } close() { - ModalManager.getInstance().hideModal(); + ModalManager.getInstance().removeModal(); } private buttonTemplate() { @@ -143,11 +148,6 @@ export class CaptureEye extends LitElement { `; } - override async connectedCallback() { - super.connectedCallback(); - ModalManager.getInstance().initializeModal(); - } - openEye(event?: Event) { if (event) { event.stopPropagation(); diff --git a/src/modal/modal-manager.ts b/src/modal/modal-manager.ts index c04ed31..a2bf385 100644 --- a/src/modal/modal-manager.ts +++ b/src/modal/modal-manager.ts @@ -31,24 +31,19 @@ export class ModalManager { return ModalManager.instance; } - initializeModal(): void { - if (!this.modalElement) { - this.modalElement = document.createElement('capture-eye-modal'); - document.body.appendChild(this.modalElement); - } - } - async updateModal(options: ModalOptions, delay = 150): Promise { - if (!this.modalElement) return; - this.modalElement.modalHidden = true; + let modal = this.getModal(); + modal.modalHidden = true; await new Promise((resolve) => setTimeout(resolve, delay)); - const nidChanged = this.modalElement.nid !== options.nid; + const nidChanged = modal.nid !== options.nid; if (nidChanged) { - this.modalElement.clearModalOptions(); + modal.clearModalOptions(); + this.removeModal(); + modal = this.getModal(); } - this.modalElement.modalHidden = false; + modal.modalHidden = false; this.registerRootClickListener(); - this.modalElement.updateModalOptions(options); + modal.updateModalOptions(options); if (nidChanged) { fetchAsset(options.nid).then((assetData) => { @@ -68,11 +63,24 @@ export class ModalManager { } } - hideModal(): void { - if (this.modalElement) { - this.modalElement.modalHidden = true; - this.unregisterRootClickListener(); + removeModal(): void { + if (!this.modalElement) return; + this.modalElement.modalHidden = true; + this.unregisterRootClickListener(); + this.modalElement.remove(); + this.modalElement = null; + } + + private getModal(): CaptureEyeModal { + if (!this.modalElement) { + this.modalElement = document.createElement('capture-eye-modal'); + this.modalElement.addEventListener('remove-capture-eye-modal', () => { + this.removeModal(); + }); + document.body.appendChild(this.modalElement); + return this.modalElement; } + return this.modalElement; } private updateModalAsset( @@ -92,11 +100,9 @@ export class ModalManager { } private handleRootClick = (event: MouseEvent) => { - if ( - this.modalElement && - !this.modalElement.contains(event.target as Node) - ) { - this.hideModal(); + const modal = this.getModal(); + if (!modal.contains(event.target as Node)) { + this.removeModal(); } }; } diff --git a/src/modal/modal-styles.ts b/src/modal/modal-styles.ts index c17b9f8..465e4f1 100644 --- a/src/modal/modal-styles.ts +++ b/src/modal/modal-styles.ts @@ -27,32 +27,43 @@ export function getModalStyles() { .modal { z-index: 1000; - display: flex; justify-content: flex-start; align-items: flex-start; + display: none; opacity: 0; transform: scale(0.5); - transition: opacity 0.3s ease-in-out, transform 0.3s ease-in; + transition: opacity 0.3s ease-in-out, transform 0.3s ease-in, + display 0.3s ease; + transition-behavior: allow-discrete; position: absolute; } .modal-visible { + display: flex; opacity: 1; transform: scale(1); } - .modal-container { - background-color: var(--background-color); - border-radius: var(--border-radius); - width: 20rem; - box-shadow: var(--box-shadow); + @starting-style { + .modal.modal-visible { + opacity: 0; + transform: scale(0.5); + } } .modal-hidden { + display: none; opacity: 0; transform: scale(0.5); } + .modal-container { + background-color: var(--background-color); + border-radius: var(--border-radius); + width: 20rem; + box-shadow: var(--box-shadow); + } + .modal-content { padding: 12px 24px 12px 24px; } diff --git a/src/modal/modal.ts b/src/modal/modal.ts index d6183bc..3d96397 100644 --- a/src/modal/modal.ts +++ b/src/modal/modal.ts @@ -93,13 +93,12 @@ export class CaptureEyeModal extends LitElement { this.stopEngagementZoneRotation(); } - override firstUpdated() { - this.updateModalVisibility(); - } - override updated(changedProperties: Map) { if (changedProperties.has('modalHidden')) { - this.updateModalVisibility(); + const closeButton = this.shadowRoot?.querySelector('.close-button'); + if (this.modalElement && closeButton && !this.modalHidden) { + this.updateModelPosition(closeButton as HTMLElement); + } } } @@ -148,35 +147,6 @@ export class CaptureEyeModal extends LitElement { this._position = undefined; } - private updateModalVisibility() { - const closeButton = this.shadowRoot?.querySelector('.close-button'); - if (this.modalElement && closeButton) { - if (this.modalHidden) { - this.modalElement.classList.add('modal-hidden'); - this.modalElement.classList.remove('modal-visible'); - closeButton.classList.add('close-button-hidden'); - closeButton.classList.remove('close-button-visible'); - // Add a transitionend event listener to move the modal off-screen after the animation - this.modalElement.addEventListener( - 'transitionend', - () => { - if (this.modalHidden) { - this.modalElement.style.top = '-9999px'; - this.modalElement.style.left = '-9999px'; - } - }, - { once: true } - ); - } else { - this.updateModelPosition(closeButton as HTMLElement); - this.modalElement.classList.remove('modal-hidden'); - this.modalElement.classList.add('modal-visible'); - closeButton.classList.remove('close-button-hidden'); - closeButton.classList.add('close-button-visible'); - } - } - } - private remToPixels(rem: number): number { return ( rem * parseFloat(getComputedStyle(document.documentElement).fontSize) @@ -533,7 +503,12 @@ export class CaptureEyeModal extends LitElement { ${this.renderEngagementZone()} -
+
${generateCaptureEyeCloseSvg(color, size)}
@@ -552,8 +527,9 @@ export class CaptureEyeModal extends LitElement { } } - private hideModal() { - this.modalHidden = true; + private emitRemoveEvent() { + // Emit remove event to trigger ModalManager to remove the modal + this.dispatchEvent(new CustomEvent('remove-capture-eye-modal')); } private trackEngagement() { diff --git a/src/test/modal_test.ts b/src/test/modal_test.ts index b78aa54..7402ab1 100644 --- a/src/test/modal_test.ts +++ b/src/test/modal_test.ts @@ -1,5 +1,5 @@ import { html } from 'lit'; -import { fixture, assert, expect } from '@open-wc/testing'; +import { fixture, assert, expect, waitUntil } from '@open-wc/testing'; import { CaptureEyeModal, formatTxHash, @@ -90,9 +90,9 @@ suite('capture-eye-modal', () => { ); }); - test('handles modal visibility', async () => { + test('handles modal visibility correctly', async () => { const el = await fixture( - html`` + html`` ); const modal = el.shadowRoot?.querySelector('.modal'); expect(modal).to.not.be.null; @@ -102,25 +102,33 @@ suite('capture-eye-modal', () => { await el.updateComplete; expect(modal?.classList.contains('modal-hidden')).to.be.false; expect(modal?.classList.contains('modal-visible')).to.be.true; + + el.modalHidden = true; + await el.updateComplete; + expect(modal?.classList.contains('modal-hidden')).to.be.true; }); - test('calls hideModal() when close button is clicked', async () => { + test('emits remove-capture-eye-modal event when close button is clicked', async () => { const el = await fixture( html`` ); const closeButton = el.shadowRoot?.querySelector( '.close-button' ) as HTMLElement; - expect(closeButton).to.exist; + + const eventSpy = sinon.spy(el, 'dispatchEvent'); el.modalHidden = false; await el.updateComplete; closeButton.click(); - await el.updateComplete; - const modal = el.shadowRoot?.querySelector('.modal'); - expect(modal?.classList.contains('modal-hidden')).to.be.true; - expect(el.modalHidden).to.be.true; + expect(eventSpy).to.have.been.calledWith( + sinon.match + .instanceOf(CustomEvent) + .and(sinon.match.has('type', 'remove-capture-eye-modal')) + ); + + eventSpy.restore(); }); test('renders engagement image and link correctly', async () => { @@ -128,12 +136,10 @@ suite('capture-eye-modal', () => { 'https://static-cdn.numbersprotocol.io/capture-eye/capture-ad.png'; const engagementLink = 'https://example.com'; - // Fixture to create the CaptureEyeModal component const el = await fixture(html` `); - // Set the modal options with engagement zones el.updateModalOptions({ nid: '123', engagementZones: [{ image: engagementImage, link: engagementLink }], @@ -299,7 +305,7 @@ suite('capture-eye-modal', () => { expect((el as any)._position).to.equal(undefined); }); - test('modal visibility toggle works with transition end', async () => { + test('modal visibility toggle works', async () => { const el = await fixture(html` `); @@ -309,21 +315,23 @@ suite('capture-eye-modal', () => { el.modalHidden = false; await el.updateComplete; - // Check that the modal is visible - expect(modal.style.top).to.not.equal('-9999px'); - expect(modal.style.left).to.not.equal('-9999px'); + // Wait until the modal becomes visible + await waitUntil( + () => getComputedStyle(modal).display === 'flex', + 'Modal should be visible' + ); + expect(getComputedStyle(modal).display).to.equal('flex'); // Set the modal to hidden el.modalHidden = true; await el.updateComplete; - // Simulate the 'transitionend' event to trigger moving the modal off-screen - modal.dispatchEvent(new Event('transitionend')); - await el.updateComplete; - - // Check that the modal is moved off-screen after hiding - expect(modal.style.top).to.equal('-9999px'); - expect(modal.style.left).to.equal('-9999px'); + // Wait until the modal is hidden + await waitUntil( + () => getComputedStyle(modal).display === 'none', + 'Modal should be hidden' + ); + expect(getComputedStyle(modal).display).to.equal('none'); }); test('tracks engagement when engagement link is clicked', async () => { @@ -389,16 +397,16 @@ suite('capture-eye-modal', () => { await el.updateComplete; // Check that the asset details are rendered - const creator = el.shadowRoot?.querySelector('.top-name') as HTMLElement; - expect(creator.innerText).to.equal(assetData.creator); + const creator = el.shadowRoot?.querySelector('.top-name a') as HTMLElement; + expect(creator.textContent?.trim()).to.equal(assetData.creator); const location = el.shadowRoot?.querySelector( '.top-info:last-child' ) as HTMLElement; - expect(location.innerText).to.equal(assetData.captureLocation); + expect(location.textContent?.trim()).to.equal(assetData.captureLocation); const headline = el.shadowRoot?.querySelector('.headline') as HTMLElement; - expect(headline.innerText).to.equal(assetData.headline); + expect(headline.textContent?.trim()).to.equal(assetData.headline); const img = el.shadowRoot?.querySelector( '.profile-img'