File

src/dialog/dialog.directive.ts

Description

A generic directive that can be inherited from to create dialogs (for example, a tooltip or popover)

This class contains the relevant initialization code, specific templates, options, and additional inputs should be specified in the derived class.

NOTE: All child classes should add DialogService as a provider, otherwise they will lose context that the service relies on.

Implements

OnInit OnDestroy OnChanges

Metadata

Index

Properties
Methods
Inputs
Outputs
HostBindings
Accessors

Constructor

constructor(elementRef: ElementRef, viewContainerRef: ViewContainerRef, dialogService: DialogService, eventService: EventService)

Creates an instance of DialogDirective.

Parameters :
Name Type Optional
elementRef ElementRef No
viewContainerRef ViewContainerRef No
dialogService DialogService No
eventService EventService No

Inputs

appendInline
Type : boolean
Default value : false

Set to true to open the dialog next to the triggering component

cdsDialog
Type : string | TemplateRef<any>
closeTrigger
Type : "mouseout" | "mouseleave"
Default value : "mouseleave"

Defines how the Dialog close event is triggered.

See here for more on the difference between mouseleave and mouseout.

Defaults to click when trigger is set to click.

data
Type : {}
Default value : {}

Optional data for templates

disabled
Type : boolean
Default value : false

This prevents the dialog from being toggled

gap
Type : number
Default value : 0

Spacing between the dialog and it's triggering element

ibmDialog
Type : string | TemplateRef
isOpen
Type : boolean
Default value : false
offset
Type : literal type

This specifies any vertical and horizontal offset for the position of the dialog

placement
Type : string
Default value : "left"

Placement of the dialog, usually relative to the element the directive is on.

shouldClose
Type : function

This input allows explicit control over how the dialog should close

title
Type : string
Default value : ""

Title for the dialog

trigger
Type : "click" | "hover" | "mouseenter"
Default value : "click"

Defines how the Dialog is triggered.(Hover and click behave the same on mobile - both respond to a single tap). Do not add focusable elements if trigger is hover or mouseenter.

wrapperClass
Type : string

Classes to add to the dialog container

Outputs

isOpenChange
Type : EventEmitter

Emits an event when the state of isOpen changes. Allows isOpen to be double bound

onClose
Type : EventEmitter<any>

Emits an event when the dialog is closed

onOpen
Type : EventEmitter<any>

Emits an event when the dialog is opened

HostBindings

attr.aria-haspopup
Type : boolean
Default value : true
attr.aria-owns
Type : string
attr.role
Type : string
Default value : "button"

Methods

close
close(meta: CloseMeta)

Helper method to close the dialogRef.

Parameters :
Name Type Optional Default value
meta CloseMeta No { reason: CloseReasons.interaction }
Returns : void
ngOnChanges
ngOnChanges(changes: SimpleChanges)
Parameters :
Name Type Optional
changes SimpleChanges No
Returns : void
ngOnDestroy
ngOnDestroy()

When the host dies, kill the popover.

  • Useful for use in a modal or similar.
Returns : void
ngOnInit
ngOnInit()

Sets the config object and binds events for hovering or clicking before running code from child class.

Returns : void
Protected onDialogChanges
onDialogChanges(_changes: SimpleChanges)

Empty method for child to override and specify additional on changes steps. run after DialogDirective completes it's ngOnChanges.

Parameters :
Name Type Optional
_changes SimpleChanges No
Returns : void
Protected onDialogInit
onDialogInit()

Empty method for child classes to override and specify additional init steps. Run after DialogDirective completes it's ngOnInit.

Returns : void
open
open(component?)

Helper method to call dialogService 'open'.

  • Enforce accessibility by updating an aria attr for nativeElement.
Parameters :
Name Optional
component Yes
toggle
toggle(meta: CloseMeta)

Helper method to toggle the open state of the dialog

Parameters :
Name Type Optional Default value
meta CloseMeta No { reason: CloseReasons.interaction }
Returns : void
Protected updateConfig
updateConfig()
Returns : void

Properties

dialogConfig
Type : DialogConfig

Config object passed to the rendered component

Static dialogCounter
Type : number
Default value : 0
Protected dialogRef
Type : ComponentRef<Dialog>

Keeps a reference to the currently opened dialog

hasPopup
Default value : true
Decorators :
@HostBinding('attr.aria-haspopup')
role
Type : string
Default value : "button"
Decorators :
@HostBinding('attr.role')
Private subscriptions
Type : Subscription[]
Default value : []

Accessors

ibmDialog
setibmDialog(body: string | TemplateRef)

Dialog body content.

