Skip to content

Commit

Permalink
feat(design): create DaffModalCloseDirective (#2832)
Browse files Browse the repository at this point in the history
This adds a close button to the DaffModalCHeaderComponent
and allows the close button to be reusable
  • Loading branch information
xelaint authored and damienwebdev committed Jul 23, 2024
1 parent fd838fd commit 3140fee
Show file tree
Hide file tree
Showing 14 changed files with 212 additions and 27 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {
ChangeDetectionStrategy,
Component,
HostBinding,
} from '@angular/core';

@Component({
selector: 'daff-modal-actions',
template: '<ng-content></ng-content>',
styleUrls: ['./modal-actions.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DaffModalActionsComponent { }
export class DaffModalActionsComponent {
@HostBinding('class.daff-modal-actions') class = true;
}
54 changes: 54 additions & 0 deletions libs/design/modal/src/modal-close/modal-close.directive.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
Component,
DebugElement,
} from '@angular/core';
import {
waitForAsync,
ComponentFixture,
TestBed,
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';

import { DaffModalCloseDirective } from './modal-close.directive';
import { DaffModalService } from '../service/modal.service';

@Component({
template: `
<button daffModalClose></button>
`,
})
class WrapperComponent {}

describe('@daffodil/design/modal | DaffModalCloseDirective', () => {
let wrapper: WrapperComponent;
let de: DebugElement;
let fixture: ComponentFixture<WrapperComponent>;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [
DaffModalCloseDirective,
WrapperComponent,
],
providers: [
DaffModalService,
],
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(WrapperComponent);
wrapper = fixture.componentInstance;
de = fixture.debugElement.query(By.css('button[daffModalClose]'));
fixture.detectChanges();
});

it('should create', () => {
expect(wrapper).toBeTruthy();
});

it('should set the type to button', () => {
expect(de.attributes['type']).toBe('button');
});
});
41 changes: 41 additions & 0 deletions libs/design/modal/src/modal-close/modal-close.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
Directive,
HostBinding,
HostListener,
Optional,
} from '@angular/core';

import { DaffModalComponent } from '../modal/modal.component';
import { DaffModalService } from '../service/modal.service';

/**
* The DaffModalCloseDirective is a helper directive that passes a click
* event to the button it's used with to close a modal. It needs to be
* implemented with the `<button>` HTML element to work. This helps to
* reduce code duplication.
*/
@Directive({
selector: 'button[daffModalClose]',
})

export class DaffModalCloseDirective {
constructor(
private modalService: DaffModalService,
@Optional() private modal: DaffModalComponent,
) {}

/**
* Event fired when the button the directive is attached to is clicked. This is used to close a modal.
*/
@HostListener('click')
_onCloseModal(event: MouseEvent) {
if(this.modal) {
this.modalService.close(this.modal);
}
}

/**
* Sets the button type attribute to button.
*/
@HostBinding('attr.type') typeAttribute = 'button';
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {
Component,
ChangeDetectionStrategy,
HostBinding,
} from '@angular/core';

@Component({
selector: 'daff-modal-content',
template: '<ng-content></ng-content>',
styleUrls: ['./modal-content.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DaffModalContentComponent {}
export class DaffModalContentComponent {
@HostBinding('class.daff-modal-content') class = true;
}
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
<ng-content select="[daffModalTitle]"></ng-content>
<ng-container *ngIf="dismissible">
<button daff-icon-button color="theme-contrast"
daffModalClose
class="daff-modal-header__dismiss-button"
aria-label="Close modal">
<fa-icon [icon]="faXmark" size="lg"></fa-icon>
</button>
</ng-container>
12 changes: 10 additions & 2 deletions libs/design/modal/src/modal-header/modal-header.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import { By } from '@angular/platform-browser';
import { DaffModalHeaderComponent } from './modal-header.component';

@Component ({
template: `<daff-modal-header></daff-modal-header>`,
template: `<daff-modal-header [dismissible]="dismissible"></daff-modal-header>`,
})

class WrapperComponent {}
class WrapperComponent {
dismissible = true;
}

describe('@daffodil/design/modal | DaffModalHeaderComponent', () => {
let fixture: ComponentFixture<WrapperComponent>;
Expand Down Expand Up @@ -52,4 +54,10 @@ describe('@daffodil/design/modal | DaffModalHeaderComponent', () => {
}));
});
});

describe('dismissible property', () => {
it('should be set to true by default', () => {
expect(component.dismissible).toBe(true);
});
});
});
9 changes: 9 additions & 0 deletions libs/design/modal/src/modal-header/modal-header.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import {
ViewEncapsulation,
HostBinding,
ChangeDetectionStrategy,
Input,
} from '@angular/core';
import { faXmark } from '@fortawesome/free-solid-svg-icons';

