feat(modal): add default ionic theme styles (#29876)
Issue number: resolves internal --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> No default modal styles for the Ionic theme ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Splits common and native styles into separate stylesheets - Adds default styles to modals for Ionic theme ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> I only applied some of the global styles to the sheet modal vairation since it is unclear whether the card variation will be used in the widgets
@ -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: "";
|
@ -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);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import "./modal";
|
||||
@import "./modal.native";
|
||||
@import "./modal.ios.vars";
|
||||
|
||||
// iOS Modals
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import "./modal";
|
||||
@import "./modal.native";
|
||||
@import "./modal.md.vars";
|
||||
|
||||
// Material Design Modals
|
||||
|
50
core/src/components/modal/modal.native.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -14,10 +14,6 @@
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
|
||||
<style>
|
||||
ion-modal {
|
||||
--box-shadow: 0px 0px 30px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
@ -72,6 +72,10 @@
|
||||
.grid-item {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.half-sheet {
|
||||
--height: 50%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@ -140,6 +144,13 @@
|
||||
Backdrop is inactive
|
||||
</button>
|
||||
|
||||
<button
|
||||
id="half-sheet"
|
||||
onclick="presentHalfSheetModal({ cssClass: 'half-sheet', backdropBreakpoint: 0.5, initialBreakpoint: 1, breakpoints: [1] })"
|
||||
>
|
||||
Half Sheet w/ Footer
|
||||
</button>
|
||||
|
||||
<div class="grid">
|
||||
<div class="grid-item red"></div>
|
||||
<div class="grid-item green"></div>
|
||||
@ -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 += `<ion-item>Item ${i}</ion-item>`;
|
||||
}
|
||||
|
||||
// create component to open
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = `
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Super Modal</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button class="dismiss">Dismiss Modal</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
${items}
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-button expand="block">Action</ion-button>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
`;
|
||||
|
||||
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();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
@ -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 }) => {
|
||||
|
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 26 KiB |
@ -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);
|
||||
}
|
||||
|
||||
|