From b4114b5617628679d3a7614627e0d837e0b7ae51 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 13:29:40 +0000 Subject: [PATCH 01/10] Initial plan From 976d9a9b4b44491a65785ab209f49448d3c94bd5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 13:43:04 +0000 Subject: [PATCH 02/10] Complete phone messaging modal implementation Co-authored-by: micahmills <2531805+micahmills@users.noreply.github.com> --- index.html | 8 + .../form/dt-multi-text/dt-multi-text.js | 64 +++++ .../dt-multi-text/dt-multi-text.stories.js | 20 +- .../layout/dt-phone-modal/dt-phone-modal.js | 227 ++++++++++++++++++ src/components/layout/index.js | 1 + 5 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 src/components/layout/dt-phone-modal/dt-phone-modal.js diff --git a/index.html b/index.html index db9550b0..8cdd5043 100644 --- a/index.html +++ b/index.html @@ -52,6 +52,14 @@

Form Elements

label="Connection Field" options='[{"id":"1","label":"John Doe"},{"id":"2","label":"Jane Smith","user":true},{"id":"3","label":"Trevor Virtue","user":true},{"id":"4","label":"Jane Meldrum"}]' > + + diff --git a/src/components/form/dt-multi-text/dt-multi-text.js b/src/components/form/dt-multi-text/dt-multi-text.js index aee869ad..288de228 100644 --- a/src/components/form/dt-multi-text/dt-multi-text.js +++ b/src/components/form/dt-multi-text/dt-multi-text.js @@ -4,6 +4,7 @@ import { classMap } from 'lit/directives/class-map.js'; import { when } from 'lit/directives/when.js'; import { DtText } from '../dt-text/dt-text.js'; import '../../icons/dt-icon.js'; +import '../../layout/dt-phone-modal/dt-phone-modal.js'; /** * Field to edit multiple text values with ability to add/remove values. @@ -144,6 +145,37 @@ export class DtMultiText extends DtText { .field-container:has(.btn-remove) ~ .icon-overlay { inset-inline-end: 5.5rem; } + + /* Phone messaging button styles */ + .phone-messaging-button { + margin-left: 0.5rem; + padding: 0.5rem; + border: none; + background: var(--dt-form-background-color, #f9f9f9); + border-radius: 0.25rem; + color: var(--dt-form-text-color, #333); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 2.5rem; + min-height: 2.5rem; + transition: background-color 0.2s ease; + } + + .phone-messaging-button:hover { + background: var(--dt-form-hover-background-color, #e5e5e5); + } + + .phone-messaging-button:focus { + outline: 2px solid var(--dt-form-focus-color, #0073aa); + outline-offset: 2px; + } + + .phone-messaging-button dt-icon { + width: 1.2rem; + height: 1.2rem; + } `, ]; } @@ -252,7 +284,23 @@ export class DtMultiText extends DtText { } } + _openPhoneModal(e) { + const phoneNumber = e.currentTarget.dataset.phoneNumber; + if (phoneNumber) { + // Get or create the phone modal + let modal = document.querySelector('dt-phone-modal'); + if (!modal) { + modal = document.createElement('dt-phone-modal'); + document.body.appendChild(modal); + } + modal.open(phoneNumber); + } + } + _inputFieldTemplate(item, itemCount) { + const isPhone = this.type === 'phone'; + const hasPhoneValue = isPhone && item.value && item.value.trim() !== ''; + return html`
+ ${when( + hasPhoneValue, + () => html` + + `, + )} + ${when( itemCount > 1 || item.key || item.value, () => html` diff --git a/src/components/form/dt-multi-text/dt-multi-text.stories.js b/src/components/form/dt-multi-text/dt-multi-text.stories.js index 0f28c52a..2f8d31dc 100644 --- a/src/components/form/dt-multi-text/dt-multi-text.stories.js +++ b/src/components/form/dt-multi-text/dt-multi-text.stories.js @@ -22,7 +22,7 @@ export default { placeholder: { control: 'text' }, type: { control: 'select', - options: ['text', 'password', 'email', 'number', 'tel', 'url'], + options: ['text', 'password', 'email', 'number', 'tel', 'url', 'phone'], defaultValue: 'text', }, disabled: { control: 'boolean' }, @@ -177,6 +177,24 @@ password.args = { type: 'password', }; +export const phone = Template.bind({}); +phone.args = { + type: 'phone', + label: 'Phone Numbers', + value: [ + { + verified: false, + value: '+1-555-123-4567', + key: 'phone_1', + }, + { + verified: false, + value: '19995551234', + key: 'phone_2', + }, + ], +}; + export const requiredCustomMessage = Template.bind({}); requiredCustomMessage.args = { required: true, diff --git a/src/components/layout/dt-phone-modal/dt-phone-modal.js b/src/components/layout/dt-phone-modal/dt-phone-modal.js new file mode 100644 index 00000000..65770f0e --- /dev/null +++ b/src/components/layout/dt-phone-modal/dt-phone-modal.js @@ -0,0 +1,227 @@ +import { html, css } from 'lit'; +import { msg } from '@lit/localize'; +import DtBase from '../../dt-base.js'; +import '../dt-modal/dt-modal.js'; +import '../../icons/dt-icon.js'; + +/** + * Modal component for phone messaging services integration + * Displays options to open phone numbers with various messaging apps + */ +export class DtPhoneModal extends DtBase { + static get styles() { + return css` + :host { + display: none; + } + + dt-modal { + --dt-modal-padding: 1rem; + } + + .messaging-services { + list-style: none; + margin: 0; + padding: 0; + } + + .messaging-service { + display: flex; + align-items: center; + padding: 0.75rem; + margin: 0.5rem 0; + border: 1px solid var(--dt-form-border-color, #ccc); + border-radius: 0.25rem; + background: var(--dt-form-background-color, #fff); + text-decoration: none; + color: inherit; + min-height: 60px; + transition: background-color 0.2s ease; + } + + .messaging-service:hover { + background: var(--dt-form-hover-background-color, #f5f5f5); + } + + .messaging-service:focus { + outline: 2px solid var(--dt-form-focus-color, #0073aa); + outline-offset: 2px; + } + + .service-icon { + width: 24px; + height: 24px; + margin-right: 0.75rem; + flex-shrink: 0; + } + + .service-text { + flex-grow: 1; + font-size: 1rem; + } + + .phone-number { + font-weight: bold; + color: var(--dt-form-text-color, #333); + } + + .service-name { + color: var(--dt-form-text-color-secondary, #666); + } + `; + } + + static get properties() { + return { + phoneNumber: { type: String }, + isOpen: { type: Boolean }, + messagingServices: { type: Object }, + }; + } + + constructor() { + super(); + this.phoneNumber = ''; + this.isOpen = false; + this.messagingServices = this._getDefaultMessagingServices(); + } + + _getDefaultMessagingServices() { + return { + phone: { + name: 'Phone', + icon: 'mdi:phone', + link: 'tel:PHONE_NUMBER', + }, + whatsapp: { + name: 'WhatsApp', + icon: 'mdi:whatsapp', + link: 'https://wa.me/PHONE_NUMBER_NO_PLUS', + }, + viber: { + name: 'Viber', + icon: 'mdi:viber', + link: 'viber://chat?number=PHONE_NUMBER', + }, + signal: { + name: 'Signal', + icon: 'mdi:signal', + link: 'sgnl://signal.me/#p/PHONE_NUMBER', + }, + telegram: { + name: 'Telegram', + icon: 'mdi:telegram', + link: 'https://t.me/PHONE_NUMBER_NO_PLUS', + }, + }; + } + + open(phoneNumber) { + this.phoneNumber = phoneNumber; + this.isOpen = true; + this.style.display = 'block'; + + // Open the modal + this.updateComplete.then(() => { + const modal = this.shadowRoot.querySelector('dt-modal'); + if (modal) { + modal.dispatchEvent(new CustomEvent('open')); + } + }); + } + + close() { + this.isOpen = false; + this.style.display = 'none'; + + // Close the modal + const modal = this.shadowRoot.querySelector('dt-modal'); + if (modal) { + modal.dispatchEvent(new CustomEvent('close')); + } + } + + _handleModalClose() { + this.close(); + } + + _createServiceLink(service, phoneNumber) { + // Clean phone number - remove all non-digit characters + const cleanNumber = phoneNumber.replace(/\D/g, ''); + // For PHONE_NUMBER_NO_PLUS, remove the leading 1 if it's a US/CA number + const cleanNumberNoPlus = cleanNumber.startsWith('1') ? cleanNumber.substring(1) : cleanNumber; + + return service.link + .replace(/PHONE_NUMBER_NO_PLUS/g, cleanNumberNoPlus) + .replace(/PHONE_NUMBER/g, phoneNumber); + } + + _renderMessagingService(serviceKey, service) { + const link = this._createServiceLink(service, this.phoneNumber); + + return html` + + +
+ ${msg('Open')} ${this.phoneNumber} + ${msg('with')} ${service.name} +
+
+ `; + } + + _renderMessagingServices() { + // Add iMessage for Apple devices + const services = { ...this.messagingServices }; + + // Check if user is on Apple device + const isApple = /Mac|iPhone|iPad|iPod/.test(navigator.platform) || + (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); + + if (isApple) { + services.imessage = { + name: 'iMessage', + icon: 'mdi:message-text', + link: 'imessage:PHONE_NUMBER', + }; + } + + return Object.entries(services).map(([key, service]) => + this._renderMessagingService(key, service) + ); + } + + render() { + if (!this.isOpen) { + return html``; + } + + return html` + +
+
+ ${this._renderMessagingServices()} +
+
+
+ `; + } +} + +window.customElements.define('dt-phone-modal', DtPhoneModal); \ No newline at end of file diff --git a/src/components/layout/index.js b/src/components/layout/index.js index d974d31c..c181f1ca 100644 --- a/src/components/layout/index.js +++ b/src/components/layout/index.js @@ -1,4 +1,5 @@ export { DtAlert } from './dt-alert/dt-alert.js'; export { DtList } from './dt-list/dt-list.js'; export { DtModal } from './dt-modal/dt-modal.js'; +export { DtPhoneModal } from './dt-phone-modal/dt-phone-modal.js'; export { DtTile } from './dt-tile/dt-tile.js'; From 5e942807e5ec27605a503a32f3fd4a249c14fe53 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 14:00:31 +0000 Subject: [PATCH 03/10] Update phone messaging button to use inline styling with "Open" text Co-authored-by: micahmills <2531805+micahmills@users.noreply.github.com> --- .../form/dt-multi-text/dt-multi-text.js | 44 ++++++------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/src/components/form/dt-multi-text/dt-multi-text.js b/src/components/form/dt-multi-text/dt-multi-text.js index 288de228..e2167384 100644 --- a/src/components/form/dt-multi-text/dt-multi-text.js +++ b/src/components/form/dt-multi-text/dt-multi-text.js @@ -146,35 +146,19 @@ export class DtMultiText extends DtText { inset-inline-end: 5.5rem; } - /* Phone messaging button styles */ - .phone-messaging-button { - margin-left: 0.5rem; - padding: 0.5rem; - border: none; - background: var(--dt-form-background-color, #f9f9f9); - border-radius: 0.25rem; - color: var(--dt-form-text-color, #333); - cursor: pointer; - display: inline-flex; + .input-addon.btn-phone-open { + color: var(--primary-color, #0073aa); + display: flex; align-items: center; - justify-content: center; - min-width: 2.5rem; - min-height: 2.5rem; - transition: background-color 0.2s ease; - } - - .phone-messaging-button:hover { - background: var(--dt-form-hover-background-color, #e5e5e5); - } - - .phone-messaging-button:focus { - outline: 2px solid var(--dt-form-focus-color, #0073aa); - outline-offset: 2px; - } - - .phone-messaging-button dt-icon { - width: 1.2rem; - height: 1.2rem; + gap: 0.25rem; + white-space: nowrap; + &:disabled { + color: var(--dt-text-placeholder-color, #999); + } + &:hover:not([disabled]) { + background-color: var(--primary-color, #0073aa); + color: var(--dt-multi-text-button-hover-color, #ffffff); + } } `, ]; @@ -321,14 +305,14 @@ export class DtMultiText extends DtText { hasPhoneValue, () => html` `, )} From 194c0605dfb317814400c63560f64a7cffb52206 Mon Sep 17 00:00:00 2001 From: Micah Mills Date: Sun, 14 Sep 2025 17:21:01 +0300 Subject: [PATCH 04/10] Improve phone modal handling and messaging services Refactored dt-multi-text to support 'phone-intl' type. Updated dt-phone-modal to use static methods for messaging services --- .../form/dt-multi-text/dt-multi-text.js | 8 +-- .../layout/dt-phone-modal/dt-phone-modal.js | 50 ++++++++++--------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/components/form/dt-multi-text/dt-multi-text.js b/src/components/form/dt-multi-text/dt-multi-text.js index e2167384..722d2f2f 100644 --- a/src/components/form/dt-multi-text/dt-multi-text.js +++ b/src/components/form/dt-multi-text/dt-multi-text.js @@ -269,7 +269,8 @@ export class DtMultiText extends DtText { } _openPhoneModal(e) { - const phoneNumber = e.currentTarget.dataset.phoneNumber; + // Use 'this' to comply with class method expectations + const { phoneNumber } = e.currentTarget.dataset; if (phoneNumber) { // Get or create the phone modal let modal = document.querySelector('dt-phone-modal'); @@ -277,12 +278,14 @@ export class DtMultiText extends DtText { modal = document.createElement('dt-phone-modal'); document.body.appendChild(modal); } + // Optionally, store a reference to the modal on this instance if needed + this._phoneModal = modal; modal.open(phoneNumber); } } _inputFieldTemplate(item, itemCount) { - const isPhone = this.type === 'phone'; + const isPhone = this.type === 'phone' || this.type === 'phone-intl'; const hasPhoneValue = isPhone && item.value && item.value.trim() !== ''; return html` @@ -316,7 +319,6 @@ export class DtMultiText extends DtText { `, )} - ${when( itemCount > 1 || item.key || item.value, () => html` diff --git a/src/components/layout/dt-phone-modal/dt-phone-modal.js b/src/components/layout/dt-phone-modal/dt-phone-modal.js index 65770f0e..9a76c14e 100644 --- a/src/components/layout/dt-phone-modal/dt-phone-modal.js +++ b/src/components/layout/dt-phone-modal/dt-phone-modal.js @@ -83,10 +83,10 @@ export class DtPhoneModal extends DtBase { super(); this.phoneNumber = ''; this.isOpen = false; - this.messagingServices = this._getDefaultMessagingServices(); + this.messagingServices = DtPhoneModal._getDefaultMessagingServices(); } - _getDefaultMessagingServices() { + static _getDefaultMessagingServices() { return { phone: { name: 'Phone', @@ -120,7 +120,7 @@ export class DtPhoneModal extends DtBase { this.phoneNumber = phoneNumber; this.isOpen = true; this.style.display = 'block'; - + // Open the modal this.updateComplete.then(() => { const modal = this.shadowRoot.querySelector('dt-modal'); @@ -133,7 +133,7 @@ export class DtPhoneModal extends DtBase { close() { this.isOpen = false; this.style.display = 'none'; - + // Close the modal const modal = this.shadowRoot.querySelector('dt-modal'); if (modal) { @@ -145,36 +145,39 @@ export class DtPhoneModal extends DtBase { this.close(); } - _createServiceLink(service, phoneNumber) { + static _createServiceLink(service, phoneNumber) { // Clean phone number - remove all non-digit characters - const cleanNumber = phoneNumber.replace(/\D/g, ''); + const cleanNumber = phoneNumber.replace(/\D/g, ''); // For PHONE_NUMBER_NO_PLUS, remove the leading 1 if it's a US/CA number - const cleanNumberNoPlus = cleanNumber.startsWith('1') ? cleanNumber.substring(1) : cleanNumber; - + const cleanNumberNoPlus = cleanNumber.startsWith('1') + ? cleanNumber.substring(1) + : cleanNumber; + return service.link .replace(/PHONE_NUMBER_NO_PLUS/g, cleanNumberNoPlus) .replace(/PHONE_NUMBER/g, phoneNumber); } _renderMessagingService(serviceKey, service) { - const link = this._createServiceLink(service, this.phoneNumber); - + const link = DtPhoneModal._createServiceLink(service, this.phoneNumber); + return html` - -
- ${msg('Open')} ${this.phoneNumber} - ${msg('with')} ${service.name} + ${msg('Open')} + ${this.phoneNumber} ${msg('with')} + ${service.name}
`; @@ -183,11 +186,12 @@ export class DtPhoneModal extends DtBase { _renderMessagingServices() { // Add iMessage for Apple devices const services = { ...this.messagingServices }; - + // Check if user is on Apple device - const isApple = /Mac|iPhone|iPad|iPod/.test(navigator.platform) || - (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); - + const isApple = + /Mac|iPhone|iPad|iPod/.test(navigator.platform) || + (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); + if (isApple) { services.imessage = { name: 'iMessage', @@ -196,8 +200,8 @@ export class DtPhoneModal extends DtBase { }; } - return Object.entries(services).map(([key, service]) => - this._renderMessagingService(key, service) + return Object.entries(services).map(([key, service]) => + this._renderMessagingService(key, service), ); } @@ -224,4 +228,4 @@ export class DtPhoneModal extends DtBase { } } -window.customElements.define('dt-phone-modal', DtPhoneModal); \ No newline at end of file +window.customElements.define('dt-phone-modal', DtPhoneModal); From c92ba59b5349afdd1a4b766af492ae09f89e1cba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 14:29:16 +0000 Subject: [PATCH 05/10] Add comprehensive tests for phone messaging functionality Co-authored-by: micahmills <2531805+micahmills@users.noreply.github.com> --- .../form/dt-multi-text/dt-multi-text.test.js | 140 ++++++++++++ .../dt-phone-modal/dt-phone-modal.test.js | 210 ++++++++++++++++++ 2 files changed, 350 insertions(+) create mode 100644 src/components/layout/dt-phone-modal/dt-phone-modal.test.js diff --git a/src/components/form/dt-multi-text/dt-multi-text.test.js b/src/components/form/dt-multi-text/dt-multi-text.test.js index 9aa08ea1..120c9514 100644 --- a/src/components/form/dt-multi-text/dt-multi-text.test.js +++ b/src/components/form/dt-multi-text/dt-multi-text.test.js @@ -2,6 +2,7 @@ import { html } from 'lit'; import { fixture, expect, oneEvent, aTimeout, nextFrame } from '@open-wc/testing'; import { sendKeys } from '@web/test-runner-commands'; import './dt-multi-text.js'; +import '../../layout/dt-phone-modal/dt-phone-modal.js'; describe('DtMultiText', () => { @@ -168,4 +169,143 @@ describe('DtMultiText', () => { expect(detail.newValue[0].value).to.equal('Value 1') expect(detail.newValue[0].delete).to.equal(true); }); + + describe('Phone type functionality', () => { + it('shows "Open" button when type is phone and field has value', async () => { + const el = await fixture( + html`` + ); + + await nextFrame(); + + const phoneButton = el.shadowRoot.querySelector('.btn-phone-open'); + expect(phoneButton).to.exist; + expect(phoneButton.textContent.trim()).to.include('Open'); + expect(phoneButton.dataset.phoneNumber).to.equal('+1-555-123-4567'); + }); + + it('hides "Open" button when phone field is empty', async () => { + const el = await fixture( + html`` + ); + + await nextFrame(); + + const phoneButton = el.shadowRoot.querySelector('.btn-phone-open'); + expect(phoneButton).to.not.exist; + }); + + it('shows "Open" button when type is phone-intl and field has value', async () => { + const el = await fixture( + html`` + ); + + await nextFrame(); + + const phoneButton = el.shadowRoot.querySelector('.btn-phone-open'); + expect(phoneButton).to.exist; + expect(phoneButton.dataset.phoneNumber).to.equal('+33123456789'); + }); + + it('does not show "Open" button for non-phone types', async () => { + const el = await fixture( + html`` + ); + + await nextFrame(); + + const phoneButton = el.shadowRoot.querySelector('.btn-phone-open'); + expect(phoneButton).to.not.exist; + }); + + it('opens phone modal when "Open" button is clicked', async () => { + const el = await fixture( + html`` + ); + + await nextFrame(); + + // Remove any existing phone modals + const existingModals = document.querySelectorAll('dt-phone-modal'); + existingModals.forEach(modal => modal.remove()); + + const phoneButton = el.shadowRoot.querySelector('.btn-phone-open'); + expect(phoneButton).to.exist; + + phoneButton.click(); + await aTimeout(50); + + // Check that a phone modal was created + const phoneModal = document.querySelector('dt-phone-modal'); + expect(phoneModal).to.exist; + expect(phoneModal.phoneNumber).to.equal('+1-555-123-4567'); + expect(phoneModal.isOpen).to.be.true; + + // Clean up + phoneModal.remove(); + }); + + it('updates "Open" button visibility when phone value changes', async () => { + const el = await fixture( + html`` + ); + + await nextFrame(); + + // Initially no button should be visible + let phoneButton = el.shadowRoot.querySelector('.btn-phone-open'); + expect(phoneButton).to.not.exist; + + // Add a phone number value + const input = el.shadowRoot.querySelector('input'); + input.focus(); + input.value = '+1-555-123-4567'; + input.dispatchEvent(new Event('change')); + + await nextFrame(); + + // Now button should be visible + phoneButton = el.shadowRoot.querySelector('.btn-phone-open'); + expect(phoneButton).to.exist; + expect(phoneButton.dataset.phoneNumber).to.equal('+1-555-123-4567'); + + // Clear the value + input.value = ''; + input.dispatchEvent(new Event('change')); + + await nextFrame(); + + // Button should be hidden again + phoneButton = el.shadowRoot.querySelector('.btn-phone-open'); + expect(phoneButton).to.not.exist; + }); + }); }); diff --git a/src/components/layout/dt-phone-modal/dt-phone-modal.test.js b/src/components/layout/dt-phone-modal/dt-phone-modal.test.js new file mode 100644 index 00000000..a907ee18 --- /dev/null +++ b/src/components/layout/dt-phone-modal/dt-phone-modal.test.js @@ -0,0 +1,210 @@ +import { html } from 'lit'; +import { fixture, expect, aTimeout, nextFrame } from '@open-wc/testing'; +import { DtPhoneModal } from './dt-phone-modal.js'; + +describe('DtPhoneModal', () => { + it('can be instantiated', async () => { + const el = await fixture(html``); + expect(el).to.exist; + expect(el.phoneNumber).to.equal(''); + expect(el.isOpen).to.be.false; + }); + + it('has default messaging services', async () => { + const el = await fixture(html``); + + expect(el.messagingServices).to.exist; + expect(el.messagingServices.phone).to.exist; + expect(el.messagingServices.whatsapp).to.exist; + expect(el.messagingServices.viber).to.exist; + expect(el.messagingServices.signal).to.exist; + expect(el.messagingServices.telegram).to.exist; + }); + + it('opens with phone number', async () => { + const el = await fixture(html``); + + el.open('+1-555-123-4567'); + await nextFrame(); + + expect(el.phoneNumber).to.equal('+1-555-123-4567'); + expect(el.isOpen).to.be.true; + expect(el.style.display).to.equal('block'); + }); + + it('closes modal', async () => { + const el = await fixture(html``); + + // Open first + el.open('+1-555-123-4567'); + await nextFrame(); + expect(el.isOpen).to.be.true; + + // Then close + el.close(); + await nextFrame(); + + expect(el.isOpen).to.be.false; + expect(el.style.display).to.equal('none'); + }); + + it('renders modal content when open', async () => { + const el = await fixture(html``); + + el.open('+1-555-123-4567'); + await nextFrame(); + + const modal = el.shadowRoot.querySelector('dt-modal'); + expect(modal).to.exist; + + const services = el.shadowRoot.querySelectorAll('.messaging-service'); + expect(services.length).to.be.at.least(5); // At least 5 services + }); + + it('does not render when closed', async () => { + const el = await fixture(html``); + + const modal = el.shadowRoot.querySelector('dt-modal'); + expect(modal).to.not.exist; + }); + + it('creates correct service links for different messaging apps', async () => { + const phoneNumber = '+1-555-123-4567'; + const services = { + phone: { link: 'tel:PHONE_NUMBER' }, + whatsapp: { link: 'https://wa.me/PHONE_NUMBER_NO_PLUS' }, + telegram: { link: 'https://t.me/PHONE_NUMBER_NO_PLUS' }, + viber: { link: 'viber://chat?number=PHONE_NUMBER' }, + signal: { link: 'sgnl://signal.me/#p/PHONE_NUMBER' } + }; + + // Test tel link + const telLink = DtPhoneModal._createServiceLink(services.phone, phoneNumber); + expect(telLink).to.equal('tel:+1-555-123-4567'); + + // Test WhatsApp link (should remove plus and leading 1) + const whatsappLink = DtPhoneModal._createServiceLink(services.whatsapp, phoneNumber); + expect(whatsappLink).to.equal('https://wa.me/5551234567'); + + // Test Telegram link (should remove plus and leading 1) + const telegramLink = DtPhoneModal._createServiceLink(services.telegram, phoneNumber); + expect(telegramLink).to.equal('https://t.me/5551234567'); + + // Test Viber link + const viberLink = DtPhoneModal._createServiceLink(services.viber, phoneNumber); + expect(viberLink).to.equal('viber://chat?number=+1-555-123-4567'); + + // Test Signal link + const signalLink = DtPhoneModal._createServiceLink(services.signal, phoneNumber); + expect(signalLink).to.equal('sgnl://signal.me/#p/+1-555-123-4567'); + }); + + it('handles phone numbers without leading 1', async () => { + const phoneNumber = '+33123456789'; + const service = { link: 'https://wa.me/PHONE_NUMBER_NO_PLUS' }; + + const link = DtPhoneModal._createServiceLink(service, phoneNumber); + expect(link).to.equal('https://wa.me/33123456789'); + }); + + it('renders correct number of messaging services', async () => { + const el = await fixture(html``); + + el.open('+1-555-123-4567'); + await nextFrame(); + + const serviceLinks = el.shadowRoot.querySelectorAll('.messaging-service'); + + // Should have at least 5 base services (phone, whatsapp, viber, signal, telegram) + expect(serviceLinks.length).to.be.at.least(5); + + // Check that each service has proper structure + serviceLinks.forEach(service => { + expect(service.href).to.exist; + expect(service.querySelector('dt-icon')).to.exist; + expect(service.querySelector('.service-text')).to.exist; + }); + }); + + it('includes phone number in service text', async () => { + const el = await fixture(html``); + const phoneNumber = '+1-555-123-4567'; + + el.open(phoneNumber); + await nextFrame(); + + const phoneNumberSpans = el.shadowRoot.querySelectorAll('.phone-number'); + expect(phoneNumberSpans.length).to.be.at.least(1); + + phoneNumberSpans.forEach(span => { + expect(span.textContent).to.equal(phoneNumber); + }); + }); + + it('closes modal when service link is clicked', async () => { + const el = await fixture(html``); + + el.open('+1-555-123-4567'); + await nextFrame(); + + const serviceLink = el.shadowRoot.querySelector('.messaging-service'); + expect(serviceLink).to.exist; + + // Mock the close behavior + let closeCalled = false; + const originalClose = el.close.bind(el); + el.close = () => { + closeCalled = true; + originalClose(); + }; + + serviceLink.click(); + await aTimeout(50); + + expect(closeCalled).to.be.true; + }); + + it('adds iMessage on Apple devices', async () => { + const el = await fixture(html``); + + // Mock Apple device detection + const originalPlatform = navigator.platform; + Object.defineProperty(navigator, 'platform', { + writable: true, + value: 'iPhone' + }); + + el.open('+1-555-123-4567'); + await nextFrame(); + + const serviceLinks = el.shadowRoot.querySelectorAll('.messaging-service'); + const serviceTexts = Array.from(serviceLinks).map(link => + link.querySelector('.service-name').textContent + ); + + expect(serviceTexts).to.include('iMessage'); + + // Restore original platform + Object.defineProperty(navigator, 'platform', { + writable: true, + value: originalPlatform + }); + }); + + it('handles modal close event', async () => { + const el = await fixture(html``); + + el.open('+1-555-123-4567'); + await nextFrame(); + + const modal = el.shadowRoot.querySelector('dt-modal'); + expect(modal).to.exist; + + // Simulate modal close event + modal.dispatchEvent(new CustomEvent('close')); + await nextFrame(); + + expect(el.isOpen).to.be.false; + expect(el.style.display).to.equal('none'); + }); +}); \ No newline at end of file From 1ee46ed3cd5430b4eba29ce88ec5cb29ed4d1b5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 14:59:20 +0000 Subject: [PATCH 06/10] Fix messaging modal icons by implementing direct SVG icons Co-authored-by: micahmills <2531805+micahmills@users.noreply.github.com> --- .../layout/dt-phone-modal/dt-phone-modal.js | 66 +++++++++++++++---- .../dt-phone-modal/dt-phone-modal.test.js | 2 +- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/src/components/layout/dt-phone-modal/dt-phone-modal.js b/src/components/layout/dt-phone-modal/dt-phone-modal.js index 9a76c14e..b266fa71 100644 --- a/src/components/layout/dt-phone-modal/dt-phone-modal.js +++ b/src/components/layout/dt-phone-modal/dt-phone-modal.js @@ -2,7 +2,6 @@ import { html, css } from 'lit'; import { msg } from '@lit/localize'; import DtBase from '../../dt-base.js'; import '../dt-modal/dt-modal.js'; -import '../../icons/dt-icon.js'; /** * Modal component for phone messaging services integration @@ -53,6 +52,15 @@ export class DtPhoneModal extends DtBase { height: 24px; margin-right: 0.75rem; flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + } + + .service-icon svg { + width: 100%; + height: 100%; + color: var(--dt-form-text-color, #333); } .service-text { @@ -90,32 +98,68 @@ export class DtPhoneModal extends DtBase { return { phone: { name: 'Phone', - icon: 'mdi:phone', + icon: 'phone', link: 'tel:PHONE_NUMBER', }, whatsapp: { name: 'WhatsApp', - icon: 'mdi:whatsapp', + icon: 'whatsapp', link: 'https://wa.me/PHONE_NUMBER_NO_PLUS', }, viber: { name: 'Viber', - icon: 'mdi:viber', + icon: 'viber', link: 'viber://chat?number=PHONE_NUMBER', }, signal: { name: 'Signal', - icon: 'mdi:signal', + icon: 'signal', link: 'sgnl://signal.me/#p/PHONE_NUMBER', }, telegram: { name: 'Telegram', - icon: 'mdi:telegram', + icon: 'telegram', link: 'https://t.me/PHONE_NUMBER_NO_PLUS', }, }; } + static _getSvgIcon(iconName) { + const icons = { + phone: html` + + + + `, + whatsapp: html` + + + + `, + viber: html` + + + + `, + signal: html` + + + + `, + telegram: html` + + + + `, + imessage: html` + + + + `, + }; + return icons[iconName] || html`
`; + } + open(phoneNumber) { this.phoneNumber = phoneNumber; this.isOpen = true; @@ -169,11 +213,9 @@ export class DtPhoneModal extends DtBase { rel="noopener noreferrer" @click=${this.close} > - +
+ ${DtPhoneModal._getSvgIcon(service.icon)} +
${msg('Open')} ${this.phoneNumber} ${msg('with')} @@ -195,7 +237,7 @@ export class DtPhoneModal extends DtBase { if (isApple) { services.imessage = { name: 'iMessage', - icon: 'mdi:message-text', + icon: 'imessage', link: 'imessage:PHONE_NUMBER', }; } diff --git a/src/components/layout/dt-phone-modal/dt-phone-modal.test.js b/src/components/layout/dt-phone-modal/dt-phone-modal.test.js index a907ee18..636fbae0 100644 --- a/src/components/layout/dt-phone-modal/dt-phone-modal.test.js +++ b/src/components/layout/dt-phone-modal/dt-phone-modal.test.js @@ -121,7 +121,7 @@ describe('DtPhoneModal', () => { // Check that each service has proper structure serviceLinks.forEach(service => { expect(service.href).to.exist; - expect(service.querySelector('dt-icon')).to.exist; + expect(service.querySelector('.service-icon')).to.exist; expect(service.querySelector('.service-text')).to.exist; }); }); From cf7be7980c25e82c14f4fbfca94dce5c559884fe Mon Sep 17 00:00:00 2001 From: Micah Mills Date: Sun, 14 Sep 2025 20:12:21 +0300 Subject: [PATCH 07/10] updates SVG icons and adds Line as a service --- .../layout/dt-phone-modal/dt-phone-modal.js | 78 ++++++++++++++++--- 1 file changed, 68 insertions(+), 10 deletions(-) diff --git a/src/components/layout/dt-phone-modal/dt-phone-modal.js b/src/components/layout/dt-phone-modal/dt-phone-modal.js index b266fa71..8a2b1efa 100644 --- a/src/components/layout/dt-phone-modal/dt-phone-modal.js +++ b/src/components/layout/dt-phone-modal/dt-phone-modal.js @@ -121,6 +121,11 @@ export class DtPhoneModal extends DtBase { icon: 'telegram', link: 'https://t.me/PHONE_NUMBER_NO_PLUS', }, + line: { + name: 'LINE', + icon: 'line', + link: 'line://ti/p/~PHONE_NUMBER', + }, }; } @@ -128,36 +133,89 @@ export class DtPhoneModal extends DtBase { const icons = { phone: html` - + `, whatsapp: html` - + `, viber: html` - - + + + + + `, signal: html` - - + + + + + `, telegram: html` - + `, imessage: html` - - + + + + `, + line: html` + + `, }; - return icons[iconName] || html`
`; + return ( + icons[iconName] || html`
` + ); } open(phoneNumber) { From b191ffd9163eea7d2018f54799a25fbcca42675e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:05:11 +0000 Subject: [PATCH 08/10] Fix blue button and input height issues in phone modal Co-authored-by: micahmills <2531805+micahmills@users.noreply.github.com> --- .../form/dt-multi-text/dt-multi-text.js | 1 + .../layout/dt-phone-modal/dt-phone-modal.js | 23 ++++++++++------ .../dt-phone-modal/dt-phone-modal.test.js | 27 +++++++++++-------- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/components/form/dt-multi-text/dt-multi-text.js b/src/components/form/dt-multi-text/dt-multi-text.js index 722d2f2f..282c0918 100644 --- a/src/components/form/dt-multi-text/dt-multi-text.js +++ b/src/components/form/dt-multi-text/dt-multi-text.js @@ -152,6 +152,7 @@ export class DtMultiText extends DtText { align-items: center; gap: 0.25rem; white-space: nowrap; + aspect-ratio: auto; &:disabled { color: var(--dt-text-placeholder-color, #999); } diff --git a/src/components/layout/dt-phone-modal/dt-phone-modal.js b/src/components/layout/dt-phone-modal/dt-phone-modal.js index 8a2b1efa..47074e09 100644 --- a/src/components/layout/dt-phone-modal/dt-phone-modal.js +++ b/src/components/layout/dt-phone-modal/dt-phone-modal.js @@ -11,7 +11,7 @@ export class DtPhoneModal extends DtBase { static get styles() { return css` :host { - display: none; + display: block; } dt-modal { @@ -221,7 +221,7 @@ export class DtPhoneModal extends DtBase { open(phoneNumber) { this.phoneNumber = phoneNumber; this.isOpen = true; - this.style.display = 'block'; + this.setAttribute('open', ''); // Open the modal this.updateComplete.then(() => { @@ -233,18 +233,25 @@ export class DtPhoneModal extends DtBase { } close() { + if (!this.isOpen) { + return; // Already closed, prevent infinite loop + } + this.isOpen = false; - this.style.display = 'none'; + this.removeAttribute('open'); - // Close the modal + // Close the modal without triggering another close event const modal = this.shadowRoot.querySelector('dt-modal'); if (modal) { - modal.dispatchEvent(new CustomEvent('close')); + modal._closeModal(); // Call internal close method directly } } _handleModalClose() { - this.close(); + // Don't call close() to avoid infinite loop + // Just update our state + this.isOpen = false; + this.removeAttribute('open'); } static _createServiceLink(service, phoneNumber) { @@ -314,8 +321,8 @@ export class DtPhoneModal extends DtBase {
diff --git a/src/components/layout/dt-phone-modal/dt-phone-modal.test.js b/src/components/layout/dt-phone-modal/dt-phone-modal.test.js index 636fbae0..aad2a9cc 100644 --- a/src/components/layout/dt-phone-modal/dt-phone-modal.test.js +++ b/src/components/layout/dt-phone-modal/dt-phone-modal.test.js @@ -29,7 +29,7 @@ describe('DtPhoneModal', () => { expect(el.phoneNumber).to.equal('+1-555-123-4567'); expect(el.isOpen).to.be.true; - expect(el.style.display).to.equal('block'); + expect(el.hasAttribute('open')).to.be.true; }); it('closes modal', async () => { @@ -45,7 +45,7 @@ describe('DtPhoneModal', () => { await nextFrame(); expect(el.isOpen).to.be.false; - expect(el.style.display).to.equal('none'); + expect(el.hasAttribute('open')).to.be.false; }); it('renders modal content when open', async () => { @@ -150,18 +150,23 @@ describe('DtPhoneModal', () => { const serviceLink = el.shadowRoot.querySelector('.messaging-service'); expect(serviceLink).to.exist; - // Mock the close behavior - let closeCalled = false; - const originalClose = el.close.bind(el); - el.close = () => { - closeCalled = true; - originalClose(); - }; + // Check that modal is open before click + expect(el.isOpen).to.be.true; + + // Add event listener to prevent navigation and check state + serviceLink.addEventListener('click', (e) => { + e.preventDefault(); + // Check state immediately after click handler runs + setTimeout(() => { + expect(el.isOpen).to.be.false; + }, 0); + }, { once: true }); serviceLink.click(); await aTimeout(50); - expect(closeCalled).to.be.true; + // Verify modal is closed + expect(el.isOpen).to.be.false; }); it('adds iMessage on Apple devices', async () => { @@ -205,6 +210,6 @@ describe('DtPhoneModal', () => { await nextFrame(); expect(el.isOpen).to.be.false; - expect(el.style.display).to.equal('none'); + expect(el.hasAttribute('open')).to.be.false; }); }); \ No newline at end of file From 101e82205e85944b7bf91d98c26c6b25c6ef0fb1 Mon Sep 17 00:00:00 2001 From: Micah Mills Date: Fri, 14 Nov 2025 21:07:54 +0200 Subject: [PATCH 09/10] Sanitize phone numbers and improve service link generation Removes hidden Unicode characters from phone numbers before generating service links. Adds special handling for Viber links by encoding the plus sign, ensuring correct formatting for messaging services. --- .../layout/dt-phone-modal/dt-phone-modal.js | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/components/layout/dt-phone-modal/dt-phone-modal.js b/src/components/layout/dt-phone-modal/dt-phone-modal.js index 47074e09..2b3dbcd4 100644 --- a/src/components/layout/dt-phone-modal/dt-phone-modal.js +++ b/src/components/layout/dt-phone-modal/dt-phone-modal.js @@ -236,7 +236,7 @@ export class DtPhoneModal extends DtBase { if (!this.isOpen) { return; // Already closed, prevent infinite loop } - + this.isOpen = false; this.removeAttribute('open'); @@ -255,16 +255,32 @@ export class DtPhoneModal extends DtBase { } static _createServiceLink(service, phoneNumber) { + // Remove hidden Unicode characters (e.g., LRM, RLM, etc.) and clean phone number + const sanitized = phoneNumber.replace( + /[\u200E\u200F\u202A-\u202E\u2066-\u2069]/g, + '', + ); // Clean phone number - remove all non-digit characters - const cleanNumber = phoneNumber.replace(/\D/g, ''); + const cleanNumber = sanitized.replace(/\D/g, ''); // For PHONE_NUMBER_NO_PLUS, remove the leading 1 if it's a US/CA number const cleanNumberNoPlus = cleanNumber.startsWith('1') ? cleanNumber.substring(1) : cleanNumber; - return service.link - .replace(/PHONE_NUMBER_NO_PLUS/g, cleanNumberNoPlus) - .replace(/PHONE_NUMBER/g, phoneNumber); + let resultLink = service.link; + // For Viber, encode + as %2B + if (service.icon === 'viber') { + // Use sanitized number, but encode + if present + const viberNumber = sanitized.startsWith('+') + ? `%2B${sanitized.substring(1)}` + : sanitized; + resultLink = resultLink.replace(/PHONE_NUMBER/g, viberNumber); + } else { + resultLink = resultLink + .replace(/PHONE_NUMBER_NO_PLUS/g, cleanNumberNoPlus) + .replace(/PHONE_NUMBER/g, sanitized); + } + return resultLink; } _renderMessagingService(serviceKey, service) { From b2fbaa3080ccbe415111bebffc61e63aa97e984f Mon Sep 17 00:00:00 2001 From: Micah Mills Date: Fri, 14 Nov 2025 21:19:27 +0200 Subject: [PATCH 10/10] Improve phone number sanitization in service link Refines the phone number cleaning logic by removing hidden Unicode characters, spaces, and minus signs, preserving only digits and a leading '+'. Adjusts logic to remove only the leading '+' for PHONE_NUMBER_NO_PLUS instead of handling US/CA numbers specifically. --- src/components/layout/dt-phone-modal/dt-phone-modal.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/layout/dt-phone-modal/dt-phone-modal.js b/src/components/layout/dt-phone-modal/dt-phone-modal.js index 2b3dbcd4..62886498 100644 --- a/src/components/layout/dt-phone-modal/dt-phone-modal.js +++ b/src/components/layout/dt-phone-modal/dt-phone-modal.js @@ -255,15 +255,17 @@ export class DtPhoneModal extends DtBase { } static _createServiceLink(service, phoneNumber) { - // Remove hidden Unicode characters (e.g., LRM, RLM, etc.) and clean phone number - const sanitized = phoneNumber.replace( + // Remove hidden Unicode characters (e.g., LRM, RLM, etc.) + let sanitized = phoneNumber.replace( /[\u200E\u200F\u202A-\u202E\u2066-\u2069]/g, '', ); + // Remove all spaces and minus signs, keep only digits and a leading + + sanitized = sanitized.replace(/[^\d+]/g, ''); // Clean phone number - remove all non-digit characters const cleanNumber = sanitized.replace(/\D/g, ''); - // For PHONE_NUMBER_NO_PLUS, remove the leading 1 if it's a US/CA number - const cleanNumberNoPlus = cleanNumber.startsWith('1') + // For PHONE_NUMBER_NO_PLUS, remove the leading + sign only + const cleanNumberNoPlus = cleanNumber.startsWith('+') ? cleanNumber.substring(1) : cleanNumber;