mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(focus-trap): allow setting initially focused element (#4577)
This commit is contained in:
@@ -61,3 +61,27 @@
|
||||
<button (click)="invert = !invert">Change sides</button>
|
||||
</div>
|
||||
</md-sidenav-container>
|
||||
|
||||
<h2>Sidenav with focus attributes</h2>
|
||||
|
||||
<md-sidenav-container class="demo-sidenav-container">
|
||||
<md-sidenav #focusSidenav>
|
||||
<md-nav-list>
|
||||
<a md-list-item routerLink>Link</a>
|
||||
<a md-list-item routerLink cdk-focus-region-start>Focus region start</a>
|
||||
<a md-list-item routerLink>Link</a>
|
||||
<a md-list-item routerLink cdk-focus-initial>Initially focused</a>
|
||||
<a md-list-item routerLink cdk-focus-region-end>Focus region end</a>
|
||||
<a md-list-item routerLink>Link</a>
|
||||
</md-nav-list>
|
||||
</md-sidenav>
|
||||
|
||||
<div class="demo-sidenav-content">
|
||||
<h1>My Content</h1>
|
||||
|
||||
<div>
|
||||
<header>Sidenav</header>
|
||||
<button md-button (click)="focusSidenav.toggle()">Toggle Drawer</button>
|
||||
</div>
|
||||
</div>
|
||||
</md-sidenav-container>
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('FocusTrap', () => {
|
||||
FocusTrapWithBindings,
|
||||
SimpleFocusTrap,
|
||||
FocusTrapTargets,
|
||||
FocusTrapWithSvg
|
||||
FocusTrapWithSvg,
|
||||
],
|
||||
providers: [InteractivityChecker, Platform, FocusTrapFactory]
|
||||
});
|
||||
@@ -104,6 +104,13 @@ describe('FocusTrap', () => {
|
||||
focusTrapInstance = fixture.componentInstance.focusTrapDirective.focusTrap;
|
||||
});
|
||||
|
||||
it('should be able to set initial focus target', () => {
|
||||
// Because we can't mimic a real tab press focus change in a unit test, just call the
|
||||
// focus event handler directly.
|
||||
focusTrapInstance.focusInitialElement();
|
||||
expect(document.activeElement.id).toBe('middle');
|
||||
});
|
||||
|
||||
it('should be able to prioritize the first focus target', () => {
|
||||
// Because we can't mimic a real tab press focus change in a unit test, just call the
|
||||
// focus event handler directly.
|
||||
@@ -131,7 +138,6 @@ describe('FocusTrap', () => {
|
||||
expect(() => focusTrapInstance.focusLastTabbableElement()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -167,8 +173,11 @@ class FocusTrapWithBindings {
|
||||
template: `
|
||||
<div cdkTrapFocus>
|
||||
<input>
|
||||
<button id="last" cdk-focus-end></button>
|
||||
<button id="first" cdk-focus-start>SAVE</button>
|
||||
<button>before</button>
|
||||
<button id="first" cdk-focus-region-start></button>
|
||||
<button id="middle" cdk-focus-initial></button>
|
||||
<button id="last" cdk-focus-region-end></button>
|
||||
<button>after</button>
|
||||
<input>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -81,6 +81,10 @@ export class FocusTrap {
|
||||
});
|
||||
}
|
||||
|
||||
focusInitialElementWhenReady() {
|
||||
this._ngZone.onMicrotaskEmpty.first().subscribe(() => this.focusInitialElement());
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for microtask queue to empty, then focuses
|
||||
* the first tabbable element within the focus trap region.
|
||||
@@ -97,11 +101,45 @@ export class FocusTrap {
|
||||
this._ngZone.onMicrotaskEmpty.first().subscribe(() => this.focusLastTabbableElement());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the specified boundary element of the trapped region.
|
||||
* @param bound The boundary to get (start or end of trapped region).
|
||||
* @returns The boundary element.
|
||||
*/
|
||||
private _getRegionBoundary(bound: 'start' | 'end'): HTMLElement | null {
|
||||
let markers = [
|
||||
...Array.prototype.slice.call(this._element.querySelectorAll(`[cdk-focus-region-${bound}]`)),
|
||||
// Deprecated version of selector, for temporary backwards comparability:
|
||||
...Array.prototype.slice.call(this._element.querySelectorAll(`[cdk-focus-${bound}]`)),
|
||||
];
|
||||
|
||||
markers.forEach((el: HTMLElement) => {
|
||||
if (el.hasAttribute(`cdk-focus-${bound}`)) {
|
||||
console.warn(`Found use of deprecated attribute 'cdk-focus-${bound}',` +
|
||||
` use 'cdk-focus-region-${bound}' instead.`, el);
|
||||
}
|
||||
});
|
||||
|
||||
if (bound == 'start') {
|
||||
return markers.length ? markers[0] : this._getFirstTabbableElement(this._element);
|
||||
}
|
||||
return markers.length ?
|
||||
markers[markers.length - 1] : this._getLastTabbableElement(this._element);
|
||||
}
|
||||
|
||||
/** Focuses the element that should be focused when the focus trap is initialized. */
|
||||
focusInitialElement() {
|
||||
let redirectToElement = this._element.querySelector('[cdk-focus-initial]') as HTMLElement;
|
||||
if (redirectToElement) {
|
||||
redirectToElement.focus();
|
||||
} else {
|
||||
this.focusFirstTabbableElement();
|
||||
}
|
||||
}
|
||||
|
||||
/** Focuses the first tabbable element within the focus trap region. */
|
||||
focusFirstTabbableElement() {
|
||||
let redirectToElement = this._element.querySelector('[cdk-focus-start]') as HTMLElement ||
|
||||
this._getFirstTabbableElement(this._element);
|
||||
|
||||
let redirectToElement = this._getRegionBoundary('start');
|
||||
if (redirectToElement) {
|
||||
redirectToElement.focus();
|
||||
}
|
||||
@@ -109,15 +147,7 @@ export class FocusTrap {
|
||||
|
||||
/** Focuses the last tabbable element within the focus trap region. */
|
||||
focusLastTabbableElement() {
|
||||
let focusTargets = this._element.querySelectorAll('[cdk-focus-end]');
|
||||
let redirectToElement: HTMLElement = null;
|
||||
|
||||
if (focusTargets.length) {
|
||||
redirectToElement = focusTargets[focusTargets.length - 1] as HTMLElement;
|
||||
} else {
|
||||
redirectToElement = this._getLastTabbableElement(this._element);
|
||||
}
|
||||
|
||||
let redirectToElement = this._getRegionBoundary('end');
|
||||
if (redirectToElement) {
|
||||
redirectToElement.focus();
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ export class MdDialogContainer extends BasePortalHost {
|
||||
// If were to attempt to focus immediately, then the content of the dialog would not yet be
|
||||
// ready in instances where change detection has to run first. To deal with this, we simply
|
||||
// wait for the microtask queue to be empty.
|
||||
this._focusTrap.focusFirstTabbableElementWhenReady();
|
||||
this._focusTrap.focusInitialElementWhenReady();
|
||||
}
|
||||
|
||||
/** Restores focus to the element that was focused before the dialog opened. */
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
border-top-color: mat-color($foreground, divider);
|
||||
}
|
||||
|
||||
.mat-nav-list .mat-list-item-content {
|
||||
.mat-nav-list .mat-list-item {
|
||||
outline: none;
|
||||
|
||||
&:hover, &.mat-list-item-focus {
|
||||
background: mat-color($background, 'hover');
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="mat-list-item-content" [class.mat-list-item-focus]="_hasFocus">
|
||||
<div class="mat-list-item-content">
|
||||
<div class="mat-list-item-ripple" md-ripple
|
||||
[mdRippleTrigger]="_getHostElement()"
|
||||
[mdRippleDisabled]="!isRippleEnabled()">
|
||||
|
||||
@@ -28,18 +28,19 @@ describe('MdList', () => {
|
||||
|
||||
it('should add and remove focus class on focus/blur', () => {
|
||||
let fixture = TestBed.createComponent(ListWithOneAnchorItem);
|
||||
let listItem = fixture.debugElement.query(By.directive(MdListItem));
|
||||
let listItemDiv = fixture.debugElement.query(By.css('.mat-list-item-content'));
|
||||
fixture.detectChanges();
|
||||
expect(listItemDiv.nativeElement.classList).not.toContain('mat-list-item-focus');
|
||||
let listItem = fixture.debugElement.query(By.directive(MdListItem));
|
||||
let listItemEl = fixture.debugElement.query(By.css('.mat-list-item'));
|
||||
|
||||
expect(listItemEl.nativeElement.classList).not.toContain('mat-list-item-focus');
|
||||
|
||||
listItem.componentInstance._handleFocus();
|
||||
fixture.detectChanges();
|
||||
expect(listItemDiv.nativeElement.classList).toContain('mat-list-item-focus');
|
||||
expect(listItemEl.nativeElement.classList).toContain('mat-list-item-focus');
|
||||
|
||||
listItem.componentInstance._handleBlur();
|
||||
fixture.detectChanges();
|
||||
expect(listItemDiv.nativeElement.classList).not.toContain('mat-list-item-focus');
|
||||
expect(listItemEl.nativeElement.classList).not.toContain('mat-list-item-focus');
|
||||
});
|
||||
|
||||
it('should not apply any additional class to a list without lines', () => {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import {
|
||||
AfterContentInit,
|
||||
Component,
|
||||
ViewEncapsulation,
|
||||
ContentChildren,
|
||||
ContentChild,
|
||||
QueryList,
|
||||
ContentChildren,
|
||||
Directive,
|
||||
ElementRef,
|
||||
Input,
|
||||
Optional,
|
||||
QueryList,
|
||||
Renderer2,
|
||||
AfterContentInit,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import {MdLine, MdLineSetter, coerceBooleanProperty} from '../core';
|
||||
import {coerceBooleanProperty, MdLine, MdLineSetter} from '../core';
|
||||
|
||||
@Directive({
|
||||
selector: 'md-divider, mat-divider'
|
||||
@@ -128,8 +128,6 @@ export class MdListItem implements AfterContentInit {
|
||||
private _disableRipple: boolean = false;
|
||||
private _isNavList: boolean = false;
|
||||
|
||||
_hasFocus: boolean = false;
|
||||
|
||||
/**
|
||||
* Whether the ripple effect on click should be disabled. This applies only to list items that are
|
||||
* part of a nav list. The value of `disableRipple` on the `md-nav-list` overrides this flag.
|
||||
@@ -166,11 +164,11 @@ export class MdListItem implements AfterContentInit {
|
||||
}
|
||||
|
||||
_handleFocus() {
|
||||
this._hasFocus = true;
|
||||
this._renderer.addClass(this._element.nativeElement, 'mat-list-item-focus');
|
||||
}
|
||||
|
||||
_handleBlur() {
|
||||
this._hasFocus = false;
|
||||
this._renderer.removeClass(this._element.nativeElement, 'mat-list-item-focus');
|
||||
}
|
||||
|
||||
/** Retrieves the DOM element of the component host. */
|
||||
|
||||
@@ -131,7 +131,7 @@ export class MdSidenav implements AfterContentInit, OnDestroy {
|
||||
this._elementFocusedBeforeSidenavWasOpened = document.activeElement as HTMLElement;
|
||||
|
||||
if (this.isFocusTrapEnabled && this._focusTrap) {
|
||||
this._focusTrap.focusFirstTabbableElementWhenReady();
|
||||
this._focusTrap.focusInitialElementWhenReady();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user