Skip to content

Commit

Permalink
fix(modal): backdrop click focus to title (backport to 16.x) (#1513)
Browse files Browse the repository at this point in the history
Backport 9b9be45 from #1506. <br> ## PR
Checklist

Please check if your PR fulfills the following requirements:

- [x] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)
- [ ] If applicable, have a visual design approval

## PR Type

What kind of change does this PR introduce?

- [x] Bugfix
- [ ] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Documentation content changes
- [ ] Other... Please describe:

## What is the current behavior?
Clicking on modal/wizard backdrop alows the focus trap to be escaped.

Issue Number: CDE-910

## What is the new behavior?
Clicking on modal/wizard backdrop returns focus to the modal title
keeping the focus trap.

## Does this PR introduce a breaking change?

- [ ] Yes
- [x] No

## Other information

Co-authored-by: Valentin Mladenov <valentin.mladenov@broadcom.com>
  • Loading branch information
github-actions[bot] and valentin-mladenov authored Aug 9, 2024
1 parent 8204485 commit f118943
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 4 deletions.
4 changes: 4 additions & 0 deletions projects/angular/clarity.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2796,6 +2796,8 @@ export class ClrModal implements OnChanges, OnDestroy {
// (undocumented)
get backdrop(): boolean;
// (undocumented)
backdropClick(): void;
// (undocumented)
bypassScrollService: boolean;
// (undocumented)
closable: boolean;
Expand Down Expand Up @@ -2835,6 +2837,8 @@ export class ClrModal implements OnChanges, OnDestroy {
// (undocumented)
stopClose: boolean;
// (undocumented)
title: ElementRef;
// (undocumented)
static ɵcmp: i0.ɵɵComponentDeclaration<ClrModal, "clr-modal", never, { "_open": "clrModalOpen"; "closable": "clrModalClosable"; "closeButtonAriaLabel": "clrModalCloseButtonAriaLabel"; "size": "clrModalSize"; "staticBackdrop": "clrModalStaticBackdrop"; "skipAnimation": "clrModalSkipAnimation"; "stopClose": "clrModalPreventClose"; "labelledBy": "clrModalLabelledById"; "bypassScrollService": "clrModalOverrideScrollService"; }, { "_openChanged": "clrModalOpenChange"; "altClose": "clrModalAlternateClose"; }, never, [".modal-nav", ".modal-title", ".modal-body", ".modal-footer"], false, never>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<ClrModal, never>;
Expand Down
4 changes: 2 additions & 2 deletions projects/angular/src/modal/modal.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

<div class="modal-content">
<div class="modal-header--accessible">
<div class="modal-title-wrapper" id="{{modalId}}" cdkFocusInitial tabindex="-1">
<div class="modal-title-wrapper" #title id="{{modalId}}" cdkFocusInitial tabindex="-1">
<ng-content select=".modal-title"></ng-content>
</div>
<button
Expand All @@ -50,5 +50,5 @@
<div class="clr-sr-only">{{commonStrings.keys.modalContentEnd}}</div>
</div>

<div [@fade] *ngIf="backdrop" class="modal-backdrop" aria-hidden="true" (click)="staticBackdrop || close()"></div>
<div [@fade] *ngIf="backdrop" class="modal-backdrop" aria-hidden="true" (click)="backdropClick()"></div>
</div>
24 changes: 23 additions & 1 deletion projects/angular/src/modal/modal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ClrModalModule } from './modal.module';

@Component({
template: `
<button class="btn to-focus"></button>
<clr-modal
[(clrModalOpen)]="opened"
[clrModalClosable]="closable"
Expand All @@ -29,7 +30,7 @@ import { ClrModalModule } from './modal.module';
<p>Body</p>
</div>
<div class="modal-footer">
<button (click)="opened = false">Footer</button>
<button class="btn" (click)="opened = false">Footer</button>
</div>
</clr-modal>
`,
Expand Down Expand Up @@ -258,6 +259,27 @@ describe('Modal', () => {
flushAndExpectOpen(fixture, false);
}));

it('focus trap remain active after clicking on backdrop', fakeAsync(() => {
const backdrop: HTMLElement = compiled.querySelector('div.modal-backdrop');
const titleWrapperElement: HTMLElement = compiled.querySelector('div.modal-title-wrapper');
const focusStealButton: HTMLElement = compiled.querySelector('button.btn.to-focus');

fixture.componentInstance.staticBackdrop = true;
fixture.detectChanges();

// Just make sure we have the "x" to close the modal,
// because this is different from the clrModalClosable option.
expect(compiled.querySelector('.close')).not.toBeNull();
expect(document.activeElement).toBe(titleWrapperElement);

focusStealButton.focus();
expect(document.activeElement).toBe(focusStealButton);

backdrop.click();
flushAndExpectOpen(fixture, true);
expect(document.activeElement).toBe(titleWrapperElement);
}));

it('traps user focus', () => {
fixture.detectChanges();
const focusTrap = fixture.debugElement.query(By.directive(CdkTrapFocusModule_CdkTrapFocus));
Expand Down
23 changes: 22 additions & 1 deletion projects/angular/src/modal/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,18 @@
*/

import { animate, AnimationEvent, style, transition, trigger } from '@angular/animations';
import { Component, EventEmitter, HostBinding, Input, OnChanges, OnDestroy, Output, SimpleChange } from '@angular/core';
import {
Component,
ElementRef,
EventEmitter,
HostBinding,
Input,
OnChanges,
OnDestroy,
Output,
SimpleChange,
ViewChild,
} from '@angular/core';

import { ClrCommonStringsService } from '../utils/i18n/common-strings.service';
import { uniqueIdFactory } from '../utils/id-generator/id-generator.service';
Expand Down Expand Up @@ -48,6 +59,7 @@ import { ModalStackService } from './modal-stack.service';
})
export class ClrModal implements OnChanges, OnDestroy {
modalId = uniqueIdFactory();
@ViewChild('title') title: ElementRef;

@Input('clrModalOpen') @HostBinding('class.open') _open = false;
@Output('clrModalOpenChange') _openChanged = new EventEmitter<boolean>(false);
Expand Down Expand Up @@ -109,6 +121,15 @@ export class ClrModal implements OnChanges, OnDestroy {
this.modalStackService.trackModalOpen(this);
}

backdropClick(): void {
if (this.staticBackdrop) {
this.title.nativeElement.focus();
return;
}

this.close();
}

close(): void {
if (this.stopClose) {
this.altClose.emit(false);
Expand Down

0 comments on commit f118943

Please sign in to comment.