Adding wizard and dialog footer loading spinner (#21230)

* Adding wizard and dialog loading

* Moving apis to proposed

* fixing namespace

* Only firing event when the value changes

* Only firing when value is changed

* Adding loading complete message to dialog and wizard

* Registering listeners and making a new base interface for loading components

* Fixing api comment

* Renaming prop to loadingCompleted

* old loading icon
This commit is contained in:
Aasim Khan
2022-11-30 12:18:34 -08:00
committed by GitHub
parent 62d5c1f2d6
commit c0a194df4a
12 changed files with 242 additions and 14 deletions

View File

@@ -306,7 +306,10 @@ export function createViewContext(): ViewTestContext {
onClosed: new vscode.EventEmitter<azdata.window.CloseReason>().event,
registerContent: () => { },
modelView: undefined!,
valid: true
valid: true,
loading: false,
loadingText: '',
loadingCompletedText: ''
};
let wizard: azdata.window.Wizard = {
title: '',
@@ -327,7 +330,10 @@ export function createViewContext(): ViewTestContext {
close: () => { return Promise.resolve(); },
registerNavigationValidator: () => { },
message: dialogMessage,
registerOperation: () => { }
registerOperation: () => { },
loading: false,
loadingText: '',
loadingCompletedText: ''
};
let wizardPage: azdata.window.WizardPage = {
title: '',

View File

@@ -134,6 +134,24 @@ declare module 'azdata' {
}
}
export interface LoadingComponentBase {
/**
* When true, the component will display a loading spinner.
*/
loading?: boolean;
/**
* This sets the alert text which gets announced when the loading spinner is shown.
*/
loadingText?: string;
/**
* The text to display while loading is set to false. Will also be announced through screen readers
* once loading is completed.
*/
loadingCompletedText?: string;
}
/**
* The column information of a data set.
*/
@@ -1702,4 +1720,12 @@ declare module 'azdata' {
*/
objectType?: string;
}
export namespace window {
export interface Wizard extends LoadingComponentBase {
}
export interface Dialog extends LoadingComponentBase {
}
}
}

View File

@@ -4,7 +4,6 @@
circle {
animation: ball 0.6s linear infinite;
}
circle:nth-child(2) { animation-delay: 0.075s; }
circle:nth-child(3) { animation-delay: 0.15s; }
circle:nth-child(4) { animation-delay: 0.225s; }
@@ -12,7 +11,6 @@
circle:nth-child(6) { animation-delay: 0.375s; }
circle:nth-child(7) { animation-delay: 0.45s; }
circle:nth-child(8) { animation-delay: 0.525s; }
@keyframes ball {
0% { opacity: 1; }
100% { opacity: 0.3; }

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -144,6 +144,9 @@ export class MainThreadModelViewDialog extends Disposable implements MainThreadM
dialog.customButtons = details.customButtons.map(buttonHandle => this.getButton(buttonHandle));
}
dialog.message = details.message;
dialog.loading = details.loading;
dialog.loadingText = dialog.loadingText;
dialog.loadingCompletedText = dialog.loadingCompletedText;
return Promise.resolve();
}
@@ -223,7 +226,9 @@ export class MainThreadModelViewDialog extends Disposable implements MainThreadM
wizard.customButtons = details.customButtons.map(buttonHandle => this.getButton(buttonHandle));
}
wizard.message = details.message;
wizard.loading = details.loading;
wizard.loadingText = details.loadingText;
wizard.loadingCompletedText = details.loadingCompletedText;
return Promise.resolve();
}

View File

