From 3f50a06ecfb28f3cb800a699e787d4805a57f7f8 Mon Sep 17 00:00:00 2001 From: xidedix Date: Sat, 15 Jun 2024 18:41:00 +0200 Subject: [PATCH] fix(modal): attempt to focus when there is no focusable element on modal dialog --- .../modal-body/modal-body.component.spec.ts | 7 ++- .../modal/modal-body/modal-body.component.ts | 5 +- .../modal-content.component.spec.ts | 7 ++- .../modal-content/modal-content.component.ts | 2 +- .../modal-dialog.component.spec.ts | 7 ++- .../modal-dialog/modal-dialog.component.ts | 2 +- .../modal-footer.component.spec.ts | 7 ++- .../modal-footer/modal-footer.component.ts | 6 +- .../modal-header.component.spec.ts | 7 ++- .../modal-header/modal-header.component.ts | 4 +- .../src/lib/modal/modal/modal.component.html | 2 +- .../lib/modal/modal/modal.component.spec.ts | 5 ++ .../src/lib/modal/modal/modal.component.ts | 55 +++++++++---------- 13 files changed, 64 insertions(+), 52 deletions(-) diff --git a/projects/coreui-angular/src/lib/modal/modal-body/modal-body.component.spec.ts b/projects/coreui-angular/src/lib/modal/modal-body/modal-body.component.spec.ts index 95292a84..32c03ed2 100644 --- a/projects/coreui-angular/src/lib/modal/modal-body/modal-body.component.spec.ts +++ b/projects/coreui-angular/src/lib/modal/modal-body/modal-body.component.spec.ts @@ -9,8 +9,7 @@ describe('ModalBodyComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ModalBodyComponent] - }) - .compileComponents(); + }).compileComponents(); }); beforeEach(() => { @@ -22,4 +21,8 @@ describe('ModalBodyComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have css classes', () => { + expect(fixture.nativeElement).toHaveClass('modal-body'); + }); }); diff --git a/projects/coreui-angular/src/lib/modal/modal-body/modal-body.component.ts b/projects/coreui-angular/src/lib/modal/modal-body/modal-body.component.ts index 3ebea103..cb0f4aaf 100644 --- a/projects/coreui-angular/src/lib/modal/modal-body/modal-body.component.ts +++ b/projects/coreui-angular/src/lib/modal/modal-body/modal-body.component.ts @@ -2,16 +2,15 @@ import { Component, HostBinding } from '@angular/core'; @Component({ selector: 'c-modal-body', - template: '', + template: '', styleUrls: ['./modal-body.component.scss'], standalone: true }) export class ModalBodyComponent { - @HostBinding('class') get hostClasses(): any { return { - 'modal-body': true, + 'modal-body': true }; } } diff --git a/projects/coreui-angular/src/lib/modal/modal-content/modal-content.component.spec.ts b/projects/coreui-angular/src/lib/modal/modal-content/modal-content.component.spec.ts index be7ec86d..ef006d95 100644 --- a/projects/coreui-angular/src/lib/modal/modal-content/modal-content.component.spec.ts +++ b/projects/coreui-angular/src/lib/modal/modal-content/modal-content.component.spec.ts @@ -9,8 +9,7 @@ describe('ModalContentComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ModalContentComponent] - }) - .compileComponents(); + }).compileComponents(); }); beforeEach(() => { @@ -22,4 +21,8 @@ describe('ModalContentComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have css classes', () => { + expect(fixture.nativeElement).toHaveClass('modal-content'); + }); }); diff --git a/projects/coreui-angular/src/lib/modal/modal-content/modal-content.component.ts b/projects/coreui-angular/src/lib/modal/modal-content/modal-content.component.ts index 1a6ac176..7e170bf5 100644 --- a/projects/coreui-angular/src/lib/modal/modal-content/modal-content.component.ts +++ b/projects/coreui-angular/src/lib/modal/modal-content/modal-content.component.ts @@ -2,7 +2,7 @@ import { Component, HostBinding } from '@angular/core'; @Component({ selector: 'c-modal-content', - template: '', + template: '', standalone: true }) export class ModalContentComponent { diff --git a/projects/coreui-angular/src/lib/modal/modal-dialog/modal-dialog.component.spec.ts b/projects/coreui-angular/src/lib/modal/modal-dialog/modal-dialog.component.spec.ts index d845369c..bf1d06d2 100644 --- a/projects/coreui-angular/src/lib/modal/modal-dialog/modal-dialog.component.spec.ts +++ b/projects/coreui-angular/src/lib/modal/modal-dialog/modal-dialog.component.spec.ts @@ -9,8 +9,7 @@ describe('ModalDialogComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ModalDialogComponent] - }) - .compileComponents(); + }).compileComponents(); }); beforeEach(() => { @@ -22,4 +21,8 @@ describe('ModalDialogComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have css classes', () => { + expect(fixture.nativeElement).toHaveClass('modal-dialog'); + }); }); diff --git a/projects/coreui-angular/src/lib/modal/modal-dialog/modal-dialog.component.ts b/projects/coreui-angular/src/lib/modal/modal-dialog/modal-dialog.component.ts index 750e036e..68f8d938 100644 --- a/projects/coreui-angular/src/lib/modal/modal-dialog/modal-dialog.component.ts +++ b/projects/coreui-angular/src/lib/modal/modal-dialog/modal-dialog.component.ts @@ -2,7 +2,7 @@ import { Component, HostBinding, Input } from '@angular/core'; @Component({ selector: 'c-modal-dialog', - template: '', + template: '', styleUrls: ['./modal-dialog.component.scss'], standalone: true }) diff --git a/projects/coreui-angular/src/lib/modal/modal-footer/modal-footer.component.spec.ts b/projects/coreui-angular/src/lib/modal/modal-footer/modal-footer.component.spec.ts index 8a8202d6..045091bb 100644 --- a/projects/coreui-angular/src/lib/modal/modal-footer/modal-footer.component.spec.ts +++ b/projects/coreui-angular/src/lib/modal/modal-footer/modal-footer.component.spec.ts @@ -9,8 +9,7 @@ describe('ModalFooterComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ModalFooterComponent] - }) - .compileComponents(); + }).compileComponents(); }); beforeEach(() => { @@ -22,4 +21,8 @@ describe('ModalFooterComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have css classes', () => { + expect(fixture.nativeElement).toHaveClass('modal-footer'); + }); }); diff --git a/projects/coreui-angular/src/lib/modal/modal-footer/modal-footer.component.ts b/projects/coreui-angular/src/lib/modal/modal-footer/modal-footer.component.ts index 82cb7600..8cebd49b 100644 --- a/projects/coreui-angular/src/lib/modal/modal-footer/modal-footer.component.ts +++ b/projects/coreui-angular/src/lib/modal/modal-footer/modal-footer.component.ts @@ -2,16 +2,14 @@ import { Component, HostBinding } from '@angular/core'; @Component({ selector: 'c-modal-footer', - template: '', + template: '', standalone: true }) export class ModalFooterComponent { - @HostBinding('class') get hostClasses(): any { return { - 'modal-footer': true, + 'modal-footer': true }; } - } diff --git a/projects/coreui-angular/src/lib/modal/modal-header/modal-header.component.spec.ts b/projects/coreui-angular/src/lib/modal/modal-header/modal-header.component.spec.ts index b6f7b0a5..6c4bbf41 100644 --- a/projects/coreui-angular/src/lib/modal/modal-header/modal-header.component.spec.ts +++ b/projects/coreui-angular/src/lib/modal/modal-header/modal-header.component.spec.ts @@ -9,8 +9,7 @@ describe('ModalHeaderComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ModalHeaderComponent] - }) - .compileComponents(); + }).compileComponents(); }); beforeEach(() => { @@ -22,4 +21,8 @@ describe('ModalHeaderComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have css classes', () => { + expect(fixture.nativeElement).toHaveClass('modal-header'); + }); }); diff --git a/projects/coreui-angular/src/lib/modal/modal-header/modal-header.component.ts b/projects/coreui-angular/src/lib/modal/modal-header/modal-header.component.ts index c3e1eca0..66f10340 100644 --- a/projects/coreui-angular/src/lib/modal/modal-header/modal-header.component.ts +++ b/projects/coreui-angular/src/lib/modal/modal-header/modal-header.component.ts @@ -2,16 +2,14 @@ import { Component, HostBinding } from '@angular/core'; @Component({ selector: 'c-modal-header', - template: ``, + template: '', standalone: true }) export class ModalHeaderComponent { - @HostBinding('class') get hostClasses(): any { return { 'modal-header': true }; } - } diff --git a/projects/coreui-angular/src/lib/modal/modal/modal.component.html b/projects/coreui-angular/src/lib/modal/modal/modal.component.html index a110c003..194fe867 100644 --- a/projects/coreui-angular/src/lib/modal/modal/modal.component.html +++ b/projects/coreui-angular/src/lib/modal/modal/modal.component.html @@ -5,7 +5,7 @@ [size]="size">
- +
diff --git a/projects/coreui-angular/src/lib/modal/modal/modal.component.spec.ts b/projects/coreui-angular/src/lib/modal/modal/modal.component.spec.ts index a98d893b..35f0b0f4 100644 --- a/projects/coreui-angular/src/lib/modal/modal/modal.component.spec.ts +++ b/projects/coreui-angular/src/lib/modal/modal/modal.component.spec.ts @@ -22,4 +22,9 @@ describe('ModalComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have css classes', () => { + expect(fixture.nativeElement).toHaveClass('modal'); + expect(fixture.nativeElement).toHaveClass('fade'); + }); }); diff --git a/projects/coreui-angular/src/lib/modal/modal/modal.component.ts b/projects/coreui-angular/src/lib/modal/modal/modal.component.ts index 106bacf8..892492e9 100644 --- a/projects/coreui-angular/src/lib/modal/modal/modal.component.ts +++ b/projects/coreui-angular/src/lib/modal/modal/modal.component.ts @@ -54,7 +54,6 @@ import { ModalDialogComponent } from '../modal-dialog/modal-dialog.component'; imports: [ModalDialogComponent, ModalContentComponent, A11yModule] }) export class ModalComponent implements OnInit, OnDestroy, AfterViewInit { - #destroyRef = inject(DestroyRef); #focusMonitor = inject(FocusMonitor); @@ -64,7 +63,7 @@ export class ModalComponent implements OnInit, OnDestroy, AfterViewInit { private hostElement: ElementRef, private modalService: ModalService, private backdropService: BackdropService - ) { } + ) {} /** * Align the modal in the center or top of the screen. @@ -89,7 +88,7 @@ export class ModalComponent implements OnInit, OnDestroy, AfterViewInit { * @type boolean * @default true */ - @Input({ transform: booleanAttribute }) keyboard = true; + @Input({ transform: booleanAttribute }) keyboard: boolean = true; @Input() id?: string; /** * Size the component small, large, or extra large. @@ -104,21 +103,21 @@ export class ModalComponent implements OnInit, OnDestroy, AfterViewInit { * @type string * @default 'dialog' */ - @Input() @HostBinding('attr.role') role = 'dialog'; - + @Input() @HostBinding('attr.role') role: string = 'dialog'; /** * Set aria-modal html attr for modal. [docs] * @type boolean * @default null */ - @Input() @HostBinding('attr.aria-modal') + @Input() + @HostBinding('attr.aria-modal') set ariaModal(value: boolean | null) { this.#ariaModal = value; } get ariaModal(): boolean | null { return this.visible || this.#ariaModal ? true : null; - }; + } #ariaModal: boolean | null = null; @@ -155,8 +154,12 @@ export class ModalComponent implements OnInit, OnDestroy, AfterViewInit { this.#activeElement = this.document.activeElement as HTMLElement; // this.#activeElement?.blur(); setTimeout(() => { - const focusable = this.modalContentRef.nativeElement.querySelectorAll('[tabindex]:not([tabindex="-1"]), button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled])'); - this.#focusMonitor.focusVia(focusable[0], 'keyboard'); + const focusable = this.modalContentRef.nativeElement.querySelectorAll( + '[tabindex]:not([tabindex="-1"]), button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled])' + ); + if (focusable.length) { + this.#focusMonitor.focusVia(focusable[0], 'keyboard'); + } }); } else { if (this.document.contains(this.#activeElement)) { @@ -192,7 +195,7 @@ export class ModalComponent implements OnInit, OnDestroy, AfterViewInit { @HostBinding('attr.aria-hidden') get ariaHidden(): boolean | null { return this.visible ? null : true; - }; + } @HostBinding('attr.tabindex') get tabIndex(): string | null { @@ -256,7 +259,6 @@ export class ModalComponent implements OnInit, OnDestroy, AfterViewInit { @HostListener('click', ['$event']) public onClickHandler($event: MouseEvent): void { - if (this.mouseDownTarget !== $event.target) { this.mouseDownTarget = null; return; @@ -264,7 +266,6 @@ export class ModalComponent implements OnInit, OnDestroy, AfterViewInit { const targetElement = $event.target; if (targetElement === this.hostElement.nativeElement) { - if (this.backdrop === 'static') { this.setStaticBackdrop(); return; @@ -290,27 +291,23 @@ export class ModalComponent implements OnInit, OnDestroy, AfterViewInit { } private stateToggleSubscribe(): void { - this.modalService.modalState$ - .pipe( - takeUntilDestroyed(this.#destroyRef) - ) - .subscribe( - (action) => { - if (this === action.modal || this.id === action.id) { - if ('show' in action) { - this.visible = action?.show === 'toggle' ? !this.visible : action.show; - } - } else { - if (this.visible) { - this.visible = false; - } - } + this.modalService.modalState$.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe((action) => { + if (this === action.modal || this.id === action.id) { + if ('show' in action) { + this.visible = action?.show === 'toggle' ? !this.visible : action.show; + } + } else { + if (this.visible) { + this.visible = false; } - ); + } + }); } private setBackdrop(setBackdrop: boolean): void { - this.#activeBackdrop = setBackdrop ? this.backdropService.setBackdrop('modal') : this.backdropService.clearBackdrop(this.#activeBackdrop); + this.#activeBackdrop = setBackdrop + ? this.backdropService.setBackdrop('modal') + : this.backdropService.clearBackdrop(this.#activeBackdrop); } private setBodyStyles(open: boolean): void {