libs/ui/drawer/src/lib/drawer/drawer.component.ts
A drawer that can overlay or push content.
<ts-drawer
[collapsedSize]="collapsedSize"
[expandedSize]="expandedSize"
[hideShadowWhenCollapsed]="true"
[isExpanded]="isExpanded"
[mode]="mode"
[position]="position"
[role]="role"
(expandedChange)="expandedChanged($event)"
(expandedStart)="expandedStarted($event)"
(collapsedStart)="collapsedStarted($event)"
(positionChange)="positionChanged($event)"
></ts-drawer>
<example-url>https://getterminus.github.io/ui-demos-release/components/drawer</example-url>
changeDetection | ChangeDetectionStrategy.OnPush |
encapsulation | ViewEncapsulation.None |
exportAs | tsDrawer |
host | { |
selector | ts-drawer |
styleUrls | ./drawer.component.scss |
templateUrl | ./drawer.component.html |
Properties |
|
Methods |
Inputs |
Outputs |
HostListeners |
Accessors |
constructor(elementRef: ElementRef
|
collapsedSize | |
Type : string
|
|
Collapsed drawer width |
expandedSize | |
Type : string
|
|
Expanded drawer width |
hideShadowWhenCollapsed | |
Type : boolean
|
|
Hide shadow when drawer is collapsed |
isExpanded | |
Type : boolean
|
|
Define whether the drawer is open |
mode | |
Mode of the drawer, overlay or push |
position | |
The side that the drawer is attached to. |
role | |
Default value : ''
|
|
Define the aria role label, default to nothing |
closed | |
Type : Observable<void>
|
|
Event emitted when the drawer has been collapsed. |
collapsedStart | |
Type : Observable<void>
|
|
Event emitted when the drawer has started collapsing. |
expandedChange | |
Type : EventEmitter
|
|
Event emitted when the drawer open state is changed. NOTE: This has to be async in order to avoid some issues with two-way bindings - setting isAsync to true. |
expandedStart | |
Type : Observable<void>
|
|
Event emitted when the drawer has started expanding. |
isExpanded | |
Type : Observable<void>
|
|
Event emitted when the drawer has been expanded. |
positionChanged | |
Type : EventEmitter
|
|
Event emitted when the drawer's position changes. |
@transform.done |
Arguments : '$event'
|
@transform.done(event: AnimationEvent)
|
We have to use a |
@transform.start |
Arguments : '$event'
|
@transform.start(event: AnimationEvent)
|
We have to use a |
Public collapse |
collapse()
|
Collapse the drawer.
Returns :
Promise<TsDrawerToggleResult>
Promise |
Public expand |
expand()
|
Expand the drawer.
Returns :
Promise<TsDrawerToggleResult>
Promise |
Public toggle | ||||||||
toggle(isOpen)
|
||||||||
Toggle this drawer.
Parameters :
Returns :
Promise<TsDrawerToggleResult>
Promise |
Public _collapsedSize |
Type : string
|
Default value : '3.75rem'
|
Public _expandedSize |
Type : string
|
Default value : '12.75rem'
|
Public animationEnd |
Default value : new Subject<AnimationEvent>()
|
Emits whenever the drawer is done animating. |
Public animationStarted |
Default value : new Subject<AnimationEvent>()
|
Emits whenever the drawer has started animating. |
Public animationState |
Type : "open-instant" | "open" | "void" | "void-shadow"
|
Default value : this.hideShadowWhenCollapsed ? 'void' : 'void-shadow'
|
Define animation state, defaults to void state |
Public elementRef |
Type : ElementRef<HTMLElement>
|
Public renderer |
Type : Renderer2
|
collapsedSize | ||||||
getcollapsedSize()
|
||||||
setcollapsedSize(value: string)
|
||||||
Collapsed drawer width
Parameters :
Returns :
void
|
expandedSize | ||||||
getexpandedSize()
|
||||||
setexpandedSize(value: string)
|
||||||
Expanded drawer width
Parameters :
Returns :
void
|
hideShadowWhenCollapsed | ||||||
gethideShadowWhenCollapsed()
|
||||||
sethideShadowWhenCollapsed(value: boolean)
|
||||||
Hide shadow when drawer is collapsed
Parameters :
Returns :
void
|
isExpanded | ||||||
getisExpanded()
|
||||||
setisExpanded(value: boolean)
|
||||||
Define whether the drawer is open
Parameters :
Returns :
void
|
mode | ||||
getmode()
|
||||
setmode(value)
|
||||
Mode of the drawer, overlay or push
Parameters :
Returns :
void
|
position | ||||
getposition()
|
||||
setposition(value)
|
||||
The side that the drawer is attached to.
Parameters :
Returns :
void
|
import { AnimationEvent } from '@angular/animations';
import { hasModifierKey } from '@angular/cdk/keycodes';
import { Platform } from '@angular/cdk/platform';
import {
AfterContentChecked,
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
HostListener,
Input,
NgZone,
OnDestroy,
Output,
Renderer2,
ViewEncapsulation,
} from '@angular/core';
import {
fromEvent,
Observable,
Subject,
} from 'rxjs';
import {
distinctUntilChanged,
filter,
map,
take,
} from 'rxjs/operators';
import {
isUnset,
KEYS,
untilComponentDestroyed,
} from '@terminus/fe-utilities';
import { tsDrawerAnimations } from './drawer-animations';
/**
* Result of the toggle promise that indicates the state of the drawer.
*/
export type TsDrawerToggleResult = 'open' | 'close';
/**
* Type of drawer display mode
*/
export type TsDrawerModes = 'overlay' | 'push';
/**
* Type of drawer position
*/
export type TsDrawerPosition = 'start' | 'end';
export const TS_DRAWER_DEFAULT_COLLAPSE_SIZE = '3.75rem';
export const TS_DRAWER_DEFAULT_EXPAND_SIZE = '12.5rem';
/**
* A drawer that can overlay or push content.
*
* @example
* <ts-drawer
* [collapsedSize]="collapsedSize"
* [expandedSize]="expandedSize"
* [hideShadowWhenCollapsed]="true"
* [isExpanded]="isExpanded"
* [mode]="mode"
* [position]="position"
* [role]="role"
* (expandedChange)="expandedChanged($event)"
* (expandedStart)="expandedStarted($event)"
* (collapsedStart)="collapsedStarted($event)"
* (positionChange)="positionChanged($event)"
* ></ts-drawer>
*
* <example-url>https://getterminus.github.io/ui-demos-release/components/drawer</example-url>
*/
@Component({
selector: 'ts-drawer',
templateUrl: './drawer.component.html',
styleUrls: ['./drawer.component.scss'],
animations: [tsDrawerAnimations.transformDrawer],
host: {
'class': 'ts-drawer',
// set align to null is to prevent the browser from aligning text based on value
'[attr.align]': 'null',
'[attr.role]': 'role',
'[class.ts-drawer--end]': 'position === "end"',
'[class.ts-drawer--overlay]': 'mode === "overlay"',
'[class.ts-drawer--push]': 'mode === "push"',
'tabIndex': '-1',
'[@transform]': `{
value: animationState,
params: {
collapsedSize: collapsedSize,
expandedSize: expandedSize
}
}`,
},
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
exportAs: 'tsDrawer',
})
export class TsDrawerComponent implements AfterContentChecked, OnDestroy {
/**
* Define animation state, defaults to void state
*/
public animationState: 'open-instant' | 'open' | 'void' | 'void-shadow' = this.hideShadowWhenCollapsed ? 'void' : 'void-shadow';
/**
* Emits whenever the drawer has started animating.
*/
public animationStarted = new Subject<AnimationEvent>();
/**
* Emits whenever the drawer is done animating.
*/
public animationEnd = new Subject<AnimationEvent>();
/**
* Emits when the component is destroyed.
*/
private readonly destroyed = new Subject<void>();
/**
* Whether the drawer is initialized. Used for disabling the initial animation.
*/
private enableAnimations = false;
/**
* An observable that emits when the drawer mode changes. This is used by the drawer container to
* to know when the mode changes so it can adapt the margins on the content.
*/
public readonly modeChanged = new Subject();
/**
* Collapsed drawer width
*
* @param value
*/
@Input()
public set collapsedSize(value: string) {
this._collapsedSize = isUnset(value) ? TS_DRAWER_DEFAULT_COLLAPSE_SIZE : value;
}
public get collapsedSize(): string {
return this._collapsedSize;
}
public _collapsedSize = '3.75rem';
/**
* Expanded drawer width
*
* @param value
*/
@Input()
public set expandedSize(value: string) {
this._expandedSize = isUnset(value) ? TS_DRAWER_DEFAULT_EXPAND_SIZE : value;
}
public get expandedSize(): string {
return this._expandedSize;
}
public _expandedSize = '12.75rem';
/**
* Hide shadow when drawer is collapsed
*
* @param value
*/
@Input()
public set hideShadowWhenCollapsed(value: boolean) {
this._hideShadowWhenCollapsed = value;
}
public get hideShadowWhenCollapsed(): boolean {
return this._hideShadowWhenCollapsed;
}
private _hideShadowWhenCollapsed = true;
/**
* Define whether the drawer is open
*
* @param value
*/
@Input()
public set isExpanded(value: boolean) {
this.toggle(value);
}
public get isExpanded(): boolean {
return this._isExpanded;
}
private _isExpanded = false;
/**
* Mode of the drawer, overlay or push
*
* @param value
*/
@Input()
public set mode(value: TsDrawerModes) {
this._mode = value;
this.modeChanged.next();
}
public get mode(): TsDrawerModes {
return this._mode;
}
private _mode: TsDrawerModes = 'overlay';
/**
* The side that the drawer is attached to.
*
* @param value
*/
@Input()
public set position(value: TsDrawerPosition) {
// Make sure we have a valid value.
value = value === 'end' ? 'end' : 'start';
if (value !== this._position) {
this._position = value;
this.positionChanged.emit();
}
}
public get position(): TsDrawerPosition {
return this._position;
}
private _position: TsDrawerPosition = 'start';
/**
* Define the aria role label, default to nothing
*/
@Input()
public role = '';
/**
* Event emitted when the drawer open state is changed.
*
* NOTE: This has to be async in order to avoid some issues with two-way bindings - setting isAsync to true.
*/
@Output()
public readonly expandedChange = new EventEmitter<boolean>(true);
/**
* Event emitted when the drawer has been expanded.
*/
@Output('isExpanded')
public get expandedStream(): Observable<void> {
return this.expandedChange.pipe(filter(o => o), map(() => {}));
}
/**
* Event emitted when the drawer has started expanding.
*/
@Output()
public get expandedStart(): Observable<void> {
return this.animationStarted.pipe(
filter(e => e.fromState !== e.toState && e.toState.indexOf('open') === 0),
untilComponentDestroyed(this),
map(() => {}),
);
}
/**
* Event emitted when the drawer has been collapsed.
*/
@Output('closed')
public get collapsedStream(): Observable<void> {
return this.expandedChange.pipe(filter(o => !o), map(() => {}));
}
/**
* Event emitted when the drawer has started collapsing.
*/
@Output()
public get collapsedStart(): Observable<void> {
return this.animationStarted.pipe(
filter(e => e.fromState !== e.toState && e.toState === 'void'),
untilComponentDestroyed(this),
map(() => {}),
);
}
/**
* Event emitted when the drawer's position changes.
*/
// eslint-disable-next-line @angular-eslint/no-output-rename
@Output('positionChanged')
public readonly positionChanged = new EventEmitter<void>();
constructor(
public elementRef: ElementRef<HTMLElement>,
private platform: Platform,
private ngZone: NgZone,
public renderer: Renderer2,
) {
/**
* Listen to `keydown` events outside the zone so that change detection is not run every
* time a key is pressed. Re-enter the zone only if the `ESC` key is pressed
*/
this.ngZone.runOutsideAngular(() => {
// TODO: Refactor deprecation
// eslint-disable-next-line deprecation/deprecation
(fromEvent(this.elementRef.nativeElement, 'keydown') as Observable<KeyboardEvent>)
.pipe(
filter(event => event.code === KEYS.ESCAPE.code && !hasModifierKey(event)),
untilComponentDestroyed(this),
).subscribe(event => this.ngZone.run(() => {
this.collapse();
event.stopPropagation();
event.preventDefault();
}));
});
// We need a Subject with distinctUntilChanged, because the `done` event fires twice on some browsers.
this.animationEnd.pipe(
distinctUntilChanged((x, y) => x.fromState === y.fromState && x.toState === y.toState),
untilComponentDestroyed(this),
).subscribe((event: AnimationEvent) => {
const { fromState, toState } = event;
if ((
toState.indexOf('open') === 0 && (fromState === 'void' || fromState === 'void-shadow'))
|| (toState === 'void' && fromState.indexOf('open') === 0)
|| (toState === 'void-shadow' && fromState.indexOf('open') === 0)) {
this.expandedChange.emit(this.isExpanded);
}
});
this.renderer.setStyle(this.elementRef.nativeElement, 'width', this.expandedSize);
}
/**
* Enable the animations after the lifecycle hooks have run, in order to avoid animating drawers that are open by default.
*/
public ngAfterContentChecked(): void {
if (this.platform.isBrowser) {
this.enableAnimations = true;
}
}
/**
* Complete the observable on destroy
*/
public ngOnDestroy(): void {
this.modeChanged.complete();
this.destroyed.next();
this.destroyed.complete();
}
/**
* Expand the drawer.
*
* @returns Promise<TsDrawerToggleResult>
*/
public expand(): Promise<TsDrawerToggleResult> {
return this.toggle(true);
}
/**
* Collapse the drawer.
*
* @returns Promise<TsDrawerToggleResult>
*/
public collapse(): Promise<TsDrawerToggleResult> {
return this.toggle(false);
}
/**
* Toggle this drawer.
*
* @param isOpen - whether the drawer should be open.
* @returns Promise<TsDrawerToggleResult>
*/
public toggle(isOpen = !this.isExpanded): Promise<TsDrawerToggleResult> {
this._isExpanded = isOpen;
if (isOpen) {
this.animationState = this.enableAnimations ? 'open' : 'open-instant';
} else {
this.animationState = this.hideShadowWhenCollapsed ? 'void' : 'void-shadow';
}
return new Promise<TsDrawerToggleResult>(resolve => {
this.expandedChange.pipe(take(1)).subscribe(open => resolve(open ? 'open' : 'close'));
});
}
/**
* We have to use a `HostListener` here in order to support both Ivy and ViewEngine.
* In Ivy the `host` bindings will be merged when this class is extended, whereas in
* ViewEngine they're overwritten.
* TODO: we move this back into `host` once Ivy is turned on by default.
*
* @param event
*/
@HostListener('@transform.start', ['$event'])
public animationStartListener(event: AnimationEvent) {
this.animationStarted.next(event);
}
/**
* We have to use a `HostListener` here in order to support both Ivy and ViewEngine.
* In Ivy the `host` bindings will be merged when this class is extended, whereas in
* ViewEngine they're overwritten.
* TODO: move this back into `host` once Ivy is turned on by default.
*
* @param event
*/
@HostListener('@transform.done', ['$event'])
public animationDoneListener(event: AnimationEvent) {
this.animationEnd.next(event);
}
}