Parameters :
Name Type Optional
body string | TemplateRef<any> No
Returns : void
ariaOwns
getariaOwns()
import {
	Directive,
	Input,
	Output,
	EventEmitter,
	OnInit,
	OnDestroy,
	ElementRef,
	TemplateRef,
	ViewContainerRef,
	OnChanges,
	HostBinding,
	SimpleChanges,
	ComponentRef
} from "@angular/core";
import { DialogService } from "./dialog.service";
import { CloseMeta, CloseReasons, DialogConfig } from "./dialog-config.interface";
import { EventService } from "carbon-components-angular/utils";
import { Dialog } from "./dialog.component";
import { fromEvent, Subscription } from "rxjs";

/**
 * A generic directive that can be inherited from to create dialogs (for example, a tooltip or popover)
 *
 * This class contains the relevant initialization code, specific templates, options, and additional inputs
 * should be specified in the derived class.
 *
 * NOTE: All child classes should add `DialogService` as a provider, otherwise they will lose context that
 * the service relies on.
 */
@Directive({
	selector: "[cdsDialog], [ibmDialog]",
	exportAs: "dialog",
	providers: [
		DialogService
	]
})
export class DialogDirective implements OnInit, OnDestroy, OnChanges {
	static dialogCounter = 0;
	/**
	 * Title for the dialog
	 */
	@Input() title = "";
	/**
	 * @deprecated as of v5, use `cdsDialog` instead
	 * Dialog body content.
	 */
	@Input() set ibmDialog(body: string | TemplateRef<any>) {
		this.cdsDialog = body;
	}

	@Input() cdsDialog: string | TemplateRef<any>;
	/**
	 * Defines how the Dialog is triggered.(Hover and click behave the same on mobile - both respond to a single tap).
	 * Do not add focusable elements if trigger is `hover` or `mouseenter`.
	 */
	@Input() trigger: "click" | "hover" | "mouseenter" = "click";
	/**
	 * Defines how the Dialog close event is triggered.
	 *
	 * [See here](https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseleave_event)
	 * for more on the difference between `mouseleave` and `mouseout`.
	 *
	 * Defaults to `click` when `trigger` is set to `click`.
	 */
	@Input() closeTrigger: "mouseout" | "mouseleave" = "mouseleave";
	/**
	 * Placement of the dialog, usually relative to the element the directive is on.
	 */
	@Input() placement = "left";
	/**
	 * This specifies any vertical and horizontal offset for the position of the dialog
	 */
	@Input() offset: { x: number, y: number };
	/**
	 * Classes to add to the dialog container
	 */
	@Input() wrapperClass: string;
	/**
	 * Spacing between the dialog and it's triggering element
	 */
	@Input() gap = 0;
	/**
	 * Set to `true` to open the dialog next to the triggering component
	 */
	@Input() appendInline = false;
	/**
	 * Optional data for templates
	 */
	@Input() data = {};

	@Input() @HostBinding("attr.aria-expanded") isOpen = false;
	/**
	 * This prevents the dialog from being toggled
	 */
	@Input() disabled = false;
	/**
	 * This input allows explicit control over how the dialog should close
	 */
	@Input() shouldClose: (meta: CloseMeta) => boolean;
	/**
	 * Config object passed to the rendered component
	 */
	dialogConfig: DialogConfig;
	/**
	 * Emits an event when the dialog is closed
	 */
	@Output() onClose: EventEmitter<any> = new EventEmitter();
	/**
	 * Emits an event when the dialog is opened
	 */
	@Output() onOpen: EventEmitter<any> = new EventEmitter();
	/**
	 * Emits an event when the state of `isOpen` changes. Allows `isOpen` to be double bound
	 */
	@Output() isOpenChange = new EventEmitter<boolean>();

	@HostBinding("attr.role") role = "button";
	@HostBinding("attr.aria-haspopup") hasPopup = true;
	@HostBinding("attr.aria-owns") get ariaOwns(): string {
		return this.isOpen ? this.dialogConfig.compID : null;
	}

	/**
	 * Keeps a reference to the currently opened dialog
	 */
	protected dialogRef: ComponentRef<Dialog>;

	private subscriptions: Subscription[] = [];

	/**
	 * Creates an instance of DialogDirective.
	 * @param elementRef
	 * @param viewContainerRef
	 * @param dialogService
	 * @param eventService
	 */
	constructor(
		protected elementRef: ElementRef,
		protected viewContainerRef: ViewContainerRef,
		protected dialogService: DialogService,
		/**
		 * Deprecated as of v5
		 */
		protected eventService: EventService
	) {}

