Skip to content

Commit

Permalink
feat: use cdkTrapFocus directive from @angular/cdk
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinbuhmann committed Dec 19, 2022
1 parent 19df445 commit 4828c5c
Show file tree
Hide file tree
Showing 19 changed files with 124 additions and 88 deletions.
88 changes: 43 additions & 45 deletions projects/angular/clarity.api.md

Large diffs are not rendered by default.

Expand Up @@ -57,7 +57,7 @@ let clrDgActionId = 0;
[attr.aria-hidden]="!open"
[attr.id]="popoverId"
clrKeyFocus
clrFocusTrap
cdkTrapFocus
(click)="closeOverflowContent($event)"
*clrPopoverContent="open; at: smartPosition; outsideClickToClose: true; scrollToClose: true"
>
Expand Down
Expand Up @@ -42,7 +42,7 @@ import { ColumnsService } from './providers/columns.service';
role="dialog"
[attr.aria-label]="commonStrings.keys.showColumnsMenuDescription"
[id]="popoverId"
clrFocusTrap
cdkTrapFocus
*clrPopoverContent="openState; at: smartPosition; outsideClickToClose: true; scrollToClose: true"
>
<div class="switch-header">
Expand Down
6 changes: 3 additions & 3 deletions projects/angular/src/data/datagrid/datagrid-detail.spec.ts
Expand Up @@ -65,13 +65,13 @@ export default function (): void {
}));

it('conditionally enables focus trap when opened', () => {
expect(context.clarityElement.innerHTML).not.toContain('clrfocustrap');
expect(context.clarityElement.innerHTML).not.toContain('cdkfocustrap');
detailService.open({});
context.detectChanges();
expect(context.clarityElement.innerHTML).toContain('clrfocustrap');
expect(context.clarityElement.innerHTML).toContain('cdkfocustrap');
detailService.close();
context.detectChanges();
expect(context.clarityElement.innerHTML).not.toContain('clrfocustrap');
expect(context.clarityElement.innerHTML).not.toContain('cdkfocustrap');
});

