Skip to content

Commit

Permalink
fix(modal): attempt to focus when there is no focusable element on mo…
Browse files Browse the repository at this point in the history
…dal dialog
  • Loading branch information
xidedix committed Jun 15, 2024
1 parent bb89f06 commit 3f50a06
Show file tree
Hide file tree
Showing 13 changed files with 64 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ describe('ModalBodyComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ModalBodyComponent]
})
.compileComponents();
}).compileComponents();
});

beforeEach(() => {
Expand All @@ -22,4 +21,8 @@ describe('ModalBodyComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});

it('should have css classes', () => {
expect(fixture.nativeElement).toHaveClass('modal-body');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@ import { Component, HostBinding } from '@angular/core';

@Component({
selector: 'c-modal-body',
template: '<ng-content></ng-content>',
template: '<ng-content />',
styleUrls: ['./modal-body.component.scss'],
standalone: true
})
export class ModalBodyComponent {

@HostBinding('class')
get hostClasses(): any {
return {
'modal-body': true,
'modal-body': true
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ describe('ModalContentComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ModalContentComponent]
})
.compileComponents();
}).compileComponents();
});

beforeEach(() => {
Expand All @@ -22,4 +21,8 @@ describe('ModalContentComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});

it('should have css classes', () => {
expect(fixture.nativeElement).toHaveClass('modal-content');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Component, HostBinding } from '@angular/core';

@Component({
selector: 'c-modal-content',
template: '<ng-content></ng-content>',
template: '<ng-content />',
standalone: true
})
export class ModalContentComponent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ describe('ModalDialogComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ModalDialogComponent]
})
.compileComponents();
}).compileComponents();
});

beforeEach(() => {
Expand All @@ -22,4 +21,8 @@ describe('ModalDialogComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});

it('should have css classes', () => {
expect(fixture.nativeElement).toHaveClass('modal-dialog');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Component, HostBinding, Input } from '@angular/core';

@Component({
selector: 'c-modal-dialog',
template: '<ng-content></ng-content>',
template: '<ng-content />',
styleUrls: ['./modal-dialog.component.scss'],
standalone: true
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ describe('ModalFooterComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ModalFooterComponent]
})
.compileComponents();
}).compileComponents();
});

beforeEach(() => {
Expand All @@ -22,4 +21,8 @@ describe('ModalFooterComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});

it('should have css classes', () => {
expect(fixture.nativeElement).toHaveClass('modal-footer');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@ import { Component, HostBinding } from '@angular/core';

@Component({
selector: 'c-modal-footer',
template: '<ng-content></ng-content>',
template: '<ng-content />',
standalone: true
})
export class ModalFooterComponent {

@HostBinding('class')
get hostClasses(): any {
return {
'modal-footer': true,
'modal-footer': true
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ describe('ModalHeaderComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ModalHeaderComponent]
})
.compileComponents();
}).compileComponents();
});

beforeEach(() => {
Expand All @@ -22,4 +21,8 @@ describe('ModalHeaderComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});

it('should have css classes', () => {
expect(fixture.nativeElement).toHaveClass('modal-header');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@ import { Component, HostBinding } from '@angular/core';

@Component({
selector: 'c-modal-header',
template: `<ng-content></ng-content>`,
template: '<ng-content />',
standalone: true
})
export class ModalHeaderComponent {

@HostBinding('class')
get hostClasses(): any {
return {
'modal-header': true
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[size]="size">
<c-modal-content>
<div [cdkTrapFocus]="visible" [cdkTrapFocusAutoCapture]="visible" style="display: contents;" #modalContentRef>
<ng-content></ng-content>
<ng-content />
</div>
</c-modal-content>
</c-modal-dialog>
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
55 changes: 26 additions & 29 deletions projects/coreui-angular/src/lib/modal/modal/modal.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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;

Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -256,15 +259,13 @@ export class ModalComponent implements OnInit, OnDestroy, AfterViewInit {

@HostListener('click', ['$event'])
public onClickHandler($event: MouseEvent): void {

if (this.mouseDownTarget !== $event.target) {
this.mouseDownTarget = null;
return;
}

const targetElement = $event.target;
if (targetElement === this.hostElement.nativeElement) {

if (this.backdrop === 'static') {
this.setStaticBackdrop();
return;
Expand All @@ -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 {
Expand Down

0 comments on commit 3f50a06

Please sign in to comment.