Skip to content

Commit 34ca337

Browse files
authored
feat(loading): use loading overlay inline (#26153)
1 parent d1fb7b0 commit 34ca337

File tree

13 files changed

+536
-15
lines changed

13 files changed

+536
-15
lines changed

core/api.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,21 +695,27 @@ ion-loading,prop,cssClass,string | string[] | undefined,undefined,false,false
695695
ion-loading,prop,duration,number,0,false,false
696696
ion-loading,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
697697
ion-loading,prop,htmlAttributes,undefined | { [key: string]: any; },undefined,false,false
698+
ion-loading,prop,isOpen,boolean,false,false,false
698699
ion-loading,prop,keyboardClose,boolean,true,false,false
699700
ion-loading,prop,leaveAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
700701
ion-loading,prop,message,IonicSafeString | string | undefined,undefined,false,false
701702
ion-loading,prop,mode,"ios" | "md",undefined,false,false
702703
ion-loading,prop,showBackdrop,boolean,true,false,false
703704
ion-loading,prop,spinner,"bubbles" | "circles" | "circular" | "crescent" | "dots" | "lines" | "lines-sharp" | "lines-sharp-small" | "lines-small" | null | undefined,undefined,false,false
704705
ion-loading,prop,translucent,boolean,false,false,false
706+
ion-loading,prop,trigger,string | undefined,undefined,false,false
705707
ion-loading,method,dismiss,dismiss(data?: any, role?: string) => Promise<boolean>
706708
ion-loading,method,onDidDismiss,onDidDismiss<T = any>() => Promise<OverlayEventDetail<T>>
707709
ion-loading,method,onWillDismiss,onWillDismiss<T = any>() => Promise<OverlayEventDetail<T>>
708710
ion-loading,method,present,present() => Promise<void>
711+
ion-loading,event,didDismiss,OverlayEventDetail<any>,true
712+
ion-loading,event,didPresent,void,true
709713
ion-loading,event,ionLoadingDidDismiss,OverlayEventDetail<any>,true
710714
ion-loading,event,ionLoadingDidPresent,void,true
711715
ion-loading,event,ionLoadingWillDismiss,OverlayEventDetail<any>,true
712716
ion-loading,event,ionLoadingWillPresent,void,true
717+
ion-loading,event,willDismiss,OverlayEventDetail<any>,true
718+
ion-loading,event,willPresent,void,true
713719
ion-loading,css-prop,--backdrop-opacity
714720
ion-loading,css-prop,--background
715721
ion-loading,css-prop,--height

core/src/components.d.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,6 +1385,7 @@ export namespace Components {
13851385
* Additional classes to apply for custom CSS. If multiple classes are provided they should be separated by spaces.
13861386
*/
13871387
"cssClass"?: string | string[];
1388+
"delegate"?: FrameworkDelegate;
13881389
/**
13891390
* Dismiss the loading overlay after it has been presented.
13901391
* @param data Any data to emit in the dismiss events.
@@ -1399,10 +1400,15 @@ export namespace Components {
13991400
* Animation to use when the loading indicator is presented.
14001401
*/
14011402
"enterAnimation"?: AnimationBuilder;
1403+
"hasController": boolean;
14021404
/**
14031405
* Additional attributes to pass to the loader.
14041406
*/
14051407
"htmlAttributes"?: LoadingAttributes;
1408+
/**
1409+
* 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.
1410+
*/
1411+
"isOpen": boolean;
14061412
/**
14071413
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
14081414
*/
@@ -1444,6 +1450,10 @@ export namespace Components {
14441450
* If `true`, the loading indicator will be translucent. Only applies when the mode is `"ios"` and the device supports [`backdrop-filter`](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter#Browser_compatibility).
14451451
*/
14461452
"translucent": boolean;
1453+
/**
1454+
* An ID corresponding to the trigger element that causes the loading indicator to open when clicked.
1455+
*/
1456+
"trigger": string | undefined;
14471457
}
14481458
interface IonMenu {
14491459
/**
@@ -5183,6 +5193,7 @@ declare namespace LocalJSX {
51835193
* Additional classes to apply for custom CSS. If multiple classes are provided they should be separated by spaces.
51845194
*/
51855195
"cssClass"?: string | string[];
5196+
"delegate"?: FrameworkDelegate;
51865197
/**
51875198
* Number of milliseconds to wait before dismissing the loading indicator.
51885199
*/
@@ -5191,10 +5202,15 @@ declare namespace LocalJSX {
51915202
* Animation to use when the loading indicator is presented.
51925203
*/
51935204
"enterAnimation"?: AnimationBuilder;
5205+
"hasController"?: boolean;
51945206
/**
51955207
* Additional attributes to pass to the loader.
51965208
*/
51975209
"htmlAttributes"?: LoadingAttributes;
5210+
/**
5211+
* 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.
5212+
*/
5213+
"isOpen"?: boolean;
51985214
/**
51995215
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
52005216
*/
@@ -5211,6 +5227,14 @@ declare namespace LocalJSX {
52115227
* The mode determines which platform styles to use.
52125228
*/
52135229
"mode"?: "ios" | "md";
5230+
/**
5231+
* Emitted after the loading indicator has dismissed. Shorthand for ionLoadingDidDismiss.
5232+
*/
5233+
"onDidDismiss"?: (event: IonLoadingCustomEvent<OverlayEventDetail>) => void;
5234+
/**
5235+
* Emitted after the loading indicator has presented. Shorthand for ionLoadingWillDismiss.
5236+
*/
5237+
"onDidPresent"?: (event: IonLoadingCustomEvent<void>) => void;
52145238
/**
52155239
* Emitted after the loading has dismissed.
52165240
*/
@@ -5227,6 +5251,14 @@ declare namespace LocalJSX {
52275251
* Emitted before the loading has presented.
52285252
*/
52295253
"onIonLoadingWillPresent"?: (event: IonLoadingCustomEvent<void>) => void;
5254+
/**
5255+
* Emitted before the loading indicator has dismissed. Shorthand for ionLoadingWillDismiss.
5256+
*/
5257+
"onWillDismiss"?: (event: IonLoadingCustomEvent<OverlayEventDetail>) => void;
5258+
/**
5259+
* Emitted before the loading indicator has presented. Shorthand for ionLoadingWillPresent.
5260+
*/
5261+
"onWillPresent"?: (event: IonLoadingCustomEvent<void>) => void;
52305262
"overlayIndex": number;
52315263
/**
52325264
* If `true`, a backdrop will be displayed behind the loading indicator.
@@ -5240,6 +5272,10 @@ declare namespace LocalJSX {
52405272
* If `true`, the loading indicator will be translucent. Only applies when the mode is `"ios"` and the device supports [`backdrop-filter`](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter#Browser_compatibility).
52415273
*/
52425274
"translucent"?: boolean;
5275+
/**
5276+
* An ID corresponding to the trigger element that causes the loading indicator to open when clicked.
5277+
*/
5278+
"trigger"?: string | undefined;
52435279
}
52445280
interface IonMenu {
52455281
/**

core/src/components/loading/loading.tsx

Lines changed: 119 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
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+
createTriggerController,
23+
} from '../../utils/overlays';
1424
import type { IonicSafeString } from '../../utils/sanitization';
1525
import { sanitizeDOMString } from '../../utils/sanitization';
1626
import { getClassMap } from '../../utils/theme';
@@ -32,7 +42,10 @@ import { mdLeaveAnimation } from './animations/md.leave';
3242
scoped: true,
3343
})
3444
export class Loading implements ComponentInterface, OverlayInterface {
45+
private readonly delegateController = createDelegateController(this);
46+
private readonly triggerController = createTriggerController();
3547
private durationTimeout: any;
48+
private currentTransition?: Promise<any>;
3649

3750
presented = false;
3851
lastFocus?: HTMLElement;
@@ -42,6 +55,12 @@ export class Loading implements ComponentInterface, OverlayInterface {
4255
/** @internal */
4356
@Prop() overlayIndex!: number;
4457

58+
/** @internal */
59+
@Prop() delegate?: FrameworkDelegate;
60+
61+
/** @internal */
62+
@Prop() hasController = false;
63+
4564
/**
4665
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
4766
*/
@@ -105,6 +124,36 @@ export class Loading implements ComponentInterface, OverlayInterface {
105124
*/
106125
@Prop() htmlAttributes?: LoadingAttributes;
107126

127+
/**
128+
* If `true`, the loading indicator will open. If `false`, the loading indicator will close.
129+
* Use this if you need finer grained control over presentation, otherwise
130+
* just use the loadingController or the `trigger` property.
131+
* Note: `isOpen` will not automatically be set back to `false` when
132+
* the loading indicator dismisses. You will need to do that in your code.
133+
*/
134+
@Prop() isOpen = false;
135+
@Watch('isOpen')
136+
onIsOpenChange(newValue: boolean, oldValue: boolean) {
137+
if (newValue === true && oldValue === false) {
138+
this.present();
139+
} else if (newValue === false && oldValue === true) {
140+
this.dismiss();
141+
}
142+
}
143+
144+
/**
145+
* An ID corresponding to the trigger element that
146+
* causes the loading indicator to open when clicked.
147+
*/
148+
@Prop() trigger: string | undefined;
149+
@Watch('trigger')
150+
triggerChanged() {
151+
const { trigger, el, triggerController } = this;
152+
if (trigger) {
153+
triggerController.addClickListener(el, trigger);
154+
}
155+
}
156+
108157
/**
109158
* Emitted after the loading has presented.
110159
*/
@@ -125,8 +174,33 @@ export class Loading implements ComponentInterface, OverlayInterface {
125174
*/
126175
@Event({ eventName: 'ionLoadingDidDismiss' }) didDismiss!: EventEmitter<OverlayEventDetail>;
127176

177+
/**
178+
* Emitted after the loading indicator has presented.
179+
* Shorthand for ionLoadingWillDismiss.
180+
*/
181+
@Event({ eventName: 'didPresent' }) didPresentShorthand!: EventEmitter<void>;
182+
183+
/**
184+
* Emitted before the loading indicator has presented.
185+
* Shorthand for ionLoadingWillPresent.
186+
*/
187+
@Event({ eventName: 'willPresent' }) willPresentShorthand!: EventEmitter<void>;
188+
189+
/**
190+
* Emitted before the loading indicator has dismissed.
191+
* Shorthand for ionLoadingWillDismiss.
192+
*/
193+
@Event({ eventName: 'willDismiss' }) willDismissShorthand!: EventEmitter<OverlayEventDetail>;
194+
195+
/**
196+
* Emitted after the loading indicator has dismissed.
197+
* Shorthand for ionLoadingDidDismiss.
198+
*/
199+
@Event({ eventName: 'didDismiss' }) didDismissShorthand!: EventEmitter<OverlayEventDetail>;
200+
128201
connectedCallback() {
129202
prepareOverlay(this.el);
203+
this.triggerChanged();
130204
}
131205

132206
componentWillLoad() {
@@ -136,16 +210,48 @@ export class Loading implements ComponentInterface, OverlayInterface {
136210
}
137211
}
138212

213+
componentDidLoad() {
214+
/**
215+
* If loading indicator was rendered with isOpen="true"
216+
* then we should open loading indicator immediately.
217+
*/
218+
if (this.isOpen === true) {
219+
raf(() => this.present());
220+
}
221+
}
222+
223+
disconnectedCallback() {
224+
this.triggerController.removeClickListener();
225+
}
226+
139227
/**
140228
* Present the loading overlay after it has been created.
141229
*/
142230
@Method()
143231
async present(): Promise<void> {
144-
await present(this, 'loadingEnter', iosEnterAnimation, mdEnterAnimation, undefined);
232+
/**
233+
* When using an inline loading indicator
234+
* and dismissing a loading indicator it is possible to
235+
* quickly present the loading indicator while it is
236+
* dismissing. We need to await any current
237+
* transition to allow the dismiss to finish
238+
* before presenting again.
239+
*/
240+
if (this.currentTransition !== undefined) {
241+
await this.currentTransition;
242+
}
243+
244+
await this.delegateController.attachViewToDom();
245+
246+
this.currentTransition = present(this, 'loadingEnter', iosEnterAnimation, mdEnterAnimation);
247+
248+
await this.currentTransition;
145249

146250
if (this.duration > 0) {
147251
this.durationTimeout = setTimeout(() => this.dismiss(), this.duration + 10);
148252
}
253+
254+
this.currentTransition = undefined;
149255
}
150256

151257
/**
@@ -158,11 +264,19 @@ export class Loading implements ComponentInterface, OverlayInterface {
158264
* Some examples include: ``"cancel"`, `"destructive"`, "selected"`, and `"backdrop"`.
159265
*/
160266
@Method()
161-
dismiss(data?: any, role?: string): Promise<boolean> {
267+
async dismiss(data?: any, role?: string): Promise<boolean> {
162268
if (this.durationTimeout) {
163269
clearTimeout(this.durationTimeout);
164270
}
165-
return dismiss(this, data, role, 'loadingLeave', iosLeaveAnimation, mdLeaveAnimation);
271+
this.currentTransition = dismiss(this, data, role, 'loadingLeave', iosLeaveAnimation, mdLeaveAnimation);
272+
273+
const dismissed = await this.currentTransition;
274+
275+
if (dismissed) {
276+
this.delegateController.removeViewFromDom();
277+
}
278+
279+
return dismissed;
166280
}
167281

168282
/**

0 commit comments

Comments
 (0)