Skip to content

Commit 60e6b31

Browse files
authored
feat(modal): add shape prop & styling for ionic theme (#29853)
Issue number: 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 shape prop or ionic theme styling ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Adds `shape` prop to `ion-modal` - Adds styling for shape in 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. -->
1 parent 1841b59 commit 60e6b31

File tree

31 files changed

+290
-2
lines changed

31 files changed

+290
-2
lines changed

core/api.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,6 +1395,7 @@ ion-modal,prop,keyboardClose,boolean,true,false,false
13951395
ion-modal,prop,leaveAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
13961396
ion-modal,prop,mode,"ios" | "md",undefined,false,false
13971397
ion-modal,prop,presentingElement,HTMLElement | undefined,undefined,false,false
1398+
ion-modal,prop,shape,"rectangular" | "round" | "soft" | undefined,undefined,false,false
13981399
ion-modal,prop,showBackdrop,boolean,true,false,false
13991400
ion-modal,prop,theme,"ios" | "md" | "ionic",undefined,false,false
14001401
ion-modal,prop,trigger,string | undefined,undefined,false,false

core/src/components.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2097,6 +2097,10 @@ export namespace Components {
20972097
* Move a sheet style modal to a specific breakpoint. The breakpoint value must be a value defined in your `breakpoints` array.
20982098
*/
20992099
"setCurrentBreakpoint": (breakpoint: number) => Promise<void>;
2100+
/**
2101+
* Set to `"soft"` for a modal with slightly rounded corners, `"round"` for a modal with fully rounded corners, or `"rectangular"` for a modal without rounded corners. Defaults to `"round"` for the `ionic` theme, undefined for all other themes.
2102+
*/
2103+
"shape"?: 'soft' | 'round' | 'rectangular';
21002104
/**
21012105
* If `true`, a backdrop will be displayed behind the modal. This property controls whether or not the backdrop darkens the screen when the modal is presented. It does not control whether or not the backdrop is active or present in the DOM.
21022106
*/
@@ -7431,6 +7435,10 @@ declare namespace LocalJSX {
74317435
* The element that presented the modal. This is used for card presentation effects and for stacking multiple modals on top of each other. Only applies in iOS mode.
74327436
*/
74337437
"presentingElement"?: HTMLElement;
7438+
/**
7439+
* Set to `"soft"` for a modal with slightly rounded corners, `"round"` for a modal with fully rounded corners, or `"rectangular"` for a modal without rounded corners. Defaults to `"round"` for the `ionic` theme, undefined for all other themes.
7440+
*/
7441+
"shape"?: 'soft' | 'round' | 'rectangular';
74347442
/**
74357443
* If `true`, a backdrop will be displayed behind the modal. This property controls whether or not the backdrop darkens the screen when the modal is presented. It does not control whether or not the backdrop is active or present in the DOM.
74367444
*/
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
@use "../../themes/ionic/ionic.globals.scss" as globals;
2+
@import "./modal";
3+
4+
// Ionic Modal
5+
// --------------------------------------------------
6+
7+
// Shape
8+
// -------------------------------------
9+
:host(.modal-round) {
10+
--border-radius: #{globals.$ionic-border-radius-1000};
11+
}
12+
13+
:host(.modal-soft) {
14+
--border-radius: #{globals.$ionic-border-radius-400};
15+
}
16+
17+
:host(.modal-rectangular) {
18+
--border-radius: #{globals.$ionic-border-radius-0};
19+
}

core/src/components/modal/modal.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ import { setCardStatusBarDark, setCardStatusBarDefault } from './utils';
6262
styleUrls: {
6363
ios: 'modal.ios.scss',
6464
md: 'modal.md.scss',
65-
ionic: 'modal.md.scss',
65+
ionic: 'modal.ionic.scss',
6666
},
6767
shadow: true,
6868
})
@@ -291,6 +291,15 @@ export class Modal implements ComponentInterface, OverlayInterface {
291291
*/
292292
@Prop() canDismiss: boolean | ((data?: any, role?: string) => Promise<boolean>) = true;
293293

294+
/**
295+
* Set to `"soft"` for a modal with slightly rounded corners,
296+
* `"round"` for a modal with fully rounded corners, or `"rectangular"`
297+
* for a modal without rounded corners.
298+
*
299+
* Defaults to `"round"` for the `ionic` theme, undefined for all other themes.
300+
*/
301+
@Prop() shape?: 'soft' | 'round' | 'rectangular';
302+
294303
/**
295304
* Emitted after the modal has presented.
296305
*/
@@ -888,6 +897,22 @@ export class Modal implements ComponentInterface, OverlayInterface {
888897
return true;
889898
}
890899

900+
private getShape(): string | undefined {
901+
const theme = getIonTheme(this);
902+
const { shape } = this;
903+
904+
// TODO(ROU-11167): Remove theme check when shapes are defined for all themes.
905+
if (theme !== 'ionic') {
906+
return undefined;
907+
}
908+
909+
if (shape === undefined) {
910+
return 'round';
911+
}
912+
913+
return shape;
914+
}
915+
891916
private onHandleClick = () => {
892917
const { sheetTransition, handleBehavior } = this;
893918
if (handleBehavior !== 'cycle' || sheetTransition !== undefined) {
@@ -936,6 +961,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
936961
const theme = getIonTheme(this);
937962
const isCardModal = presentingElement !== undefined && theme === 'ios';
938963
const isHandleCycle = handleBehavior === 'cycle';
964+
const shape = this.getShape();
939965

940966
return (
941967
<Host
@@ -950,6 +976,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
950976
['modal-default']: !isCardModal && !isSheetModal,
951977
[`modal-card`]: isCardModal,
952978
[`modal-sheet`]: isSheetModal,
979+
[`modal-${shape}`]: shape !== undefined,
953980
'overlay-hidden': true,
954981
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false,
955982
...getClassMap(this.cssClass),
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<!DOCTYPE html>
2+
<html lang="en" dir="ltr">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>Modal - Shape</title>
6+
<meta
7+
name="viewport"
8+
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
9+
/>
10+
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
11+
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
12+
<script src="../../../../../scripts/testing/scripts.js"></script>
13+
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
14+
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
15+
16+
<style>
17+
ion-modal {
18+
--box-shadow: 0px 0px 30px 10px rgba(0, 0, 0, 0.1);
19+
}
20+
21+
.container {
22+
margin-bottom: 20px;
23+
}
24+
</style>
25+
</head>
26+
27+
<script type="module">
28+
import { modalController } from '../../../../dist/ionic/index.esm.js';
29+
window.modalController = modalController;
30+
</script>
31+
32+
<body>
33+
<ion-app>
34+
<ion-header>
35+
<ion-toolbar>
36+
<ion-title>Modal - Shape</ion-title>
37+
</ion-toolbar>
38+
</ion-header>
39+
40+
<ion-content class="ion-padding" id="content" no-bounce>
41+
<div class="container">
42+
<h1>Sheet</h1>
43+
<button id="sheet-modal-default" onclick="presentSheetModal()">Present Sheet Modal (Default)</button>
44+
<button id="sheet-modal-round" onclick="presentSheetModal({ shape: 'round' })">
45+
Present Sheet Modal (Round)
46+
</button>
47+
<button id="sheet-modal-soft" onclick="presentSheetModal({ shape: 'soft' })">
48+
Present Sheet Modal (Soft)
49+
</button>
50+
<button id="sheet-modal-rectangular" onclick="presentSheetModal({ shape: 'rectangular' })">
51+
Present Sheet Modal (Rectangular)
52+
</button>
53+
</div>
54+
55+
<div class="container">
56+
<h1>Card</h1>
57+
<button id="card-modal-default" onclick="presentCardModal()">Present Card Modal (Default)</button>
58+
<button id="card-modal-round" onclick="presentCardModal({ shape: 'round' })">
59+
Present Card Modal (Round)
60+
</button>
61+
<button id="card-modal-soft" onclick="presentCardModal({ shape: 'soft' })">Present Card Modal (Soft)</button>
62+
<button id="card-modal-rectangular" onclick="presentCardModal({ shape: 'rectangular' })">
63+
Present Card Modal (Rectangular)
64+
</button>
65+
</div>
66+
</ion-content>
67+
</ion-app>
68+
</body>
69+
70+
<script>
71+
function renderContent() {
72+
let items = '';
73+
74+
for (var i = 0; i < 25; i++) {
75+
items += `<ion-item>Item ${i}</ion-item>`;
76+
}
77+
78+
return items;
79+
}
80+
81+
async function presentSheetModal(options) {
82+
const modal = await createSheetModal(options);
83+
await modal.present();
84+
85+
await modal.onDidDismiss();
86+
modal.remove();
87+
}
88+
89+
async function createSheetModal(options) {
90+
// create component to open
91+
const element = document.createElement('div');
92+
element.innerHTML = `
93+
<ion-header>
94+
<ion-toolbar>
95+
<ion-title>Super Modal</ion-title>
96+
<ion-buttons slot="end">
97+
<ion-button class="dismiss">Dismiss Modal</ion-button>
98+
</ion-buttons>
99+
</ion-toolbar>
100+
</ion-header>
101+
<ion-content>
102+
<ion-list>
103+
${renderContent()}
104+
</ion-list>
105+
</ion-content>
106+
`;
107+
108+
let extraOptions = {
109+
initialBreakpoint: 0.75,
110+
breakpoints: [0, 0.25, 0.5, 0.75, 1],
111+
};
112+
113+
if (options) {
114+
extraOptions = {
115+
...extraOptions,
116+
...options,
117+
};
118+
}
119+
120+
// present the modal
121+
const modalElement = Object.assign(document.createElement('ion-modal'), {
122+
component: element,
123+
...extraOptions,
124+
});
125+
126+
// listen for close event
127+
const button = element.querySelector('ion-button');
128+
button.addEventListener('click', () => {
129+
modalElement.dismiss();
130+
});
131+
document.body.appendChild(modalElement);
132+
return modalElement;
133+
}
134+
135+
async function presentCardModal(opts) {
136+
const modal = await createCardModal(document.querySelectorAll('.ion-page')[1], opts);
137+
await modal.present();
138+
}
139+
140+
async function createCardModal(presentingEl, opts) {
141+
// create component to open
142+
const element = document.createElement('div');
143+
element.innerHTML = `
144+
<ion-header id="modal-header">
145+
<ion-toolbar>
146+
<ion-buttons slot="end">
147+
<ion-button class="dismiss">Close</ion-button>
148+
</ion-buttons>
149+
</ion-toolbar>
150+
</ion-header>
151+
152+
<div class="content-wrapper">${renderContent()}</div>
153+
154+
<ion-footer>
155+
<ion-toolbar>
156+
<ion-title>Footer</ion-title>
157+
</ion-toolbar>
158+
</ion-footer>
159+
`;
160+
161+
// listen for close event
162+
const button = element.querySelector('ion-button.dismiss');
163+
button.addEventListener('click', () => {
164+
modalController.dismiss();
165+
});
166+
167+
// present the modal
168+
const modalElement = await modalController.create({
169+
presentingElement: presentingEl,
170+
component: element,
171+
...opts,
172+
});
173+
return modalElement;
174+
}
175+
</script>
176+
</html>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { expect } from '@playwright/test';
2+
import { configs, test } from '@utils/test/playwright';
3+
4+
/**
5+
* This behavior does not vary across directions.
6+
*/
7+
configs({ directions: ['ltr'], modes: ['ionic-md'] }).forEach(({ config, screenshot, title }) => {
8+
test.beforeEach(async ({ page }) => {
9+
await page.goto('/src/components/modal/test/shape', config);
10+
});
11+
12+
test.describe(title('modal: shape'), () => {
13+
test.describe('sheet', () => {
14+
['default', 'round', 'soft', 'rectangular'].forEach((shape) => {
15+
test(`${shape} - should not have visual regressions`, async ({ page }) => {
16+
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
17+
18+
await page.click(`#sheet-modal-${shape}`);
19+
await ionModalDidPresent.next();
20+
21+
await expect(page).toHaveScreenshot(screenshot(`modal-shape-sheet-${shape}`), {
22+
/**
23+
* Animations must be enabled to capture the screenshot.
24+
* By default, animations are disabled with toHaveScreenshot,
25+
* and when capturing the screenshot will call animation.finish().
26+
* This will cause the modal to close and the screenshot capture
27+
* to be invalid.
28+
*/
29+
animations: 'allow',
30+
});
31+
});
32+
});
33+
});
34+
35+
test.describe('card', () => {
36+
['default', 'round', 'soft', 'rectangular'].forEach((shape) => {
37+
test(`${shape} - should not have visual regressions`, async ({ page }) => {
38+
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
39+
40+
await page.click(`#card-modal-${shape}`);
41+
await ionModalDidPresent.next();
42+
43+
await expect(page).toHaveScreenshot(screenshot(`modal-shape-card-${shape}`), {
44+
/**
45+
* Animations must be enabled to capture the screenshot.
46+
* By default, animations are disabled with toHaveScreenshot,
47+
* and when capturing the screenshot will call animation.finish().
48+
* This will cause the popover to close and the screenshot capture
49+
* to be invalid.
50+
*/
51+
animations: 'allow',
52+
});
53+
});
54+
});
55+
});
56+
});
57+
});
11.3 KB
Loading
17.8 KB
Loading
10.3 KB
Loading
Loading

0 commit comments

Comments
 (0)