	ngOnChanges(changes: SimpleChanges) {
		// set the config object (this can [and should!] be added to in child classes depending on what they need)
		this.dialogConfig = {
			title: this.title,
			content: this.cdsDialog,
			placement: this.placement,
			parentRef: this.elementRef,
			gap: this.gap,
			trigger: this.trigger,
			closeTrigger: this.closeTrigger,
			shouldClose: this.shouldClose || (() => true),
			appendInline: this.appendInline,
			wrapperClass: this.wrapperClass,
			data: this.data,
			offset: this.offset,
			disabled: this.disabled
		};

		if (changes.isOpen) {
			if (changes.isOpen.currentValue) {
				this.open();
			} else if (!changes.isOpen.firstChange) {
				this.close({
					reason: CloseReasons.programmatic
				});
			}
		}

		// Run any code a child class may need.
		this.onDialogChanges(changes);
		this.updateConfig();
	}

	/**
	 * Sets the config object and binds events for hovering or clicking before
	 * running code from child class.
	 */
	ngOnInit() {
		// fix for safari hijacking clicks
		this.dialogService.singletonClickListen();

		const element: HTMLElement = this.elementRef.nativeElement;

		this.subscriptions.push(
			fromEvent(element, "keydown").subscribe((event: KeyboardEvent) => {
				if (event.target === this.dialogConfig.parentRef.nativeElement &&
					(event.key === "Tab" || event.key === "Tab" && event.shiftKey) ||
					event.key === "Escape") {
					this.close({
						reason: CloseReasons.interaction,
						target: event.target
					});
				}
			})
		);

		// bind events for hovering or clicking the host
		if (this.trigger === "hover" || this.trigger === "mouseenter") {
			this.subscriptions.push(
				fromEvent(element, "mouseenter").subscribe(() => this.open()),
				fromEvent(element, this.closeTrigger).subscribe((event) => {
					this.close({
						reason: CloseReasons.interaction,
						target: event.target
					});
				}),
				fromEvent(element, "focus").subscribe(() => this.open()),
				fromEvent(element, "blur").subscribe((event) => {
					this.close({
						reason: CloseReasons.interaction,
						target: event.target
					});
				})
			);
		} else {
			this.subscriptions.push(
				fromEvent(element, "click").subscribe((event) => {
					this.toggle({
						reason: CloseReasons.interaction,
						target: event.target
					});
				}),
				fromEvent(element, "keydown").subscribe((event: KeyboardEvent) => {
					if (event.key === "Enter" || event.key === " ") {
						setTimeout(() => {
							this.open();
						});
					}
				})
			);
		}

		DialogDirective.dialogCounter++;
		this.dialogConfig.compID = "dialog-" + DialogDirective.dialogCounter;

		// run any code a child class may need
		this.onDialogInit();
		this.updateConfig();
	}

	/**
	 * When the host dies, kill the popover.
	 * - Useful for use in a modal or similar.
	 */
	ngOnDestroy() {
		this.close({
			reason: CloseReasons.destroyed
		});
		this.subscriptions.forEach((subscription) => subscription.unsubscribe());
	}

	/**
	 * Helper method to call dialogService 'open'.
	 * - Enforce accessibility by updating an aria attr for nativeElement.
	 */
	open(component?) {
		// don't allow dialogs to be opened if they're already open
		if (this.dialogRef || this.disabled) { return; }

		// actually open the dialog, emit events, and set the open state
		this.dialogRef = this.dialogService.open(this.viewContainerRef, this.dialogConfig, component);
		this.isOpen = true;
		this.onOpen.emit();
		this.isOpenChange.emit(true);

		// Handles emitting all the close events to clean everything up
		// Also enforce accessibility on close by updating an aria attr on the nativeElement.
		const subscription = this.dialogRef.instance.close.subscribe((meta: CloseMeta) => {
			if (!this.dialogRef) { return; }
			if (this.dialogConfig.shouldClose && this.dialogConfig.shouldClose(meta)) {
				// close the dialog, emit events, and clear out the open states
				this.dialogService.close(this.dialogRef);
				this.dialogRef = null;
				this.isOpen = false;
				this.onClose.emit();
				this.isOpenChange.emit(false);
				subscription.unsubscribe();
			}
		});

		return this.dialogRef;
	}

	/**
	 * Helper method to toggle the open state of the dialog
	 */
	toggle(meta: CloseMeta = { reason: CloseReasons.interaction }) {
		if (!this.isOpen) {
			this.open();
		} else {
			this.close(meta);
		}
	}

	/**
	 * Helper method to close the dialogRef.
	 */
	close(meta: CloseMeta = { reason: CloseReasons.interaction }) {
		if (this.dialogRef) {
			this.dialogRef.instance.doClose(meta);
		}
	}

	/**
	 * Empty method for child classes to override and specify additional init steps.
	 * Run after DialogDirective completes it's ngOnInit.
	 */
	protected onDialogInit() {}

	/**
	 * Empty method for child to override and specify additional on changes steps.
	 * run after DialogDirective completes it's ngOnChanges.
	 */
	protected onDialogChanges(_changes: SimpleChanges) {}

	protected updateConfig() {}
}

results matching ""

    No results matching ""