diff --git a/demo/e2e/modal.spec.ts b/demo/e2e/modal.spec.ts index 2a84b574..b724708e 100644 --- a/demo/e2e/modal.spec.ts +++ b/demo/e2e/modal.spec.ts @@ -92,6 +92,10 @@ for (const scenario of [ height: scenario.modalHeight, top: scenario.height - scenario.modalHeight, }); + // The sheet offset controls its height while its bottom stays at the viewport edge. + await modal.evaluate((el: HTMLElement) => el.style.setProperty('--ion-modal-offset-top', '80px')); + await expect.poll(bounds).toEqual({ width: scenario.modalWidth, height: scenario.height - 80, top: 80 }); + await expect(modal.getByRole('dialog')).toHaveCSS('bottom', '0px'); }); } diff --git a/demo/e2e/screenshot.spec.ts b/demo/e2e/screenshot.spec.ts index 2a8e11f6..5a36546e 100644 --- a/demo/e2e/screenshot.spec.ts +++ b/demo/e2e/screenshot.spec.ts @@ -209,10 +209,13 @@ test.describe('Screenshot Tests - VerticalBars Back Button', () => { test.describe('Screenshot Tests - Datetime Modal', () => { for (const scenario of [ - { name: 'desktop', width: 1200, height: 800, folded: false, customWidth: false }, - { name: 'phone', width: 390, height: 844, folded: false, customWidth: false }, - { name: 'folded', width: 951, height: 669, folded: true, customWidth: false }, - { name: 'custom-width', width: 1200, height: 800, folded: false, customWidth: true }, + { name: 'desktop', width: 1200, height: 800, folded: false, customWidth: false, customOverlay: false }, + { name: 'phone', width: 390, height: 844, folded: false, customWidth: false, customOverlay: false }, + { name: 'folded', width: 951, height: 669, folded: true, customWidth: false, customOverlay: false }, + { name: 'custom-width', width: 1200, height: 800, folded: false, customWidth: true, customOverlay: false }, + { name: 'custom-overlay', width: 1200, height: 800, folded: false, customWidth: false, customOverlay: true }, + { name: 'custom-overlay-folded', width: 951, height: 669, folded: true, customWidth: false, customOverlay: true }, + { name: 'custom-overlay-phone', width: 390, height: 844, folded: false, customWidth: false, customOverlay: true }, ]) { for (const dark of [false, true]) { test(`should match datetime modal ${scenario.name}${dark ? ' dark' : ''}`, async ({ page }) => { @@ -220,7 +223,7 @@ test.describe('Screenshot Tests - Datetime Modal', () => { await page.setViewportSize({ width: scenario.width, height: scenario.height }); await page.goto('/main/index/date-and-time-pickers', { waitUntil: 'networkidle' }); await page.evaluate( - async ({ dark, folded, customWidth }) => { + async ({ dark, folded, customWidth, customOverlay }) => { document.documentElement.classList.toggle('ion-palette-dark', dark); if (folded) document.querySelector('ion-app')!.classList.add('ios-theme-fold-expanded'); if (customWidth) { @@ -228,15 +231,35 @@ test.describe('Screenshot Tests - Datetime Modal', () => { picker.style.width = '360px'; picker.style.maxWidth = 'none'; } + if (customOverlay) { + // Equivalent specificity to an Angular component's scoped ion-modal rule. + const style = document.createElement('style'); + style.textContent = 'ion-modal[data-custom-datetime] { --width: auto; --height: auto; }'; + document.head.append(style); + const modal = document.createElement('ion-modal'); + modal.setAttribute('data-custom-datetime', ''); + modal.keepContentsMounted = true; + const content = document.createElement('div'); + const picker = document.createElement('ion-datetime'); + picker.presentation = 'date'; + picker.value = document.querySelector('ion-datetime#datetime')!.value; + picker.showDefaultTitle = true; + picker.innerHTML = + 'DateCancelApply'; + content.append(picker); + modal.append(content); + document.querySelector('ion-app')!.append(modal); + } await document.fonts.ready; }, - { dark, folded: scenario.folded, customWidth: scenario.customWidth }, + { dark, folded: scenario.folded, customWidth: scenario.customWidth, customOverlay: scenario.customOverlay }, ); - const modal = page.locator('ion-modal.ion-datetime-button-overlay'); + const modal = page.locator(scenario.customOverlay ? 'ion-modal[data-custom-datetime]' : 'ion-modal.ion-datetime-button-overlay'); await modal.evaluate((el) => { el.addEventListener('ionModalDidPresent', () => el.setAttribute('data-test-presented', 'true'), { once: true }); }); - await page.locator('ion-datetime-button button').first().click(); + if (scenario.customOverlay) await modal.evaluate((el: HTMLIonModalElement) => el.present()); + else await page.locator('ion-datetime-button button').first().click(); await expect(modal).toHaveAttribute('data-test-presented', 'true'); await expect(page).toHaveScreenshot(`datetime-modal-${scenario.name}${dark ? '-dark' : ''}.png`, { animations: 'disabled' }); }); diff --git a/demo/e2e/screenshot.spec.ts-snapshots/datetime-modal-custom-overlay-dark.png b/demo/e2e/screenshot.spec.ts-snapshots/datetime-modal-custom-overlay-dark.png new file mode 100644 index 00000000..84e113db Binary files /dev/null and b/demo/e2e/screenshot.spec.ts-snapshots/datetime-modal-custom-overlay-dark.png differ diff --git a/demo/e2e/screenshot.spec.ts-snapshots/datetime-modal-custom-overlay-folded-dark.png b/demo/e2e/screenshot.spec.ts-snapshots/datetime-modal-custom-overlay-folded-dark.png new file mode 100644 index 00000000..bee3483e Binary files /dev/null and b/demo/e2e/screenshot.spec.ts-snapshots/datetime-modal-custom-overlay-folded-dark.png differ diff --git a/demo/e2e/screenshot.spec.ts-snapshots/datetime-modal-custom-overlay-folded.png b/demo/e2e/screenshot.spec.ts-snapshots/datetime-modal-custom-overlay-folded.png new file mode 100644 index 00000000..44b096ce Binary files /dev/null and b/demo/e2e/screenshot.spec.ts-snapshots/datetime-modal-custom-overlay-folded.png differ diff --git a/demo/e2e/screenshot.spec.ts-snapshots/datetime-modal-custom-overlay-phone-dark.png b/demo/e2e/screenshot.spec.ts-snapshots/datetime-modal-custom-overlay-phone-dark.png new file mode 100644 index 00000000..c2487bd4 Binary files /dev/null and b/demo/e2e/screenshot.spec.ts-snapshots/datetime-modal-custom-overlay-phone-dark.png differ diff --git a/demo/e2e/screenshot.spec.ts-snapshots/datetime-modal-custom-overlay-phone.png b/demo/e2e/screenshot.spec.ts-snapshots/datetime-modal-custom-overlay-phone.png new file mode 100644 index 00000000..52c4acec Binary files /dev/null and b/demo/e2e/screenshot.spec.ts-snapshots/datetime-modal-custom-overlay-phone.png differ diff --git a/demo/e2e/screenshot.spec.ts-snapshots/datetime-modal-custom-overlay.png b/demo/e2e/screenshot.spec.ts-snapshots/datetime-modal-custom-overlay.png new file mode 100644 index 00000000..01cd1042 Binary files /dev/null and b/demo/e2e/screenshot.spec.ts-snapshots/datetime-modal-custom-overlay.png differ diff --git a/src/styles/components/ion-modal.scss b/src/styles/components/ion-modal.scss index bbadba4d..5cb3e4b8 100644 --- a/src/styles/components/ion-modal.scss +++ b/src/styles/components/ion-modal.scss @@ -33,8 +33,11 @@ ion-modal.ios:not(.ios-theme-disabled, .ios26-disabled) { // Datetime Button overlays retain Ionic's content-based dimensions. &:where(:not(.ion-datetime-button-overlay)) { --border-radius: 32px; - --width: 580px; - --height: min(660px, calc(100% - 64px)); + // Dimension defaults must yield to component-scoped sizing. + :where(&) { + --width: 580px; + --height: min(660px, calc(100% - 64px)); + } --box-shadow: 0 24px 64px color-mix(in srgb, var(--ion-box-shadow-color, #000) 18%, transparent); &:not(.modal-card, .modal-sheet)::part(content) { @@ -42,13 +45,13 @@ ion-modal.ios:not(.ios-theme-disabled, .ios26-disabled) { clip-path: inset(0 round var(--border-radius)); } - &.modal-card { + :where(&.modal-card) { --width: min(936px, calc(100% - 40px)); --max-width: 936px; --height: calc(100% - max(32px, var(--ion-safe-area-top, 0px)) * 2); } - &.modal-sheet { + :where(&.modal-sheet) { --width: min(936px, calc(100vw - 40px)); --height: calc(100% - max(32px, var(--ion-modal-offset-top, 0px))); } @@ -97,8 +100,8 @@ ion-modal.ios:not(.ios-theme-disabled, .ios26-disabled) { // Keep ordinary phone and tablet defaults independent of these device layouts. :is(ion-app.ios-theme-vertical-bars, ion-app.ios-theme-fold-expanded) ion-modal.ios:not(.ios-theme-disabled, .ios26-disabled, .ion-datetime-button-overlay) { - &.modal-card, - &.modal-sheet { + :where(&.modal-card), + :where(&.modal-sheet) { --width: calc(100% - 16px); --max-width: none; --height: calc(100% - max(8px, var(--ion-safe-area-top, 0px))); @@ -112,16 +115,18 @@ ion-modal.ios:not(.ios-theme-disabled, .ios26-disabled) { align-items: flex-end; } - &.modal-sheet { - --height: calc(100% - max(8px, var(--ion-modal-offset-top, 0px))); - } - @media (min-width: 768px) and (min-height: 600px) { - --width: min(653px, calc(100% - 16px)); - --height: calc(100% - max(8px, var(--ion-safe-area-top, 0px))); + :where(&) { + --width: min(653px, calc(100% - 16px)); + --height: calc(100% - max(8px, var(--ion-safe-area-top, 0px))); + } align-items: flex-end; } + :where(&.modal-sheet) { + --height: calc(100% - max(8px, var(--ion-modal-offset-top, 0px))); + } + &.modal-sheet.modal-no-expand-scroll::part(content) { bottom: 0; }