@@ -133,6 +133,9 @@ class DialogImpl extends ModelViewPanelImpl implements azdata.window.Dialog {
private _renderHeader: boolean;
private _renderFooter: boolean;
private _dialogProperties: IDialogProperties;
private _loading: boolean;
private _loadingText: string;
private _loadingCompletedText: string;
private _onClosed = new Emitter<azdata.window.CloseReason>();
public onClosed = this._onClosed.event;
@@ -216,6 +219,33 @@ class DialogImpl extends ModelViewPanelImpl implements azdata.window.Dialog {
this._extHostModelViewDialog.updateDialogContent(this);
}
public get loading(): boolean {
return this._loading;
}
public set loading(value: boolean) {
this._loading = value;
this._extHostModelViewDialog.updateDialogContent(this);
}
public get loadingText(): string {
return this._loadingText;
}
public set loadingText(value: string) {
this._loadingText = value;
this._extHostModelViewDialog.updateDialogContent(this);
}
public get loadingCompletedText(): string {
return this._loadingCompletedText;
}
public set loadingCompletedText(value: string) {
this._loadingCompletedText = value;
this._extHostModelViewDialog.updateDialogContent(this);
}
public get dialogName(): string {
return this._dialogName;
}
@@ -443,6 +473,9 @@ class WizardImpl implements azdata.window.Wizard {
public readonly onPageChanged = this._pageChangedEmitter.event;
private _navigationValidator: (info: azdata.window.WizardPageChangeInfo) => boolean | Thenable<boolean>;
private _message: azdata.window.DialogMessage;
private _loading: boolean;
private _loadingText: string;
private _loadingCompletedText: string;
private _displayPageTitles: boolean = true;
private _operationHandler: BackgroundOperationHandler;
private _width: DialogWidth;
@@ -487,6 +520,33 @@ class WizardImpl implements azdata.window.Wizard {
this._extHostModelViewDialog.updateWizard(this);
}
public get loading(): boolean {
return this._loading;
}
public set loading(value: boolean) {
this._loading = value;
this._extHostModelViewDialog.updateWizard(this);
}
public get loadingText(): string {
return this._loadingText
}
public set loadingText(value: string) {
this._loadingText = value;
this._extHostModelViewDialog.updateWizard(this);
}
public get loadingCompletedText(): string {
return this._loadingCompletedText;
}
public set loadingCompletedText(value: string) {
this._loadingCompletedText = value;
this._extHostModelViewDialog.updateWizard(this);
}
public get displayPageTitles(): boolean {
return this._displayPageTitles;
}
@@ -797,7 +857,10 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape {
cancelButton: this.getHandle(dialog.cancelButton),
content: dialog.content && typeof dialog.content !== 'string' ? dialog.content.map(tab => this.getHandle(tab)) : dialog.content as string,
customButtons: dialog.customButtons ? dialog.customButtons.map(button => this.getHandle(button)) : undefined,
message: dialog.message
message: dialog.message,
loading: dialog.loading,
loadingText: dialog.loadingText,
loadingCompletedText: dialog.loadingCompletedText
});
}
@@ -944,7 +1007,10 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape {
nextButton: this.getHandle(wizard.nextButton),
customButtons: wizard.customButtons ? wizard.customButtons.map(button => this.getHandle(button)) : undefined,
message: wizard.message,
displayPageTitles: wizard.displayPageTitles
displayPageTitles: wizard.displayPageTitles,
loading: wizard.loading,
loadingText: wizard.loadingText,
loadingCompletedText: wizard.loadingCompletedText,
});
}

View File

@@ -268,6 +268,9 @@ export interface IModelViewDialogDetails {
renderHeader: boolean;
renderFooter: boolean;
dialogProperties: IDialogProperties;
loading: boolean;
loadingText: string;
loadingCompletedText: string;
}
export interface IModelViewTabDetails {
@@ -307,6 +310,9 @@ export interface IModelViewWizardDetails {
message: DialogMessage;
displayPageTitles: boolean;
width: DialogWidth;
loading: boolean;
loadingText: string;
loadingCompletedText: string;
}
export type DialogWidth = 'narrow' | 'medium' | 'wide' | number | string;

View File

@@ -81,6 +81,7 @@ export interface IModalOptions {
hasErrors?: boolean;
hasSpinner?: boolean;
spinnerTitle?: string;
onSpinnerHideText?: string;
renderHeader?: boolean;
renderFooter?: boolean;
dialogProperties?: IDialogProperties;
@@ -94,7 +95,7 @@ const defaultOptions: IModalOptions = {
hasBackButton: false,
hasTitleIcon: false,
hasErrors: false,
hasSpinner: false,
hasSpinner: true,
renderHeader: true,
renderFooter: true,
dialogProperties: undefined
@@ -638,6 +639,9 @@ export abstract class Modal extends Disposable implements IThemable {
}
} else {
DOM.hide(this._spinnerElement!);
if (this._modalOptions.onSpinnerHideText) {
alert(this._modalOptions.onSpinnerHideText);
}
}
}
}

View File

