Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(loading): use loading overlay inline #26153

Merged
merged 5 commits into from
Oct 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -695,21 +695,27 @@ ion-loading,prop,cssClass,string | string[] | undefined,undefined,false,false
ion-loading,prop,duration,number,0,false,false
ion-loading,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
ion-loading,prop,htmlAttributes,undefined | { [key: string]: any; },undefined,false,false
ion-loading,prop,isOpen,boolean,false,false,false
ion-loading,prop,keyboardClose,boolean,true,false,false
ion-loading,prop,leaveAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
ion-loading,prop,message,IonicSafeString | string | undefined,undefined,false,false
ion-loading,prop,mode,"ios" | "md",undefined,false,false
ion-loading,prop,showBackdrop,boolean,true,false,false
ion-loading,prop,spinner,"bubbles" | "circles" | "circular" | "crescent" | "dots" | "lines" | "lines-sharp" | "lines-sharp-small" | "lines-small" | null | undefined,undefined,false,false
ion-loading,prop,translucent,boolean,false,false,false
ion-loading,prop,trigger,string | undefined,undefined,false,false
ion-loading,method,dismiss,dismiss(data?: any, role?: string) => Promise<boolean>
ion-loading,method,onDidDismiss,onDidDismiss<T = any>() => Promise<OverlayEventDetail<T>>
ion-loading,method,onWillDismiss,onWillDismiss<T = any>() => Promise<OverlayEventDetail<T>>
ion-loading,method,present,present() => Promise<void>
ion-loading,event,didDismiss,OverlayEventDetail<any>,true
ion-loading,event,didPresent,void,true
ion-loading,event,ionLoadingDidDismiss,OverlayEventDetail<any>,true
ion-loading,event,ionLoadingDidPresent,void,true
ion-loading,event,ionLoadingWillDismiss,OverlayEventDetail<any>,true
ion-loading,event,ionLoadingWillPresent,void,true
ion-loading,event,willDismiss,OverlayEventDetail<any>,true
ion-loading,event,willPresent,void,true
ion-loading,css-prop,--backdrop-opacity
ion-loading,css-prop,--background
ion-loading,css-prop,--height
Expand Down
36 changes: 36 additions & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1385,6 +1385,7 @@ export namespace Components {
* Additional classes to apply for custom CSS. If multiple classes are provided they should be separated by spaces.
*/
"cssClass"?: string | string[];
"delegate"?: FrameworkDelegate;
/**
* Dismiss the loading overlay after it has been presented.
* @param data Any data to emit in the dismiss events.
Expand All @@ -1399,10 +1400,15 @@ export namespace Components {
* Animation to use when the loading indicator is presented.
*/
"enterAnimation"?: AnimationBuilder;
"hasController": boolean;
/**
* Additional attributes to pass to the loader.
*/
"htmlAttributes"?: LoadingAttributes;
/**
* 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.
*/
"isOpen": boolean;
/**
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
*/
Expand Down Expand Up @@ -1444,6 +1450,10 @@ export namespace Components {
* 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).
*/
"translucent": boolean;
/**
* An ID corresponding to the trigger element that causes the loading indicator to open when clicked.
*/
"trigger": string | undefined;
}
interface IonMenu {
/**
Expand Down Expand Up @@ -5183,6 +5193,7 @@ declare namespace LocalJSX {
* Additional classes to apply for custom CSS. If multiple classes are provided they should be separated by spaces.
*/
"cssClass"?: string | string[];
"delegate"?: FrameworkDelegate;
/**
* Number of milliseconds to wait before dismissing the loading indicator.
*/
Expand All @@ -5191,10 +5202,15 @@ declare namespace LocalJSX {
* Animation to use when the loading indicator is presented.
*/
"enterAnimation"?: AnimationBuilder;
"hasController"?: boolean;
/**
* Additional attributes to pass to the loader.
*/
"htmlAttributes"?: LoadingAttributes;
/**
* 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.
*/
"isOpen"?: boolean;
/**
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
*/
Expand All @@ -5211,6 +5227,14 @@ declare namespace LocalJSX {
* The mode determines which platform styles to use.
*/
"mode"?: "ios" | "md";
/**
* Emitted after the loading indicator has dismissed. Shorthand for ionLoadingDidDismiss.
*/
"onDidDismiss"?: (event: IonLoadingCustomEvent<OverlayEventDetail>) => void;
/**
* Emitted after the loading indicator has presented. Shorthand for ionLoadingWillDismiss.
*/
"onDidPresent"?: (event: IonLoadingCustomEvent<void>) => void;
/**
* Emitted after the loading has dismissed.
*/
Expand All @@ -5227,6 +5251,14 @@ declare namespace LocalJSX {
* Emitted before the loading has presented.
*/
"onIonLoadingWillPresent"?: (event: IonLoadingCustomEvent<void>) => void;
/**
* Emitted before the loading indicator has dismissed. Shorthand for ionLoadingWillDismiss.
*/
"onWillDismiss"?: (event: IonLoadingCustomEvent<OverlayEventDetail>) => void;
/**
* Emitted before the loading indicator has presented. Shorthand for ionLoadingWillPresent.
*/
"onWillPresent"?: (event: IonLoadingCustomEvent<void>) => void;
"overlayIndex": number;
/**
* If `true`, a backdrop will be displayed behind the loading indicator.
Expand All @@ -5240,6 +5272,10 @@ declare namespace LocalJSX {
* 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).
*/
"translucent"?: boolean;
/**
* An ID corresponding to the trigger element that causes the loading indicator to open when clicked.
*/
"trigger"?: string | undefined;
}
interface IonMenu {
/**
Expand Down
124 changes: 119 additions & 5 deletions core/src/components/loading/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Method, Prop, h } from '@stencil/core';
import { Watch, Component, Element, Event, Host, Method, Prop, h } from '@stencil/core';

import { config } from '../../global/config';
import { getIonMode } from '../../global/ionic-global';
import type {
AnimationBuilder,
FrameworkDelegate,
LoadingAttributes,
OverlayEventDetail,
OverlayInterface,
SpinnerTypes,
} from '../../interface';
import { BACKDROP, dismiss, eventMethod, prepareOverlay, present } from '../../utils/overlays';
import { raf } from '../../utils/helpers';
import {
BACKDROP,
dismiss,
eventMethod,
prepareOverlay,
present,
createDelegateController,
createTriggerController,
} from '../../utils/overlays';
import type { IonicSafeString } from '../../utils/sanitization';
import { sanitizeDOMString } from '../../utils/sanitization';
import { getClassMap } from '../../utils/theme';
Expand All @@ -32,7 +42,10 @@ import { mdLeaveAnimation } from './animations/md.leave';
scoped: true,
})
export class Loading implements ComponentInterface, OverlayInterface {
private readonly delegateController = createDelegateController(this);
private readonly triggerController = createTriggerController();
private durationTimeout: any;
private currentTransition?: Promise<any>;

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

/** @internal */
@Prop() delegate?: FrameworkDelegate;

/** @internal */
@Prop() hasController = false;

/**
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
*/
Expand Down Expand Up @@ -105,6 +124,36 @@ export class Loading implements ComponentInterface, OverlayInterface {
*/
@Prop() htmlAttributes?: LoadingAttributes;

/**
* 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.
*/
@Prop() isOpen = false;
@Watch('isOpen')
onIsOpenChange(newValue: boolean, oldValue: boolean) {
if (newValue === true && oldValue === false) {
this.present();
} else if (newValue === false && oldValue === true) {
this.dismiss();
}
}

/**
* An ID corresponding to the trigger element that
* causes the loading indicator to open when clicked.
*/
@Prop() trigger: string | undefined;
@Watch('trigger')
triggerChanged() {
const { trigger, el, triggerController } = this;
if (trigger) {
triggerController.addClickListener(el, trigger);
}
}

/**
* Emitted after the loading has presented.
*/
Expand All @@ -125,8 +174,33 @@ export class Loading implements ComponentInterface, OverlayInterface {
*/
@Event({ eventName: 'ionLoadingDidDismiss' }) didDismiss!: EventEmitter<OverlayEventDetail>;

/**
* Emitted after the loading indicator has presented.
* Shorthand for ionLoadingWillDismiss.
*/
@Event({ eventName: 'didPresent' }) didPresentShorthand!: EventEmitter<void>;

/**
* Emitted before the loading indicator has presented.
* Shorthand for ionLoadingWillPresent.
*/
@Event({ eventName: 'willPresent' }) willPresentShorthand!: EventEmitter<void>;

/**
* Emitted before the loading indicator has dismissed.
* Shorthand for ionLoadingWillDismiss.
*/
@Event({ eventName: 'willDismiss' }) willDismissShorthand!: EventEmitter<OverlayEventDetail>;

/**
* Emitted after the loading indicator has dismissed.
* Shorthand for ionLoadingDidDismiss.
*/
@Event({ eventName: 'didDismiss' }) didDismissShorthand!: EventEmitter<OverlayEventDetail>;

connectedCallback() {
prepareOverlay(this.el);
this.triggerChanged();
}

componentWillLoad() {
Expand All @@ -136,16 +210,48 @@ export class Loading implements ComponentInterface, OverlayInterface {
}
}

componentDidLoad() {
/**
* If loading indicator was rendered with isOpen="true"
* then we should open loading indicator immediately.
*/
if (this.isOpen === true) {
raf(() => this.present());
}
}

disconnectedCallback() {
this.triggerController.removeClickListener();
}

/**
* Present the loading overlay after it has been created.
*/
@Method()
async present(): Promise<void> {
await present(this, 'loadingEnter', iosEnterAnimation, mdEnterAnimation, undefined);
/**
* When using an inline loading indicator
* and dismissing a loading indicator it is possible to
* quickly present the loading indicator while it is
* dismissing. We need to await any current
* transition to allow the dismiss to finish
* before presenting again.
*/
if (this.currentTransition !== undefined) {
await this.currentTransition;
}

await this.delegateController.attachViewToDom();

this.currentTransition = present(this, 'loadingEnter', iosEnterAnimation, mdEnterAnimation);

await this.currentTransition;

if (this.duration > 0) {
this.durationTimeout = setTimeout(() => this.dismiss(), this.duration + 10);
}

this.currentTransition = undefined;
}

/**
Expand All @@ -158,11 +264,19 @@ export class Loading implements ComponentInterface, OverlayInterface {
* Some examples include: ``"cancel"`, `"destructive"`, "selected"`, and `"backdrop"`.
*/
@Method()
dismiss(data?: any, role?: string): Promise<boolean> {
async dismiss(data?: any, role?: string): Promise<boolean> {
if (this.durationTimeout) {
clearTimeout(this.durationTimeout);
}
return dismiss(this, data, role, 'loadingLeave', iosLeaveAnimation, mdLeaveAnimation);
this.currentTransition = dismiss(this, data, role, 'loadingLeave', iosLeaveAnimation, mdLeaveAnimation);

const dismissed = await this.currentTransition;

if (dismissed) {
this.delegateController.removeViewFromDom();
}

return dismissed;
}

/**
Expand Down
Loading