@Component({
selector: 'daff-modal-header',
Expand All @@ -12,8 +14,15 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DaffModalHeaderComponent {
faXmark = faXmark;

/**
* @docs-private
*/
@HostBinding('class.daff-modal-header') class = true;

/**
* Whether or not a modal is dismissible.
*/
@Input() dismissible = true;
}
8 changes: 8 additions & 0 deletions libs/design/modal/src/modal.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import { OverlayModule } from '@angular/cdk/overlay';
import { PortalModule } from '@angular/cdk/portal';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';

import { DaffButtonModule } from '@daffodil/design/button';

import { DaffModalComponent } from './modal/modal.component';
import { DaffModalActionsComponent } from './modal-actions/modal-actions.component';
import { DaffModalCloseDirective } from './modal-close/modal-close.directive';
import { DaffModalContentComponent } from './modal-content/modal-content.component';
import { DaffModalHeaderComponent } from './modal-header/modal-header.component';
import { DaffModalTitleDirective } from './modal-title/modal-title.directive';
Expand All @@ -15,19 +19,23 @@ import { DaffModalService } from './service/modal.service';
CommonModule,
PortalModule,
OverlayModule,
DaffButtonModule,
FontAwesomeModule,
],
exports: [
DaffModalHeaderComponent,
DaffModalTitleDirective,
DaffModalContentComponent,
DaffModalActionsComponent,
DaffModalCloseDirective,
],
declarations: [
DaffModalComponent,
DaffModalHeaderComponent,
DaffModalTitleDirective,
DaffModalContentComponent,
DaffModalActionsComponent,
DaffModalCloseDirective,
],
providers: [
DaffModalService,
Expand Down
64 changes: 58 additions & 6 deletions libs/design/modal/src/modal/modal.component.scss
Original file line number Diff line number Diff line change
@@ -1,15 +1,67 @@
@use '../../../scss/typography' as t;
@use '../../../scss/layout';

:host {
display: block;
.daff-modal {
display: flex;
flex-direction: column;
justify-content: space-between;
border-radius: 4px;
height: 100%;
max-width: 80vw;
max-height: 90vh;
max-width: 90vw;
overflow: hidden;
padding: 24px;
z-index: 3;

@include layout.breakpoint(mobile) {
height: auto;
max-height: 80vh;
max-width: 80vw;
}
}

.daff-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px;
position: relative;

.daff-modal-title {
@include t.text-truncate();
font-size: 1.25rem;
font-weight: 500;
line-height: 1.5rem;
margin: 0;
padding: 0 32px 0 0;
}

&__dismiss-button {
position: absolute;
right: 7px;
top: 12px;
}
}


.daff-modal-content {
display: block;
flex-grow: 1;
max-height: 60vh;
overflow-y: auto;
padding: 24px;
}

.daff-modal-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
padding: 24px;
}

.daff-modal-content + .daff-modal-actions {
padding: 0 24px 24px;
}

.daff-modal-header + .daff-modal-content {
padding-top: 0;
}
16 changes: 13 additions & 3 deletions libs/design/modal/src/modal/modal.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
ElementRef,
AfterContentInit,
AfterViewInit,
Self,
ViewEncapsulation,
} from '@angular/core';

import { daffFocusableElementsSelector } from '@daffodil/design';
Expand All @@ -33,10 +33,11 @@ import { DaffModalService } from '../service/modal.service';
styleUrls: ['./modal.component.scss'],
animations: [daffFadeAnimations.fade],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class DaffModalComponent implements AfterContentInit, AfterViewInit {
/**
* HostBinding to set the default modal class on the host element.
* Sets a class of .daff-modal to the host element.
*/
@HostBinding('class.daff-modal') modalClass = true;

Expand Down Expand Up @@ -127,7 +128,16 @@ export class DaffModalComponent implements AfterContentInit, AfterViewInit {
* Helper method to attach portable content to modal.
*/
attachContent(portal: ComponentPortal<any>): any {
this._portalOutlet.attachComponentPortal(portal);
const attachContent = this._portalOutlet.attachComponentPortal(portal);

// When a component is created to inject content into the modal, it can
// interfere with the display styles applied to the modal's header, content,
// and action sections. By setting `display: contents;` on the custom
// component, it is visually removed from the UI, allowing the content
// within it to inherit the modal's styles.
attachContent.location.nativeElement.style.display = 'contents';

return attachContent;
}

/** Animation hook that controls the entrance and exit animations of the modal. */
Expand Down
1 change: 1 addition & 0 deletions libs/design/modal/src/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { DaffModalHeaderComponent } from './modal-header/modal-header.component'
export { DaffModalTitleDirective } from './modal-title/modal-title.directive';
export { DaffModalContentComponent } from './modal-content/modal-content.component';
export { DaffModalActionsComponent } from './modal-actions/modal-actions.component';
export { DaffModalCloseDirective } from './modal-close/modal-close.directive';
3 changes: 2 additions & 1 deletion libs/design/modal/src/service/modal.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ export class DaffModalService {
return this.overlay.create({
hasBackdrop: true,
positionStrategy: new GlobalPositionStrategy()
.centerHorizontally(),
.centerHorizontally()
.centerVertically(),
scrollStrategy: this.overlay.scrollStrategies.block(),
});
}
Expand Down

0 comments on commit 3140fee

Please sign in to comment.