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

WIP: refactore dialog DOM creation #1671

Closed
Closed
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
2 changes: 0 additions & 2 deletions packages/dialog/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export {
IDialogService,
IDialogController,
IDialogDomRenderer,
IDialogDom,

// dialog results
DialogCloseResult,
Expand Down Expand Up @@ -46,7 +45,6 @@ export {
} from './plugins/dialog/dialog-configuration';

export {
DefaultDialogDom,
DefaultDialogDomRenderer,
DefaultDialogGlobalSettings,
} from './plugins/dialog/dialog-default-impl';
72 changes: 31 additions & 41 deletions packages/dialog/src/plugins/dialog/dialog-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
DialogDeactivationStatuses,
IDialogController,
IDialogDomRenderer,
IDialogDom,
DialogOpenResult,
DialogCloseResult,
DialogCancelError,
Expand Down Expand Up @@ -47,9 +46,9 @@ export class DialogController implements IDialogController {
public readonly closed: Promise<DialogCloseResult>;

/**
* The dom structure created to support the dialog associated with this controller
* The renderer used to create dialog DOM
*/
private dom!: IDialogDom;
private renderer!: IDialogDomRenderer;

/**
* The component controller associated with this dialog controller
Expand All @@ -74,39 +73,43 @@ export class DialogController implements IDialogController {

/** @internal */
public activate(settings: IDialogLoadedSettings): Promise<DialogOpenResult> {
const container = this.ctn.createChild();
const { model, template, rejectOnCancel } = settings;
const hostRenderer: IDialogDomRenderer = container.get(IDialogDomRenderer);
const dialogTargetHost = settings.host ?? this.p.document.body;
const dom = this.dom = hostRenderer.render(dialogTargetHost, settings);
const rootEventTarget = container.has(IEventTarget, true)
? container.get(IEventTarget) as Element
: null;
const contentHost = dom.contentHost;

this.settings = settings;
// TODO: use CEs name if provided?
const contentHost = document.createElement('div');

const container = this.ctn.createChild();
container.register(instanceRegistration(IDialogController, this));

const renderer = this.ctn.get(IDialogDomRenderer);
container.register(instanceRegistration(IDialogDomRenderer, renderer));

// moved to renderer
// const dialogTargetHost = settings.host ?? this.p.document.body;

// delegate binding has been removed, so don't need this any more?
// application root host may be a different element with the dialog root host
// example:
// <body>
// <my-app>
// <au-dialog-container>
// when it's different, needs to ensure delegate bindings work
if (rootEventTarget == null || !rootEventTarget.contains(dialogTargetHost)) {
container.register(instanceRegistration(IEventTarget, dialogTargetHost));
}
// const rootEventTarget = container.has(IEventTarget, true)
// ? container.get(IEventTarget) as Element
// : null;
// if (rootEventTarget == null || !rootEventTarget.contains(dialogTargetHost)) {
// container.register(instanceRegistration(IEventTarget, dialogTargetHost));
// }

container.register(
instanceRegistration(INode, contentHost),
instanceRegistration(IDialogDom, dom),
);
this.settings = settings;
this.renderer = renderer;

return new Promise(r => {
const cmp = Object.assign(this.cmp = this.getOrCreateVm(container, settings, contentHost), { $dialog: this });
r(cmp.canActivate?.(model) ?? true);
})
.then(canActivate => {
if (canActivate !== true) {
dom.dispose();
if (rejectOnCancel) {
throw createDialogCancelError(null, 'Dialog activation rejected');
}
Expand All @@ -116,7 +119,7 @@ export class DialogController implements IDialogController {
const cmp = this.cmp;

return onResolve(cmp.activate?.(model), () => {
const ctrlr = this.controller = Controller.$el(
const controller = this.controller = Controller.$el(
container,
cmp,
contentHost,
Expand All @@ -125,14 +128,12 @@ export class DialogController implements IDialogController {
this.getDefinition(cmp) ?? { name: CustomElement.generateName(), template }
)
) as ICustomElementController;
return onResolve(ctrlr.activate(ctrlr, null, LifecycleFlags.fromBind), () => {
dom.overlay.addEventListener(settings.mouseEvent ?? 'click', this);
return DialogOpenResult.create(false, this);
return onResolve(renderer.render(controller), () => {
bigopon marked this conversation as resolved.
Show resolved Hide resolved
return onResolve(controller.activate(controller, null, LifecycleFlags.fromBind), () => {
return DialogOpenResult.create(false, this);
});
});
});
}, e => {
dom.dispose();
throw e;
});
}

Expand All @@ -143,7 +144,7 @@ export class DialogController implements IDialogController {
}

let deactivating = true;
const { controller, dom, cmp, settings: { mouseEvent, rejectOnCancel }} = this;
const { controller, renderer, cmp, settings: { rejectOnCancel }} = this;
const dialogResult = DialogCloseResult.create(status, value);

const promise: Promise<DialogCloseResult<T>> = new Promise<DialogCloseResult<T>>(r => {
Expand All @@ -162,8 +163,7 @@ export class DialogController implements IDialogController {
return onResolve(cmp.deactivate?.(dialogResult),
() => onResolve(controller.deactivate(controller, null, LifecycleFlags.fromUnbind),
() => {
dom.dispose();
dom.overlay.removeEventListener(mouseEvent ?? 'click', this);
renderer.dispose();
if (!rejectOnCancel && status !== DialogDeactivationStatuses.Error) {
this._resolve(dialogResult);
} else {
Expand Down Expand Up @@ -217,23 +217,13 @@ export class DialogController implements IDialogController {
() => onResolve(
this.controller.deactivate(this.controller, null, LifecycleFlags.fromUnbind),
() => {
this.dom.dispose();
this.renderer.dispose();
this._reject(closeError);
}
)
)));
}

/** @internal */
public handleEvent(event: MouseEvent): void {
if (/* user allows dismiss on overlay click */this.settings.overlayDismiss
&& /* did not click inside the host element */!this.dom.contentHost.contains(event.target as Element)
) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.cancel();
}
}

private getOrCreateVm(container: IContainer, settings: IDialogLoadedSettings, host: HTMLElement): IDialogComponent<object> {
const Component = settings.component;
if (Component == null) {
Expand Down
103 changes: 73 additions & 30 deletions packages/dialog/src/plugins/dialog/dialog-default-impl.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { IPlatform } from '@aurelia/runtime-html';
import {
IDialogDomRenderer,
IDialogDom,
IDialogGlobalSettings,
} from './dialog-interfaces';
import { ICustomElementController, IPlatform } from '@aurelia/runtime-html';
import { IDialogDomRenderer, IDialogGlobalSettings, DialogActionKey, IDialogController } from './dialog-interfaces';

import { IContainer } from '@aurelia/kernel';
import { singletonRegistration } from '../../utilities-di';
import { singletonRegistration, transientRegistration } from '../../utilities-di';

export class DefaultDialogGlobalSettings implements IDialogGlobalSettings {

Expand All @@ -20,45 +16,92 @@ export class DefaultDialogGlobalSettings implements IDialogGlobalSettings {
}

const baseWrapperCss = 'position:absolute;width:100%;height:100%;top:0;left:0;';
const wrapperCss = `${baseWrapperCss}display:flex;`;
const hostCss = 'position:relative;margin:auto;';

export class DefaultDialogDomRenderer implements IDialogDomRenderer {
export class DefaultDialogDomRenderer implements IDialogDomRenderer, EventListenerObject {

/** @internal */
protected static inject = [IPlatform];
protected static inject = [IPlatform, IDialogController];

public constructor(private readonly p: IPlatform) {}
public wrapper!: HTMLElement;

public overlay!: HTMLElement;

public contentHost!: HTMLElement;

public constructor(private readonly platform: IPlatform, private readonly dialogController: IDialogController) {}

public static register(container: IContainer) {
singletonRegistration(IDialogDomRenderer, this).register(container);
transientRegistration(IDialogDomRenderer, this).register(container);
}

private readonly wrapperCss: string = `${baseWrapperCss} display:flex;`;
private readonly overlayCss: string = baseWrapperCss;
private readonly hostCss: string = 'position:relative;margin:auto;';
public render(componentController: ICustomElementController) {
const { document } = this.platform;
const { settings } = this.dialogController;
const dialogHost = settings.host ?? document.body;

public render(dialogHost: HTMLElement): IDialogDom {
const doc = this.p.document;
const h = (name: string, css: string) => {
const el = doc.createElement(name);
const h = (name: string, css: string): HTMLElement => {
const el = document.createElement(name);
el.style.cssText = css;
return el;
};
const wrapper = dialogHost.appendChild(h('au-dialog-container', this.wrapperCss));
const overlay = wrapper.appendChild(h('au-dialog-overlay', this.overlayCss));
const host = wrapper.appendChild(h('div', this.hostCss));
return new DefaultDialogDom(wrapper, overlay, host);
}
}

export class DefaultDialogDom implements IDialogDom {
public constructor(
public readonly wrapper: HTMLElement,
public readonly overlay: HTMLElement,
public readonly contentHost: HTMLElement,
) {
const wrapper = dialogHost.appendChild(h('au-dialog-container', wrapperCss));
wrapper.setAttribute('tabindex', '-1');
const overlay = wrapper.appendChild(h('au-dialog-overlay', baseWrapperCss));
const contentHost = wrapper.appendChild(componentController.host);
contentHost.style.cssText = hostCss;

overlay.addEventListener(settings.mouseEvent ?? 'click', this);
wrapper.addEventListener('keydown', this);

this.wrapper = wrapper;
this.overlay = overlay;
this.contentHost = contentHost;
}

public dispose(): void {
this.wrapper.removeEventListener('keydown', this);
this.overlay.removeEventListener(this.dialogController.settings.mouseEvent ?? 'click', this);
this.wrapper.remove();
}

/** @internal */
public handleEvent(event: KeyboardEvent | MouseEvent): void {
const { dialogController } = this;

// handle wrapper keydown
if (event.type === 'keydown') {
const key = getActionKey(event as KeyboardEvent);
if (key == null) {
return;
}

const keyboard = dialogController.settings.keyboard;
if (key === 'Escape' && keyboard.includes(key)) {
void dialogController.cancel();
} else if (key === 'Enter' && keyboard.includes(key)) {
void dialogController.ok();
}
return;
}

// handle overlay click
if (/* user allows to dismiss on overlay click */dialogController.settings.overlayDismiss
&& /* did not click inside the host element */!this.contentHost.contains(event.target as Element)
) {
void dialogController.cancel();
}
}
}

function getActionKey(e: KeyboardEvent): DialogActionKey | undefined {
if ((e.code || e.key) === 'Escape' || e.keyCode === 27) {
return 'Escape';
}
if ((e.code || e.key) === 'Enter' || e.keyCode === 13) {
return 'Enter';
}
return undefined;
}
15 changes: 3 additions & 12 deletions packages/dialog/src/plugins/dialog/dialog-interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createInterface } from '../../utilities-di';

import type { Constructable, IContainer, IDisposable } from '@aurelia/kernel';
import type { ICustomElementViewModel } from '@aurelia/runtime-html';
import type { ICustomElementController, ICustomElementViewModel } from '@aurelia/runtime-html';

/**
* The dialog service for composing view & view model into a dialog
Expand Down Expand Up @@ -45,17 +45,8 @@ export interface IDialogController {
* An interface describing the object responsible for creating the dom structure of a dialog
*/
export const IDialogDomRenderer = createInterface<IDialogDomRenderer>('IDialogDomRenderer');
export interface IDialogDomRenderer {
render(dialogHost: Element, settings: IDialogLoadedSettings): IDialogDom;
}

/**
* An interface describing the DOM structure of a dialog
*/
export const IDialogDom = createInterface<IDialogDom>('IDialogDom');
export interface IDialogDom extends IDisposable {
readonly overlay: HTMLElement;
readonly contentHost: HTMLElement;
export interface IDialogDomRenderer extends IDisposable {
render(componentController: ICustomElementController): void | Promise<void>;
}

// export type IDialogCancellableOpenResult = IDialogOpenResult | IDialogCancelResult;
Expand Down
38 changes: 1 addition & 37 deletions packages/dialog/src/plugins/dialog/dialog-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { IContainer, onResolve, resolveAll } from '@aurelia/kernel';
import { AppTask, IPlatform } from '@aurelia/runtime-html';

import {
DialogActionKey,
DialogCloseResult,
DialogDeactivationStatuses,
DialogOpenResult,
Expand Down Expand Up @@ -104,10 +103,7 @@ export class DialogService implements IDialogService {
dialogController.activate(loadedSettings),
openResult => {
if (!openResult.wasCancelled) {
if (this.dlgs.push(dialogController) === 1) {
this.p.window.addEventListener('keydown', this);
}

this.dlgs.push(dialogController);
const $removeController = () => this.remove(dialogController);
dialogController.closed.then($removeController, $removeController);
}
Expand Down Expand Up @@ -152,28 +148,6 @@ export class DialogService implements IDialogService {
if (idx > -1) {
this.dlgs.splice(idx, 1);
}
if (dlgs.length === 0) {
this.p.window.removeEventListener('keydown', this);
}
}

/** @internal */
public handleEvent(e: Event): void {
const keyEvent = e as KeyboardEvent;
const key = getActionKey(keyEvent);
if (key == null) {
return;
}
const top = this.top;
if (top === null || top.settings.keyboard.length === 0) {
return;
}
const keyboard = top.settings.keyboard;
if (key === 'Escape' && keyboard.includes(key)) {
void top.cancel();
} else if (key === 'Enter' && keyboard.includes(key)) {
void top.ok();
}
}
}

Expand Down Expand Up @@ -238,13 +212,3 @@ function asDialogOpenPromise(promise: Promise<unknown>): DialogOpenPromise {
(promise as DialogOpenPromise).whenClosed = whenClosed;
return promise as DialogOpenPromise;
}

function getActionKey(e: KeyboardEvent): DialogActionKey | undefined {
if ((e.code || e.key) === 'Escape' || e.keyCode === 27) {
return 'Escape';
}
if ((e.code || e.key) === 'Enter' || e.keyCode === 13) {
return 'Enter';
}
return undefined;
}
3 changes: 3 additions & 0 deletions packages/dialog/src/utilities-di.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ export const instanceRegistration = Registration.instance;

/** @internal */
export const callbackRegistration = Registration.callback;

/** @internal */
export const transientRegistration = Registration.transient;