Skip to content

Commit 3a010fc

Browse files
authored
feat(loading): isOpen presents and dismisses loading overlay (#26067)
1 parent 4cb32b6 commit 3a010fc

File tree

10 files changed

+344
-15
lines changed

10 files changed

+344
-15
lines changed

core/api.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,7 @@ ion-loading,prop,cssClass,string | string[] | undefined,undefined,false,false
693693
ion-loading,prop,duration,number,0,false,false
694694
ion-loading,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
695695
ion-loading,prop,htmlAttributes,undefined | { [key: string]: any; },undefined,false,false
696+
ion-loading,prop,isOpen,boolean,false,false,false
696697
ion-loading,prop,keyboardClose,boolean,true,false,false
697698
ion-loading,prop,leaveAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
698699
ion-loading,prop,message,IonicSafeString | string | undefined,undefined,false,false
@@ -704,10 +705,14 @@ ion-loading,method,dismiss,dismiss(data?: any, role?: string) => Promise<boolean
704705
ion-loading,method,onDidDismiss,onDidDismiss<T = any>() => Promise<OverlayEventDetail<T>>
705706
ion-loading,method,onWillDismiss,onWillDismiss<T = any>() => Promise<OverlayEventDetail<T>>
706707
ion-loading,method,present,present() => Promise<void>
708+
ion-loading,event,didDismiss,OverlayEventDetail<any>,true
709+
ion-loading,event,didPresent,void,true
707710
ion-loading,event,ionLoadingDidDismiss,OverlayEventDetail<any>,true
708711
ion-loading,event,ionLoadingDidPresent,void,true
709712
ion-loading,event,ionLoadingWillDismiss,OverlayEventDetail<any>,true
710713
ion-loading,event,ionLoadingWillPresent,void,true
714+
ion-loading,event,willDismiss,OverlayEventDetail<any>,true
715+
ion-loading,event,willPresent,void,true
711716
ion-loading,css-prop,--backdrop-opacity
712717
ion-loading,css-prop,--background
713718
ion-loading,css-prop,--height

core/src/components.d.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1377,6 +1377,7 @@ export namespace Components {
13771377
* Additional classes to apply for custom CSS. If multiple classes are provided they should be separated by spaces.
13781378
*/
13791379
"cssClass"?: string | string[];
1380+
"delegate"?: FrameworkDelegate;
13801381
/**
13811382
* Dismiss the loading overlay after it has been presented.
13821383
* @param data Any data to emit in the dismiss events.
@@ -1391,10 +1392,15 @@ export namespace Components {
13911392
* Animation to use when the loading indicator is presented.
13921393
*/
13931394
"enterAnimation"?: AnimationBuilder;
1395+
"hasController": boolean;
13941396
/**
13951397
* Additional attributes to pass to the loader.
13961398
*/
13971399
"htmlAttributes"?: LoadingAttributes;
1400+
/**
1401+
* If `true`, the loading indicator will open. If `false`, the loading indicator will close. Use this if you need finer grained control over presentation, otherwise just use the loadingController or the `trigger` property. Note: `isOpen` will not automatically be set back to `false` when the loading indicator dismisses. You will need to do that in your code.
1402+
*/
1403+
"isOpen": boolean;
13981404
/**
13991405
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
14001406
*/
@@ -5163,6 +5169,7 @@ declare namespace LocalJSX {
51635169
* Additional classes to apply for custom CSS. If multiple classes are provided they should be separated by spaces.
51645170
*/
51655171
"cssClass"?: string | string[];
5172+
"delegate"?: FrameworkDelegate;
51665173
/**
51675174
* Number of milliseconds to wait before dismissing the loading indicator.
51685175
*/
@@ -5171,10 +5178,15 @@ declare namespace LocalJSX {
51715178
* Animation to use when the loading indicator is presented.
51725179
*/
51735180
"enterAnimation"?: AnimationBuilder;
5181+
"hasController"?: boolean;
51745182
/**
51755183
* Additional attributes to pass to the loader.
51765184
*/
51775185
"htmlAttributes"?: LoadingAttributes;
5186+
/**
5187+
* If `true`, the loading indicator will open. If `false`, the loading indicator will close. Use this if you need finer grained control over presentation, otherwise just use the loadingController or the `trigger` property. Note: `isOpen` will not automatically be set back to `false` when the loading indicator dismisses. You will need to do that in your code.
5188+
*/
5189+
"isOpen"?: boolean;
51785190
/**
51795191
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
51805192
*/
@@ -5191,6 +5203,14 @@ declare namespace LocalJSX {
51915203
* The mode determines which platform styles to use.
51925204
*/
51935205
"mode"?: "ios" | "md";
5206+
/**
5207+
* Emitted after the loading indicator has dismissed. Shorthand for ionLoadingDidDismiss.
5208+
*/
5209+
"onDidDismiss"?: (event: IonLoadingCustomEvent<OverlayEventDetail>) => void;
5210+
/**
5211+
* Emitted after the loading indicator has presented. Shorthand for ionLoadingWillDismiss.
5212+
*/
5213+
"onDidPresent"?: (event: IonLoadingCustomEvent<void>) => void;
51945214
/**
51955215
* Emitted after the loading has dismissed.
51965216
*/
@@ -5207,6 +5227,14 @@ declare namespace LocalJSX {
52075227
* Emitted before the loading has presented.
52085228
*/
52095229
"onIonLoadingWillPresent"?: (event: IonLoadingCustomEvent<void>) => void;
5230+
/**
5231+
* Emitted before the loading indicator has dismissed. Shorthand for ionLoadingWillDismiss.
5232+
*/
5233+
"onWillDismiss"?: (event: IonLoadingCustomEvent<OverlayEventDetail>) => void;
5234+
/**
5235+
* Emitted before the loading indicator has presented. Shorthand for ionLoadingWillPresent.
5236+
*/
5237+
"onWillPresent"?: (event: IonLoadingCustomEvent<void>) => void;
52105238
"overlayIndex": number;
52115239
/**
52125240
* If `true`, a backdrop will be displayed behind the loading indicator.

core/src/components/loading/loading.tsx

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
import type { ComponentInterface, EventEmitter } from '@stencil/core';
2-
import { Component, Element, Event, Host, Method, Prop, h } from '@stencil/core';
2+
import { Watch, Component, Element, Event, Host, Method, Prop, h } from '@stencil/core';
33

44
import { config } from '../../global/config';
55
import { getIonMode } from '../../global/ionic-global';
66
import type {
77
AnimationBuilder,
8+
FrameworkDelegate,
89
LoadingAttributes,
910
OverlayEventDetail,
1011
OverlayInterface,
1112
SpinnerTypes,
1213
} from '../../interface';
13-
import { BACKDROP, dismiss, eventMethod, prepareOverlay, present } from '../../utils/overlays';
14+
import { raf } from '../../utils/helpers';
15+
import {
16+
BACKDROP,
17+
dismiss,
18+
eventMethod,
19+
prepareOverlay,
20+
present,
21+
createDelegateController,
22+
} from '../../utils/overlays';
1423
import type { IonicSafeString } from '../../utils/sanitization';
1524
import { sanitizeDOMString } from '../../utils/sanitization';
1625
import { getClassMap } from '../../utils/theme';
@@ -32,7 +41,9 @@ import { mdLeaveAnimation } from './animations/md.leave';
3241
scoped: true,
3342
})
3443
export class Loading implements ComponentInterface, OverlayInterface {
44+
private readonly delegateController = createDelegateController(this);
3545
private durationTimeout: any;
46+
private currentTransition?: Promise<any>;
3647

3748
presented = false;
3849
lastFocus?: HTMLElement;
@@ -42,6 +53,12 @@ export class Loading implements ComponentInterface, OverlayInterface {
4253
/** @internal */
4354
@Prop() overlayIndex!: number;
4455

56+
/** @internal */
57+
@Prop() delegate?: FrameworkDelegate;
58+
59+
/** @internal */
60+
@Prop() hasController = false;
61+
4562
/**
4663
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
4764
*/
@@ -105,6 +122,23 @@ export class Loading implements ComponentInterface, OverlayInterface {
105122
*/
106123
@Prop() htmlAttributes?: LoadingAttributes;
107124

125+
/**
126+
* If `true`, the loading indicator will open. If `false`, the loading indicator will close.
127+
* Use this if you need finer grained control over presentation, otherwise
128+
* just use the loadingController or the `trigger` property.
129+
* Note: `isOpen` will not automatically be set back to `false` when
130+
* the loading indicator dismisses. You will need to do that in your code.
131+
*/
132+
@Prop() isOpen = false;
133+
@Watch('isOpen')
134+
onIsOpenChange(newValue: boolean, oldValue: boolean) {
135+
if (newValue === true && oldValue === false) {
136+
this.present();
137+
} else if (newValue === false && oldValue === true) {
138+
this.dismiss();
139+
}
140+
}
141+
108142
/**
109143
* Emitted after the loading has presented.
110144
*/
@@ -125,6 +159,30 @@ export class Loading implements ComponentInterface, OverlayInterface {
125159
*/
126160
@Event({ eventName: 'ionLoadingDidDismiss' }) didDismiss!: EventEmitter<OverlayEventDetail>;
127161

162+
/**
163+
* Emitted after the loading indicator has presented.
164+
* Shorthand for ionLoadingWillDismiss.
165+
*/
166+
@Event({ eventName: 'didPresent' }) didPresentShorthand!: EventEmitter<void>;
167+
168+
/**
169+
* Emitted before the loading indicator has presented.
170+
* Shorthand for ionLoadingWillPresent.
171+
*/
172+
@Event({ eventName: 'willPresent' }) willPresentShorthand!: EventEmitter<void>;
173+
174+
/**
175+
* Emitted before the loading indicator has dismissed.
176+
* Shorthand for ionLoadingWillDismiss.
177+
*/
178+
@Event({ eventName: 'willDismiss' }) willDismissShorthand!: EventEmitter<OverlayEventDetail>;
179+
180+
/**
181+
* Emitted after the loading indicator has dismissed.
182+
* Shorthand for ionLoadingDidDismiss.
183+
*/
184+
@Event({ eventName: 'didDismiss' }) didDismissShorthand!: EventEmitter<OverlayEventDetail>;
185+
128186
connectedCallback() {
129187
prepareOverlay(this.el);
130188
}
@@ -136,16 +194,44 @@ export class Loading implements ComponentInterface, OverlayInterface {
136194
}
137195
}
138196

197+
componentDidLoad() {
198+
/**
199+
* If loading indicator was rendered with isOpen="true"
200+
* then we should open loading indicator immediately.
201+
*/
202+
if (this.isOpen === true) {
203+
raf(() => this.present());
204+
}
205+
}
206+
139207
/**
140208
* Present the loading overlay after it has been created.
141209
*/
142210
@Method()
143211
async present(): Promise<void> {
144-
await present(this, 'loadingEnter', iosEnterAnimation, mdEnterAnimation, undefined);
212+
/**
213+
* When using an inline loading indicator
214+
* and dismissing a loading indicator it is possible to
215+
* quickly present the loading indicator while it is
216+
* dismissing. We need to await any current
217+
* transition to allow the dismiss to finish
218+
* before presenting again.
219+
*/
220+
if (this.currentTransition !== undefined) {
221+
await this.currentTransition;
222+
}
223+
224+
await this.delegateController.attachViewToDom();
225+
226+
this.currentTransition = present(this, 'loadingEnter', iosEnterAnimation, mdEnterAnimation);
227+
228+
await this.currentTransition;
145229

146230
if (this.duration > 0) {
147231
this.durationTimeout = setTimeout(() => this.dismiss(), this.duration + 10);
148232
}
233+
234+
this.currentTransition = undefined;
149235
}
150236

151237
/**
@@ -158,11 +244,19 @@ export class Loading implements ComponentInterface, OverlayInterface {
158244
* Some examples include: ``"cancel"`, `"destructive"`, "selected"`, and `"backdrop"`.
159245
*/
160246
@Method()
161-
dismiss(data?: any, role?: string): Promise<boolean> {
247+
async dismiss(data?: any, role?: string): Promise<boolean> {
162248
if (this.durationTimeout) {
163249
clearTimeout(this.durationTimeout);
164250
}
165-
return dismiss(this, data, role, 'loadingLeave', iosLeaveAnimation, mdLeaveAnimation);
251+
this.currentTransition = dismiss(this, data, role, 'loadingLeave', iosLeaveAnimation, mdLeaveAnimation);
252+
253+
const dismissed = await this.currentTransition;
254+
255+
if (dismissed) {
256+
this.delegateController.removeViewFromDom();
257+
}
258+
259+
return dismissed;
166260
}
167261

168262
/**
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<!DOCTYPE html>
2+
<html lang="en" dir="ltr">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>Loading - isOpen</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, viewport-fit=cover"
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 type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
14+
<style>
15+
.grid {
16+
display: grid;
17+
grid-template-columns: repeat(2, 1fr);
18+
grid-row-gap: 20px;
19+
grid-column-gap: 20px;
20+
}
21+
22+
.grid-item {
23+
margin: 0 auto;
24+
}
25+
26+
h2 {
27+
font-size: 12px;
28+
font-weight: normal;
29+
30+
color: #6f7378;
31+
32+
margin-top: 10px;
33+
margin-left: 5px;
34+
}
35+
</style>
36+
</head>
37+
38+
<body>
39+
<ion-app>
40+
<ion-header>
41+
<ion-toolbar>
42+
<ion-title>Loading - isOpen</ion-title>
43+
</ion-toolbar>
44+
</ion-header>
45+
46+
<ion-content class="ion-padding">
47+
<div class="grid">
48+
<div class="grid-item">
49+
<h2>Default</h2>
50+
<ion-button id="default" onclick="openLoading()">Open Loading</ion-button>
51+
</div>
52+
<div class="grid-item">
53+
<h2>Open, then close after 500ms</h2>
54+
<ion-button id="timeout" onclick="openLoading(500)">Open Loading</ion-button>
55+
</div>
56+
</div>
57+
58+
<ion-loading message="Hello world"></ion-loading>
59+
</ion-content>
60+
</ion-app>
61+
62+
<script>
63+
const loading = document.querySelector('ion-loading');
64+
65+
const openLoading = (timeout) => {
66+
loading.isOpen = true;
67+
68+
if (timeout) {
69+
setTimeout(() => {
70+
loading.isOpen = false;
71+
}, timeout);
72+
}
73+
};
74+
75+
loading.addEventListener('ionLoadingDidDismiss', () => {
76+
loading.isOpen = false;
77+
});
78+
</script>
79+
</body>
80+
</html>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { test } from '@utils/test/playwright';
2+
3+
test.describe('loading: isOpen', () => {
4+
test.beforeEach(async ({ page, skip }) => {
5+
skip.rtl('isOpen does not behave differently in RTL');
6+
skip.mode('md', 'isOpen does not behave differently in MD');
7+
await page.goto('/src/components/loading/test/isOpen');
8+
});
9+
10+
test('should open the loading indicator', async ({ page }) => {
11+
const ionLoadingDidPresent = await page.spyOnEvent('ionLoadingDidPresent');
12+
await page.click('#default');
13+
14+
await ionLoadingDidPresent.next();
15+
await page.waitForSelector('ion-loading', { state: 'visible' });
16+
});
17+
18+
test('should open the loading indicator then close after a timeout', async ({ page }) => {
19+
const ionLoadingDidPresent = await page.spyOnEvent('ionLoadingDidPresent');
20+
const ionLoadingDidDismiss = await page.spyOnEvent('ionLoadingDidDismiss');
21+
await page.click('#timeout');
22+
23+
await ionLoadingDidPresent.next();
24+
await page.waitForSelector('ion-loading', { state: 'visible' });
25+
26+
await ionLoadingDidDismiss.next();
27+
28+
await page.waitForSelector('ion-loading', { state: 'hidden' });
29+
});
30+
});

0 commit comments

Comments
 (0)