diff --git a/core/src/components/modal/modal.scss b/core/src/components/modal/modal.common.scss similarity index 73% rename from core/src/components/modal/modal.scss rename to core/src/components/modal/modal.common.scss index 02b7128c99..95e597ae1d 100644 --- a/core/src/components/modal/modal.scss +++ b/core/src/components/modal/modal.common.scss @@ -1,6 +1,6 @@ -@import "./modal.vars"; +@import "../../themes/mixins.scss"; -// Modals +// Modals: Common Styles // -------------------------------------------------- :host { @@ -29,13 +29,10 @@ --min-height: auto; --max-height: auto; --overflow: hidden; - --border-radius: 0; --border-width: 0; --border-style: none; --border-color: transparent; - --background: #{$background-color}; --box-shadow: none; - --backdrop-opacity: 0; @include position(0, 0, 0, 0); @@ -47,8 +44,6 @@ outline: none; - color: $modal-text-color; - contain: strict; } @@ -90,37 +85,14 @@ ion-backdrop { background: transparent; } -@media only screen and (min-width: $modal-inset-min-width) and (min-height: $modal-inset-min-height-small) { - :host { - --width: #{$modal-inset-width}; - --height: #{$modal-inset-height-small}; - --ion-safe-area-top: 0px; - --ion-safe-area-bottom: 0px; - --ion-safe-area-right: 0px; - --ion-safe-area-left: 0px; - } -} - -@media only screen and (min-width: $modal-inset-min-width) and (min-height: $modal-inset-min-height-large) { - :host { - --width: #{$modal-inset-width}; - --height: #{$modal-inset-height-large}; - } -} - // Sheet Modal // -------------------------------------------------- .modal-handle { - @include position(5px, 0px, null, 0px); - @include border-radius(8px, 8px, 8px, 8px); @include margin(null, auto, null, auto); position: absolute; - width: 36px; - height: 5px; - /** * This allows the handle to appear * on top of user content in WebKit. @@ -129,8 +101,6 @@ ion-backdrop { border: 0; - background: var(--ion-color-step-350, var(--ion-background-color-step-350, #c0c0be)); - cursor: pointer; z-index: 11; @@ -144,9 +114,6 @@ ion-backdrop { position: absolute; - width: 36px; - height: 5px; - transform: translate(-50%, -50%); content: ""; diff --git a/core/src/components/modal/modal.ionic.scss b/core/src/components/modal/modal.ionic.scss index c3f39d0b12..075117f945 100644 --- a/core/src/components/modal/modal.ionic.scss +++ b/core/src/components/modal/modal.ionic.scss @@ -1,11 +1,15 @@ @use "../../themes/ionic/ionic.globals.scss" as globals; -@import "./modal"; +@use "./modal.common"; // Ionic Modal // -------------------------------------------------- :host { + --background: #{globals.$ionic-color-base-white}; + --box-shadow: #{globals.$ionic-elevation-300}; --backdrop-opacity: 1; + + color: globals.$ionic-color-neutral-1200; } // Shape @@ -21,3 +25,20 @@ :host(.modal-rectangular) { --border-radius: #{globals.$ionic-border-radius-0}; } + +// Sheet Modal +// -------------------------------------------------- + +.modal-handle { + @include globals.position(globals.$ionic-space-300, 0px, null, 0px); + @include globals.border-radius(globals.$ionic-border-radius-100); + + width: globals.$ionic-scale-1100; + height: globals.$ionic-scale-100; + + background-color: globals.$ionic-color-neutral-300; +} + +:host(.modal-sheet) .modal-wrapper { + @include globals.border-radius(var(--border-radius), var(--border-radius), 0, 0); +} diff --git a/core/src/components/modal/modal.ios.scss b/core/src/components/modal/modal.ios.scss index fc5e25e3d1..58954468d2 100644 --- a/core/src/components/modal/modal.ios.scss +++ b/core/src/components/modal/modal.ios.scss @@ -1,4 +1,4 @@ -@import "./modal"; +@import "./modal.native"; @import "./modal.ios.vars"; // iOS Modals diff --git a/core/src/components/modal/modal.md.scss b/core/src/components/modal/modal.md.scss index 7367732f48..e03ff3b4e7 100644 --- a/core/src/components/modal/modal.md.scss +++ b/core/src/components/modal/modal.md.scss @@ -1,4 +1,4 @@ -@import "./modal"; +@import "./modal.native"; @import "./modal.md.vars"; // Material Design Modals diff --git a/core/src/components/modal/modal.native.scss b/core/src/components/modal/modal.native.scss new file mode 100644 index 0000000000..6d95338688 --- /dev/null +++ b/core/src/components/modal/modal.native.scss @@ -0,0 +1,50 @@ +@use "./modal.common"; +@use "./modal.vars" as vars; +@forward "./modal.vars.scss"; + +// Modals: Native Styles +// -------------------------------------------------- + +:host { + --background: #{vars.$background-color}; + --border-radius: 0; + --backdrop-opacity: 0; + + color: vars.$modal-text-color; +} + +@media only screen and (min-width: vars.$modal-inset-min-width) and (min-height: vars.$modal-inset-min-height-small) { + :host { + --width: #{vars.$modal-inset-width}; + --height: #{vars.$modal-inset-height-small}; + --ion-safe-area-top: 0px; + --ion-safe-area-bottom: 0px; + --ion-safe-area-right: 0px; + --ion-safe-area-left: 0px; + } +} + +@media only screen and (min-width: vars.$modal-inset-min-width) and (min-height: vars.$modal-inset-min-height-large) { + :host { + --width: #{vars.$modal-inset-width}; + --height: #{vars.$modal-inset-height-large}; + } +} + +// Sheet Modal +// -------------------------------------------------- + +.modal-handle { + @include vars.position(5px, 0px, null, 0px); + @include vars.border-radius(8px, 8px, 8px, 8px); + + width: 36px; + height: 5px; + + background: var(--ion-color-step-350, var(--ion-background-color-step-350, #c0c0be)); + + &::before { + width: 36px; + height: 5px; + } +} diff --git a/core/src/components/modal/test/shape/index.html b/core/src/components/modal/test/shape/index.html index d5c482c407..8d895d55e3 100644 --- a/core/src/components/modal/test/shape/index.html +++ b/core/src/components/modal/test/shape/index.html @@ -14,10 +14,6 @@ @@ -140,6 +144,13 @@ Backdrop is inactive + +
@@ -214,12 +225,65 @@ modal.remove(); } - async function presentCardModal() { - const presentingEl = document.querySelectorAll('.ion-page')[1]; - const modal = createModal('card', { - presentingElement: presentingEl, + async function presentHalfSheetModal(options) { + let items = ''; + + for (var i = 0; i < 3; i++) { + items += `Item ${i}`; + } + + // create component to open + const element = document.createElement('div'); + element.innerHTML = ` + + + Super Modal + + Dismiss Modal + + + + + + ${items} + + + + + Action + + + `; + + let extraOptions = { + initialBreakpoint: 0.25, + breakpoints: [0, 0.25, 0.5, 0.75, 1], + }; + + if (options) { + extraOptions = { + ...extraOptions, + ...options, + }; + } + + // present the modal + const modalElement = Object.assign(document.createElement('ion-modal'), { + component: element, + ...extraOptions, }); - await modal.present(); + + // listen for close event + const button = element.querySelector('ion-button'); + button.addEventListener('click', () => { + modalElement.dismiss(); + }); + document.body.appendChild(modalElement); + + await modalElement.present(); + + await modalElement.onDidDismiss(); + modalElement.remove(); } diff --git a/core/src/components/modal/test/sheet/modal.e2e.ts b/core/src/components/modal/test/sheet/modal.e2e.ts index 6afb4cd67e..fcc77428bc 100644 --- a/core/src/components/modal/test/sheet/modal.e2e.ts +++ b/core/src/components/modal/test/sheet/modal.e2e.ts @@ -25,6 +25,30 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { }); }); +configs({ modes: ['ionic-md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { + test.describe(title('sheet modal: half screen rendering'), () => { + test('should not have visual regressions', async ({ page }) => { + await page.goto('/src/components/modal/test/sheet', config); + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + + await page.click('#half-sheet'); + + await ionModalDidPresent.next(); + + await expect(page).toHaveScreenshot(screenshot(`modal-half-sheet-present`), { + /** + * Animations must be enabled to capture the screenshot. + * By default, animations are disabled with toHaveScreenshot, + * and when capturing the screenshot will call animation.finish(). + * This will cause the modal to close and the screenshot capture + * to be invalid. + */ + animations: 'allow', + }); + }); + }); +}); + configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { test.describe(title('sheet modal: backdrop'), () => { test.beforeEach(async ({ page }) => { diff --git a/core/src/components/modal/test/sheet/modal.e2e.ts-snapshots/modal-half-sheet-present-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/modal/test/sheet/modal.e2e.ts-snapshots/modal-half-sheet-present-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 0000000000..be4b237894 Binary files /dev/null and b/core/src/components/modal/test/sheet/modal.e2e.ts-snapshots/modal-half-sheet-present-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/modal/test/sheet/modal.e2e.ts-snapshots/modal-half-sheet-present-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/modal/test/sheet/modal.e2e.ts-snapshots/modal-half-sheet-present-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 0000000000..2b87713710 Binary files /dev/null and b/core/src/components/modal/test/sheet/modal.e2e.ts-snapshots/modal-half-sheet-present-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/modal/test/sheet/modal.e2e.ts-snapshots/modal-half-sheet-present-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/modal/test/sheet/modal.e2e.ts-snapshots/modal-half-sheet-present-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 0000000000..cd46e6b586 Binary files /dev/null and b/core/src/components/modal/test/sheet/modal.e2e.ts-snapshots/modal-half-sheet-present-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/css/ionic/core.ionic.scss b/core/src/css/ionic/core.ionic.scss index 7bdd1e5137..61b9023fed 100644 --- a/core/src/css/ionic/core.ionic.scss +++ b/core/src/css/ionic/core.ionic.scss @@ -36,37 +36,68 @@ body.backdrop-no-scroll { // Modal - Card Style // -------------------------------------------------- +html.ionic ion-modal ion-header { + box-shadow: none; +} + /** - * Card style modal needs additional padding on the + * Modals need additional padding on the * top of the header. We accomplish this by targeting * the first toolbar in the header. * Footer also needs this. We do not adjust the bottom * padding though because of the safe area. */ -html.ios ion-modal.modal-card ion-header ion-toolbar:first-of-type, -html.ios ion-modal.modal-sheet ion-header ion-toolbar:first-of-type, -html.ios ion-modal ion-footer ion-toolbar:first-of-type { - padding-top: globals.$ionic-space-150; +html.ionic ion-modal.modal-card ion-header ion-toolbar:first-of-type, +html.ionic ion-modal.modal-sheet ion-header ion-toolbar:first-of-type, +html.ionic ion-modal ion-footer ion-toolbar:first-of-type { + padding-top: globals.$ionic-space-400; } /** -* Card style modal needs additional padding on the +* Modals need additional padding on the * bottom of the header. We accomplish this by targeting * the last toolbar in the header. */ -html.ios ion-modal.modal-card ion-header ion-toolbar:last-of-type, -html.ios ion-modal.modal-sheet ion-header ion-toolbar:last-of-type { - padding-bottom: globals.$ionic-space-150; +html.ionic ion-modal.modal-card ion-header ion-toolbar:last-of-type, +html.ionic ion-modal.modal-sheet ion-header ion-toolbar:last-of-type, +html.ionic ion-modal ion-footer ion-toolbar:last-of-type { + padding-bottom: globals.$ionic-space-400; } /** -* Add padding on the left and right -* of toolbars while accounting for -* safe area values when in landscape. +* Add additional padding to the left and right of toolbars while accounting +* for the safe area insets. */ -html.ios ion-modal ion-toolbar { - padding-right: calc(var(--ion-safe-area-right) + globals.$ionic-space-200); - padding-left: calc(var(--ion-safe-area-left) + globals.$ionic-space-200); +html.ionic ion-modal ion-toolbar { + --padding-start: calc(var(--ion-safe-area-right) + #{globals.$ionic-space-400}); + --padding-end: calc(var(--ion-safe-area-left) + #{globals.$ionic-space-400}); +} + +/** +* Center an `ion-title` within a modal header. This works around +* limitations with `:host-context()` not being supported in all browsers. +*/ +html.ionic ion-modal ion-header ion-toolbar ion-title { + @include globals.position(0, null, null, 0); + + position: absolute; + + width: 100%; + height: 100%; + + transform: translateZ(0); + + text-align: center; +} + +/** +* Add padding to the left, right, and bottom of `ion-content` +* within a modal. +*/ +html.ionic ion-modal.modal-sheet ion-content { + --padding-start: #{globals.$ionic-space-400}; + --padding-end: #{globals.$ionic-space-400}; + --padding-bottom: #{globals.$ionic-space-400}; } /** @@ -74,7 +105,7 @@ html.ios ion-modal ion-toolbar { * should only have backdrop on first instance. */ @media screen and (min-width: 768px) { - html.ios ion-modal.modal-card:first-of-type { + html.ionic ion-modal.modal-card:first-of-type { --backdrop-opacity: 0.18; } } @@ -121,7 +152,7 @@ ion-modal.modal-default.show-modal ~ ion-modal.modal-default { * radius no matter the platform. * This behavior only applies to card modals. */ -html.ios ion-modal.modal-card .ion-page { +html.ionic ion-modal.modal-card .ion-page { border-top-left-radius: var(--border-radius); }