@@ -9,8 +9,8 @@ import { Dialog, Wizard } from 'sql/workbench/services/dialog/common/dialogTypes
import { IModalOptions } from 'sql/workbench/browser/modal/modal';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export const DefaultDialogOptions: IModalOptions = { hasBackButton: false, width: 'narrow', hasErrors: true };
export const DefaultWizardOptions: IModalOptions = { hasBackButton: false, width: 'wide', hasErrors: true };
export const DefaultDialogOptions: IModalOptions = { hasBackButton: false, width: 'narrow', hasErrors: true, hasSpinner: true };
export const DefaultWizardOptions: IModalOptions = { hasBackButton: false, width: 'wide', hasErrors: true, hasSpinner: true };
export class CustomDialogService {
private _dialogModals = new Map<Dialog, DialogModal>();

View File

@@ -93,7 +93,17 @@ export class DialogModal extends Modal {
};
messageChangeHandler(this._dialog.message);
this._dialog.onMessageChange(message => messageChangeHandler(message));
this._register(this._dialog.onMessageChange(message => messageChangeHandler(message)));
this._register(this._dialog.onLoadingChange((loadingState) => {
this.spinner = loadingState;
}));
this._register(this._dialog.onLoadingTextChange((loadingText) => {
this._modalOptions.spinnerTitle = loadingText;
}));
this._register(this._dialog.onLoadingCompletedTextChange((loadingCompletedText) => {
this._modalOptions.onSpinnerHideText = loadingCompletedText;
}));
}
private addDialogButton(button: DialogButton, onSelect: () => void = () => undefined, registerClickEvent: boolean = true, requireDialogValid: boolean = false): Button {

View File

@@ -96,7 +96,21 @@ export class WizardModal extends Modal {
};
messageChangeHandler(this._wizard.message);
this._wizard.onMessageChange(message => messageChangeHandler(message));
this._register(this._wizard.onMessageChange(message => messageChangeHandler(message)));
this._register(this._wizard.onLoadingChange((loadingState) => {
this.spinner = loadingState;
}));
this._register(this._wizard.onLoadingChange((loadingState) => {
this.spinner = loadingState;
}));
this._register(this._wizard.onLoadingTextChange((loadingText) => {
this._modalOptions.spinnerTitle = loadingText;
}));
this._register(this._wizard.onLoadingCompletedTextChange((loadingCompletedText) => {
this._modalOptions.onSpinnerHideText = loadingCompletedText;
}));
}
private addDialogButton(button: DialogButton, onSelect: () => void = () => undefined, registerClickEvent: boolean = true, requirePageValid: boolean = false, index?: number): Button {

View File

@@ -53,6 +53,16 @@ export class Dialog extends ModelViewPane {
public customButtons: DialogButton[] = [];
private _onMessageChange = new Emitter<DialogMessage | undefined>();
public readonly onMessageChange = this._onMessageChange.event;
private _loading: boolean = false;
private _loadingText: string;
private _loadingCompletedText: string;
private _onLoadingChange = new Emitter<boolean | undefined>();
private _onLoadingTextChange = new Emitter<string | undefined>();
private _onLoadingCompletedTextChange = new Emitter<string | undefined>();
public readonly onLoadingChange = this._onLoadingChange.event;
public readonly onLoadingTextChange = this._onLoadingTextChange.event;
public readonly onLoadingCompletedTextChange = this._onLoadingCompletedTextChange.event;
private _message: DialogMessage | undefined;
private _closeValidator: CloseValidator | undefined;
@@ -87,6 +97,39 @@ export class Dialog extends ModelViewPane {
this._onMessageChange.fire(this._message);
}
public get loading(): boolean {
return this._loading;
}
public set loading(value: boolean) {
if (this.loading !== value) {
this._loading = value;
this._onLoadingChange.fire(this._loading);
}
}
public get loadingText(): string | undefined {
return this._loadingText;
}
public set loadingText(value: string | undefined) {
if (this.loadingText !== value) {
this._loadingText = value;
this._onLoadingTextChange.fire(this._loadingText);
}
}
public get loadingCompletedText(): string | undefined {
return this._loadingCompletedText;
}
public set loadingCompletedText(value: string | undefined) {
if (this._loadingCompletedText !== value) {
this._loadingCompletedText = value;
this._onLoadingCompletedTextChange.fire(this._loadingCompletedText);
}
}
public registerCloseValidator(validator: CloseValidator): void {
this._closeValidator = validator;
}
@@ -247,6 +290,15 @@ export class Wizard {
private _message: DialogMessage | undefined;
public displayPageTitles: boolean = false;
public width: DialogWidth | undefined;
private _loading: boolean = false;
private _loadingText: string;
private _loadingCompletedText: string;
private _onLoadingChange = new Emitter<boolean | undefined>();
private _onLoadingTextChange = new Emitter<string | undefined>();
private _onLoadingCompletedTextChange = new Emitter<string | undefined>();
public readonly onLoadingChange = this._onLoadingChange.event;
public readonly onLoadingTextChange = this._onLoadingTextChange.event;
public readonly onLoadingCompletedTextChange = this._onLoadingCompletedTextChange.event;
constructor(public title: string,
public readonly name: string,
@@ -329,4 +381,39 @@ export class Wizard {
this._message = value;
this._onMessageChange.fire(this._message);
}
public get loading(): boolean {
return this._loading;
}
public set loading(value: boolean) {
if (this.loading !== value) {
this._loading = value;
this._onLoadingChange.fire(this._loading);
}
}
public get loadingText(): string | undefined {
return this._loadingText;
}
public set loadingText(value: string | undefined) {
if (this.loadingText !== value) {
this._loadingText = value;
this._onLoadingTextChange.fire(this._loadingText);
}
}
public get loadingCompletedText(): string | undefined {
return this._loadingCompletedText;
}
public set loadingCompletedText(value: string | undefined) {
if (this._loadingCompletedText !== value) {
this._loadingCompletedText = value;
this._onLoadingCompletedTextChange.fire(this._loadingCompletedText);
}
}
}

View File

@@ -136,7 +136,10 @@ suite('MainThreadModelViewDialog Tests', () => {
okButton: okButtonHandle,
cancelButton: cancelButtonHandle,
customButtons: [button1Handle, button2Handle],
message: undefined
message: undefined,
loading: false,
loadingText: undefined,
loadingCompletedText: undefined,
};
// Set up the wizard details
@@ -183,7 +186,10 @@ suite('MainThreadModelViewDialog Tests', () => {
pages: [page1Handle, page2Handle],
message: undefined,
displayPageTitles: false,
width: 'wide'
width: 'wide',
loading: false,
loadingText: undefined,
loadingCompletedText: undefined
};
// Register the buttons, tabs, and dialog