it('should have text based boundaries for screen readers', () => {
Expand Down
5 changes: 3 additions & 2 deletions projects/angular/src/data/datagrid/datagrid-detail.ts
Expand Up @@ -15,11 +15,12 @@ import { DetailService } from './providers/detail.service';
host: {
'[class.datagrid-detail-pane]': 'true',
},
// We put the *ngIf on the clrFocusTrap so it doesn't always exist on the page
// We put the *ngIf on the cdkTrapFocus so it doesn't always exist on the page
// have to test for presence of header for aria-describedby because it was causing unit tests to crash
template: `
<div
[clrFocusTrap]="{ strict: false }"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
class="datagrid-detail-pane-content"
*ngIf="detailService.isOpen"
role="dialog"
Expand Down
2 changes: 1 addition & 1 deletion projects/angular/src/data/datagrid/datagrid-filter.ts
Expand Up @@ -63,7 +63,7 @@ import { DatagridFilterRegistrar } from './utils/datagrid-filter-registrar';
<div
class="datagrid-filter"
[id]="popoverId"
clrFocusTrap
cdkTrapFocus
*clrPopoverContent="open; at: smartPosition; outsideClickToClose: true; scrollToClose: true"
role="dialog"
[attr.aria-label]="commonStrings.keys.datagridFilterDialogAriaLabel"
Expand Down
4 changes: 2 additions & 2 deletions projects/angular/src/data/datagrid/datagrid.module.ts
Expand Up @@ -25,8 +25,8 @@ import { ClrIconModule } from '../../icon/icon.module';
import { ClrSpinnerModule } from '../../progress/spinner/spinner.module';
import { ClrExpandableAnimationModule } from '../../utils/animations/expandable-animation/expandable-animation.module';
import { CdkDragModule } from '../../utils/cdk/cdk-drag.module';
import { CdkTrapFocusModule } from '../../utils/cdk/cdk-trap-focus.module';
import { ClrConditionalModule } from '../../utils/conditional/conditional.module';
import { ClrFocusTrapModule } from '../../utils/focus-trap/focus-trap.module';
import { ClrFocusOnViewInitModule } from '../../utils/focus/focus-on-view-init/focus-on-view-init.module';
import { ClrKeyFocusModule } from '../../utils/focus/key-focus/key-focus.module';
import { ClrLoadingModule } from '../../utils/loading/loading.module';
Expand Down Expand Up @@ -119,6 +119,7 @@ export const CLR_DATAGRID_DIRECTIVES: Type<any>[] = [
imports: [
CommonModule,
CdkDragModule,
CdkTrapFocusModule,
ClrIconModule,
ClrFormsModule,
FormsModule,
Expand All @@ -129,7 +130,6 @@ export const CLR_DATAGRID_DIRECTIVES: Type<any>[] = [
ClrSpinnerModule,
ClrPopoverModuleNext,
ClrKeyFocusModule,
ClrFocusTrapModule,
ClrFocusOnViewInitModule,
],
declarations: [CLR_DATAGRID_DIRECTIVES],
Expand Down
2 changes: 1 addition & 1 deletion projects/angular/src/forms/datepicker/date-container.ts
Expand Up @@ -49,7 +49,7 @@ import { ViewManagerService } from './providers/view-manager.service';
</button>
<clr-datepicker-view-manager
*clrPopoverContent="open; at: popoverPosition; outsideClickToClose: true; scrollToClose: true"
clrFocusTrap
cdkTrapFocus
></clr-datepicker-view-manager>
</div>
<cds-icon
Expand Down
4 changes: 2 additions & 2 deletions projects/angular/src/forms/datepicker/datepicker.module.ts
Expand Up @@ -16,8 +16,8 @@ import {
} from '@cds/core/icon';

import { ClrIconModule } from '../../icon/icon.module';
import { CdkTrapFocusModule } from '../../utils/cdk/cdk-trap-focus.module';
import { ClrConditionalModule } from '../../utils/conditional/conditional.module';
import { ClrFocusTrapModule } from '../../utils/focus-trap/focus-trap.module';
import { ClrHostWrappingModule } from '../../utils/host-wrapping/host-wrapping.module';
import { ClrPopoverModuleNext } from '../../utils/popover/popover.module';
import { ClrCommonFormsModule } from '../common/common.module';
Expand Down Expand Up @@ -46,11 +46,11 @@ export const CLR_DATEPICKER_DIRECTIVES: Type<any>[] = [
@NgModule({
imports: [
CommonModule,
CdkTrapFocusModule,
ClrHostWrappingModule,
ClrConditionalModule,
ClrPopoverModuleNext,
ClrIconModule,
ClrFocusTrapModule,
ClrCommonFormsModule,
],
declarations: [CLR_DATEPICKER_DIRECTIVES],
Expand Down
19 changes: 14 additions & 5 deletions projects/angular/src/layout/nav/nav-level.spec.ts
Expand Up @@ -85,6 +85,11 @@ describe('NavLevelDirective', function () {
expect(this.clarityDirective.isOpen).toBe(false);
});

it('should disable focus trap by default', function () {
const cdkTrapFocus = getCdkTrapFocus(this.clarityDirective);
expect(cdkTrapFocus.enabled).toBe(false);
});

describe('ResponsiveNavLevel intergration:', function () {
it('#registers nav on init. sends the registration code on registerNavSubject in the service', function () {
const service = new ResponsiveNavigationService();
Expand Down Expand Up @@ -140,10 +145,10 @@ describe('NavLevelDirective', function () {
expect(this.clarityDirective.showNavigation).toHaveBeenCalled();
});

it('should call enableFocusTrap()', function () {
spyOn(this.clarityDirective, 'enableFocusTrap');
it('should enable focus trap', function () {
const cdkTrapFocus = getCdkTrapFocus(this.clarityDirective);
this.clarityDirective.open();
expect(this.clarityDirective.enableFocusTrap).toHaveBeenCalled();
expect(cdkTrapFocus.enabled).toBe(true);
});

it('should call showCloseButton()', function () {
Expand All @@ -170,9 +175,9 @@ describe('NavLevelDirective', function () {
});

it('should call removeFocusTrap()', function () {
const spy = spyOn(this.clarityDirective, 'removeFocusTrap');
const cdkTrapFocus = getCdkTrapFocus(this.clarityDirective);
this.clarityDirective.close();
expect(spy).toHaveBeenCalled();
expect(cdkTrapFocus.enabled).toBeFalse();
});

it('should call hideCloseButton()', function () {
Expand Down Expand Up @@ -203,3 +208,7 @@ describe('NavLevelDirective', function () {
});
});
});

function getCdkTrapFocus(clrNavLevel: ClrNavLevel) {
return (clrNavLevel as any).cdkTrapFocus;
}
25 changes: 17 additions & 8 deletions projects/angular/src/layout/nav/nav-level.ts
Expand Up @@ -4,6 +4,7 @@
* The full license information can be found in LICENSE in the root directory of this project.
*/

import { CdkTrapFocus } from '@angular/cdk/a11y';
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
import {
Directive,
Expand All @@ -21,9 +22,9 @@ import { filter } from 'rxjs/operators';

import { commonStringsDefault } from '../../utils';
import { LARGE_BREAKPOINT } from '../../utils/breakpoints/breakpoints';
import { FocusTrap, FocusTrapElement } from '../../utils/focus-trap/focus-trap';
import { ResponsiveNavigationService } from './providers/responsive-navigation.service';
import { ResponsiveNavCodes } from './responsive-nav-codes';

import '@cds/core/internal-components/close-button/register.js';

const createCdsCloseButton = (document: Document, ariaLabel: string) => {
Expand All @@ -40,27 +41,33 @@ const createCdsCloseButton = (document: Document, ariaLabel: string) => {
return cdsCloseButton;
};

@Directive({
standalone: true,
})
class StandaloneCdkTrapFocus extends CdkTrapFocus {}

@Directive({
selector: '[clr-nav-level]',
hostDirectives: [StandaloneCdkTrapFocus],
})
export class ClrNavLevel extends FocusTrap implements OnInit {
export class ClrNavLevel implements OnInit {
@Input('clr-nav-level') _level: number;
@Input('closeAriaLabel')
closeButtonAriaLabel: string;

private _subscription: Subscription;

private _isOpen = false;
private _document: Document;

constructor(
@Inject(PLATFORM_ID) platformId: any,
private cdkTrapFocus: StandaloneCdkTrapFocus,
private responsiveNavService: ResponsiveNavigationService,
private elementRef: ElementRef<FocusTrapElement>,
renderer: Renderer2,
private elementRef: ElementRef<HTMLElement>,
private renderer: Renderer2,
injector: Injector
) {
super(renderer, injector, platformId, elementRef.nativeElement);

if (isPlatformBrowser(platformId)) {
this._document = injector.get(DOCUMENT);
}
Expand Down Expand Up @@ -91,6 +98,8 @@ export class ClrNavLevel extends FocusTrap implements OnInit {
}

ngOnInit() {
this.cdkTrapFocus.enabled = false;

if (!this.closeButtonAriaLabel) {
this.closeButtonAriaLabel =
this._level === ResponsiveNavCodes.NAV_LEVEL_1
Expand Down Expand Up @@ -159,15 +168,15 @@ export class ClrNavLevel extends FocusTrap implements OnInit {
open(): void {
this._isOpen = true;
this.showNavigation();
this.enableFocusTrap();
this.cdkTrapFocus.enabled = true;
this.showCloseButton();
this.responsiveNavService.sendControlMessage(ResponsiveNavCodes.NAV_OPEN, this.level);
}

close(): void {
this._isOpen = false;
this.hideNavigation();
this.removeFocusTrap();
this.cdkTrapFocus.enabled = false;
this.hideCloseButton();
this.responsiveNavService.sendControlMessage(ResponsiveNavCodes.NAV_CLOSE, this.level);
}
Expand Down
2 changes: 1 addition & 1 deletion projects/angular/src/modal/modal.html
Expand Up @@ -4,7 +4,7 @@
~ The full license information can be found in LICENSE in the root directory of this project.
-->

<div clrFocusTrap class="modal" *ngIf="_open">
<div cdkTrapFocus class="modal" *ngIf="_open">
<!--fixme: revisit when ngClass works with exit animation-->
<div
[@fadeDown]="skipAnimation"
Expand Down
4 changes: 2 additions & 2 deletions projects/angular/src/modal/modal.module.ts
Expand Up @@ -9,15 +9,15 @@ import { NgModule, Type } from '@angular/core';
import { ClarityIcons, windowCloseIcon } from '@cds/core/icon';

import { ClrIconModule } from '../icon/icon.module';
import { ClrFocusTrapModule } from '../utils/focus-trap/focus-trap.module';
import { CdkTrapFocusModule } from '../utils/cdk/cdk-trap-focus.module';
import { ClrFocusOnViewInitModule } from '../utils/focus/focus-on-view-init/focus-on-view-init.module';
import { ClrModal } from './modal';
import { ClrModalBody } from './modal-body';

export const CLR_MODAL_DIRECTIVES: Type<any>[] = [ClrModal, ClrModalBody];

@NgModule({
imports: [CommonModule, ClrIconModule, ClrFocusTrapModule, ClrFocusOnViewInitModule],
imports: [CommonModule, CdkTrapFocusModule, ClrIconModule, ClrFocusOnViewInitModule],
declarations: [CLR_MODAL_DIRECTIVES],
exports: [CLR_MODAL_DIRECTIVES, ClrIconModule, ClrFocusOnViewInitModule],
})
Expand Down
7 changes: 3 additions & 4 deletions projects/angular/src/modal/modal.spec.ts
Expand Up @@ -10,8 +10,7 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testin
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';

import { FocusTrapDirective } from '../utils/focus-trap/focus-trap.directive';
import { ClrFocusTrapModule } from '../utils/focus-trap/focus-trap.module';
import { CdkTrapFocusModule, CdkTrapFocusModule_CdkTrapFocus } from '../utils/cdk/cdk-trap-focus.module';
import { ClrCommonStringsService } from '../utils/i18n/common-strings.service';
import { ClrModal } from './modal';
import { ClrModalModule } from './modal.module';
Expand Down Expand Up @@ -70,7 +69,7 @@ describe('Modal', () => {

beforeEach(() => {
TestBed.configureTestingModule({
imports: [ClrModalModule, NoopAnimationsModule, ClrFocusTrapModule],
imports: [CdkTrapFocusModule, ClrModalModule, NoopAnimationsModule],
declarations: [TestComponent, TestDefaultsComponent],
});

Expand Down Expand Up @@ -254,7 +253,7 @@ describe('Modal', () => {

it('traps user focus', () => {
fixture.detectChanges();
const focusTrap = fixture.debugElement.query(By.directive(FocusTrapDirective));
const focusTrap = fixture.debugElement.query(By.directive(CdkTrapFocusModule_CdkTrapFocus));

expect(focusTrap).toBeTruthy();
});
Expand Down
6 changes: 0 additions & 6 deletions projects/angular/src/modal/modal.ts
Expand Up @@ -15,10 +15,8 @@ import {
OnDestroy,
Output,
SimpleChange,
ViewChild,
} from '@angular/core';

import { FocusTrapDirective } from '../utils/focus-trap/focus-trap.directive';
import { ClrCommonStringsService } from '../utils/i18n/common-strings.service';
import { uniqueIdFactory } from '../utils/id-generator/id-generator.service';
import { ScrollingService } from '../utils/scrolling/scrolling-service';
Expand Down Expand Up @@ -51,8 +49,6 @@ import { ScrollingService } from '../utils/scrolling/scrolling-service';
export class ClrModal implements OnChanges, OnDestroy {
modalId = uniqueIdFactory();

@ViewChild(FocusTrapDirective) focusTrap: FocusTrapDirective;

@HostBinding('class.open')
@Input('clrModalOpen')
_open = false;
Expand Down Expand Up @@ -104,8 +100,6 @@ export class ClrModal implements OnChanges, OnDestroy {
return;
}
this._open = false;
// SPECME
this.focusTrap.setPreviousFocus(); // Handles moving focus back to the element that had it before.
}

fadeDone(e: AnimationEvent) {
Expand Down
25 changes: 25 additions & 0 deletions projects/angular/src/utils/cdk/cdk-trap-focus.module.ts
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2016-2022 VMware, Inc. All Rights Reserved.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/

import { CdkTrapFocus } from '@angular/cdk/a11y';
import { Directive, NgModule } from '@angular/core';

/**
* This is just a copy of CdkTrapFocus so it can be used independent of the rest of the A11yModule.
*/
@Directive({
selector: '[cdkTrapFocus]',
})
export class CdkTrapFocusModule_CdkTrapFocus extends CdkTrapFocus {}

/**
* This module allows us to avoid importing all of A11yModule which results in a smaller application bundle.
*/
@NgModule({
declarations: [CdkTrapFocusModule_CdkTrapFocus],
exports: [CdkTrapFocusModule_CdkTrapFocus],
})
export class CdkTrapFocusModule {}
2 changes: 1 addition & 1 deletion projects/angular/src/utils/popover/README.md
Expand Up @@ -35,7 +35,7 @@ This is what it looks like from the implementing component perspective:
<div
[id]="popoverId"
role="dialog"
clrFocusTrap
cdkTrapFocus
*clrPopoverContent="openState at contentPosition; outsideClickToClose: true; scrollToClose: true"
>
<header class="header-4" role="heading">
Expand Down
4 changes: 2 additions & 2 deletions projects/demo/src/app/modal/modal-trap.demo.html
Expand Up @@ -6,15 +6,15 @@

<p>
For accessibility purposes, you may want to trap user focus to within the modal. You can use the
<code>clrFocusTrap</code> directive to keep the user tabbing from within the modal. This is not needed when using the
<code>cdkTrapFocus</code> directive to keep the user tabbing from within the modal. This is not needed when using the
<code>clr-modal</code> component because the component includes this by default.
</p>

<div class="clr-example">
<div class="backdrop-example-container">
<div class="modal static">
<div class="modal-dialog" role="dialog" aria-hidden="true">
<div clrFocusTrap class="modal-content">
<div cdkTrapFocus class="modal-content">
<div class="modal-header">
<button aria-label="Close" class="close" type="button">
<cds-icon aria-hidden="true" shape="window-close"></cds-icon>
Expand Down

0 comments on commit 4828c5c

Please sign in to comment.