Skip to content

Commit 3f50a06

Browse files
committed
fix(modal): attempt to focus when there is no focusable element on modal dialog
1 parent bb89f06 commit 3f50a06

13 files changed

+64
-52
lines changed

projects/coreui-angular/src/lib/modal/modal-body/modal-body.component.spec.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ describe('ModalBodyComponent', () => {
99
beforeEach(async () => {
1010
await TestBed.configureTestingModule({
1111
imports: [ModalBodyComponent]
12-
})
13-
.compileComponents();
12+
}).compileComponents();
1413
});
1514

1615
beforeEach(() => {
@@ -22,4 +21,8 @@ describe('ModalBodyComponent', () => {
2221
it('should create', () => {
2322
expect(component).toBeTruthy();
2423
});
24+
25+
it('should have css classes', () => {
26+
expect(fixture.nativeElement).toHaveClass('modal-body');
27+
});
2528
});

projects/coreui-angular/src/lib/modal/modal-body/modal-body.component.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,15 @@ import { Component, HostBinding } from '@angular/core';
22

33
@Component({
44
selector: 'c-modal-body',
5-
template: '<ng-content></ng-content>',
5+
template: '<ng-content />',
66
styleUrls: ['./modal-body.component.scss'],
77
standalone: true
88
})
99
export class ModalBodyComponent {
10-
1110
@HostBinding('class')
1211
get hostClasses(): any {
1312
return {
14-
'modal-body': true,
13+
'modal-body': true
1514
};
1615
}
1716
}

projects/coreui-angular/src/lib/modal/modal-content/modal-content.component.spec.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ describe('ModalContentComponent', () => {
99
beforeEach(async () => {
1010
await TestBed.configureTestingModule({
1111
imports: [ModalContentComponent]
12-
})
13-
.compileComponents();
12+
}).compileComponents();
1413
});
1514

1615
beforeEach(() => {
@@ -22,4 +21,8 @@ describe('ModalContentComponent', () => {
2221
it('should create', () => {
2322
expect(component).toBeTruthy();
2423
});
24+
25+
it('should have css classes', () => {
26+
expect(fixture.nativeElement).toHaveClass('modal-content');
27+
});
2528
});

projects/coreui-angular/src/lib/modal/modal-content/modal-content.component.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Component, HostBinding } from '@angular/core';
22

33
@Component({
44
selector: 'c-modal-content',
5-
template: '<ng-content></ng-content>',
5+
template: '<ng-content />',
66
standalone: true
77
})
88
export class ModalContentComponent {

projects/coreui-angular/src/lib/modal/modal-dialog/modal-dialog.component.spec.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ describe('ModalDialogComponent', () => {
99
beforeEach(async () => {
1010
await TestBed.configureTestingModule({
1111
imports: [ModalDialogComponent]
12-
})
13-
.compileComponents();
12+
}).compileComponents();
1413
});
1514

1615
beforeEach(() => {
@@ -22,4 +21,8 @@ describe('ModalDialogComponent', () => {
2221
it('should create', () => {
2322
expect(component).toBeTruthy();
2423
});
24+
25+
it('should have css classes', () => {
26+
expect(fixture.nativeElement).toHaveClass('modal-dialog');
27+
});
2528
});

projects/coreui-angular/src/lib/modal/modal-dialog/modal-dialog.component.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Component, HostBinding, Input } from '@angular/core';
22

33
@Component({
44
selector: 'c-modal-dialog',
5-
template: '<ng-content></ng-content>',
5+
template: '<ng-content />',
66
styleUrls: ['./modal-dialog.component.scss'],
77
standalone: true
88
})

projects/coreui-angular/src/lib/modal/modal-footer/modal-footer.component.spec.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ describe('ModalFooterComponent', () => {
99
beforeEach(async () => {
1010
await TestBed.configureTestingModule({
1111
imports: [ModalFooterComponent]
12-
})
13-
.compileComponents();
12+
}).compileComponents();
1413
});
1514

1615
beforeEach(() => {
@@ -22,4 +21,8 @@ describe('ModalFooterComponent', () => {
2221
it('should create', () => {
2322
expect(component).toBeTruthy();
2423
});
24+
25+
it('should have css classes', () => {
26+
expect(fixture.nativeElement).toHaveClass('modal-footer');
27+
});
2528
});

projects/coreui-angular/src/lib/modal/modal-footer/modal-footer.component.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@ import { Component, HostBinding } from '@angular/core';
22

33
@Component({
44
selector: 'c-modal-footer',
5-
template: '<ng-content></ng-content>',
5+
template: '<ng-content />',
66
standalone: true
77
})
88
export class ModalFooterComponent {
9-
109
@HostBinding('class')
1110
get hostClasses(): any {
1211
return {
13-
'modal-footer': true,
12+
'modal-footer': true
1413
};
1514
}
16-
1715
}

projects/coreui-angular/src/lib/modal/modal-header/modal-header.component.spec.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ describe('ModalHeaderComponent', () => {
99
beforeEach(async () => {
1010
await TestBed.configureTestingModule({
1111
imports: [ModalHeaderComponent]
12-
})
13-
.compileComponents();
12+
}).compileComponents();
1413
});
1514

1615
beforeEach(() => {
@@ -22,4 +21,8 @@ describe('ModalHeaderComponent', () => {
2221
it('should create', () => {
2322
expect(component).toBeTruthy();
2423
});
24+
25+
it('should have css classes', () => {
26+
expect(fixture.nativeElement).toHaveClass('modal-header');
27+
});
2528
});

projects/coreui-angular/src/lib/modal/modal-header/modal-header.component.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@ import { Component, HostBinding } from '@angular/core';
22

33
@Component({
44
selector: 'c-modal-header',
5-
template: `<ng-content></ng-content>`,
5+
template: '<ng-content />',
66
standalone: true
77
})
88
export class ModalHeaderComponent {
9-
109
@HostBinding('class')
1110
get hostClasses(): any {
1211
return {
1312
'modal-header': true
1413
};
1514
}
16-
1715
}

projects/coreui-angular/src/lib/modal/modal/modal.component.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[size]="size">
66
<c-modal-content>
77
<div [cdkTrapFocus]="visible" [cdkTrapFocusAutoCapture]="visible" style="display: contents;" #modalContentRef>
8-
<ng-content></ng-content>
8+
<ng-content />
99
</div>
1010
</c-modal-content>
1111
</c-modal-dialog>

projects/coreui-angular/src/lib/modal/modal/modal.component.spec.ts

+5
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,9 @@ describe('ModalComponent', () => {
2222
it('should create', () => {
2323
expect(component).toBeTruthy();
2424
});
25+
26+
it('should have css classes', () => {
27+
expect(fixture.nativeElement).toHaveClass('modal');
28+
expect(fixture.nativeElement).toHaveClass('fade');
29+
});
2530
});

projects/coreui-angular/src/lib/modal/modal/modal.component.ts

+26-29
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ import { ModalDialogComponent } from '../modal-dialog/modal-dialog.component';
5454
imports: [ModalDialogComponent, ModalContentComponent, A11yModule]
5555
})
5656
export class ModalComponent implements OnInit, OnDestroy, AfterViewInit {
57-
5857
#destroyRef = inject(DestroyRef);
5958
#focusMonitor = inject(FocusMonitor);
6059

@@ -64,7 +63,7 @@ export class ModalComponent implements OnInit, OnDestroy, AfterViewInit {
6463
private hostElement: ElementRef,
6564
private modalService: ModalService,
6665
private backdropService: BackdropService
67-
) { }
66+
) {}
6867

6968
/**
7069
* Align the modal in the center or top of the screen.
@@ -89,7 +88,7 @@ export class ModalComponent implements OnInit, OnDestroy, AfterViewInit {
8988
* @type boolean
9089
* @default true
9190
*/
92-
@Input({ transform: booleanAttribute }) keyboard = true;
91+
@Input({ transform: booleanAttribute }) keyboard: boolean = true;
9392
@Input() id?: string;
9493
/**
9594
* Size the component small, large, or extra large.
@@ -104,21 +103,21 @@ export class ModalComponent implements OnInit, OnDestroy, AfterViewInit {
104103
* @type string
105104
* @default 'dialog'
106105
*/
107-
@Input() @HostBinding('attr.role') role = 'dialog';
108-
106+
@Input() @HostBinding('attr.role') role: string = 'dialog';
109107
/**
110108
* Set aria-modal html attr for modal. [docs]
111109
* @type boolean
112110
* @default null
113111
*/
114-
@Input() @HostBinding('attr.aria-modal')
112+
@Input()
113+
@HostBinding('attr.aria-modal')
115114
set ariaModal(value: boolean | null) {
116115
this.#ariaModal = value;
117116
}
118117

119118
get ariaModal(): boolean | null {
120119
return this.visible || this.#ariaModal ? true : null;
121-
};
120+
}
122121

123122
#ariaModal: boolean | null = null;
124123

@@ -155,8 +154,12 @@ export class ModalComponent implements OnInit, OnDestroy, AfterViewInit {
155154
this.#activeElement = this.document.activeElement as HTMLElement;
156155
// this.#activeElement?.blur();
157156
setTimeout(() => {
158-
const focusable = this.modalContentRef.nativeElement.querySelectorAll('[tabindex]:not([tabindex="-1"]), button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled])');
159-
this.#focusMonitor.focusVia(focusable[0], 'keyboard');
157+
const focusable = this.modalContentRef.nativeElement.querySelectorAll(
158+
'[tabindex]:not([tabindex="-1"]), button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled])'
159+
);
160+
if (focusable.length) {
161+
this.#focusMonitor.focusVia(focusable[0], 'keyboard');
162+
}
160163
});
161164
} else {
162165
if (this.document.contains(this.#activeElement)) {
@@ -192,7 +195,7 @@ export class ModalComponent implements OnInit, OnDestroy, AfterViewInit {
192195
@HostBinding('attr.aria-hidden')
193196
get ariaHidden(): boolean | null {
194197
return this.visible ? null : true;
195-
};
198+
}
196199

197200
@HostBinding('attr.tabindex')
198201
get tabIndex(): string | null {
@@ -256,15 +259,13 @@ export class ModalComponent implements OnInit, OnDestroy, AfterViewInit {
256259

257260
@HostListener('click', ['$event'])
258261
public onClickHandler($event: MouseEvent): void {
259-
260262
if (this.mouseDownTarget !== $event.target) {
261263
this.mouseDownTarget = null;
262264
return;
263265
}
264266

265267
const targetElement = $event.target;
266268
if (targetElement === this.hostElement.nativeElement) {
267-
268269
if (this.backdrop === 'static') {
269270
this.setStaticBackdrop();
270271
return;
@@ -290,27 +291,23 @@ export class ModalComponent implements OnInit, OnDestroy, AfterViewInit {
290291
}
291292

292293
private stateToggleSubscribe(): void {
293-
this.modalService.modalState$
294-
.pipe(
295-
takeUntilDestroyed(this.#destroyRef)
296-
)
297-
.subscribe(
298-
(action) => {
299-
if (this === action.modal || this.id === action.id) {
300-
if ('show' in action) {
301-
this.visible = action?.show === 'toggle' ? !this.visible : action.show;
302-
}
303-
} else {
304-
if (this.visible) {
305-
this.visible = false;
306-
}
307-
}
294+
this.modalService.modalState$.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe((action) => {
295+
if (this === action.modal || this.id === action.id) {
296+
if ('show' in action) {
297+
this.visible = action?.show === 'toggle' ? !this.visible : action.show;
298+
}
299+
} else {
300+
if (this.visible) {
301+
this.visible = false;
308302
}
309-
);
303+
}
304+
});
310305
}
311306

312307
private setBackdrop(setBackdrop: boolean): void {
313-
this.#activeBackdrop = setBackdrop ? this.backdropService.setBackdrop('modal') : this.backdropService.clearBackdrop(this.#activeBackdrop);
308+
this.#activeBackdrop = setBackdrop
309+
? this.backdropService.setBackdrop('modal')
310+
: this.backdropService.clearBackdrop(this.#activeBackdrop);
314311
}
315312

316313
private setBodyStyles(open: boolean): void {

0 commit comments

Comments
 (0)