mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(interactivity-checker): improve robustness of isTabbable (#1950)
* fix(focus-trap): improve robustness * Address feedback * Remove extra blank line. * Address additional feedback * Change timeout delay
This commit is contained in:
committed by
tinayuangao
parent
76a8d79534
commit
4b7e52d30a
@@ -35,6 +35,7 @@ import {PortalDemo, ScienceJoke} from './portal/portal-demo';
|
||||
import {MenuDemo} from './menu/menu-demo';
|
||||
import {TabsDemo, SunnyTabContent, RainyTabContent, FoggyTabContent} from './tabs/tabs-demo';
|
||||
import {ProjectionDemo, ProjectionTestComponent} from './projection/projection-demo';
|
||||
import {PlatformDemo} from './platform/platform-demo';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -86,6 +87,7 @@ import {ProjectionDemo, ProjectionTestComponent} from './projection/projection-d
|
||||
SunnyTabContent,
|
||||
RainyTabContent,
|
||||
FoggyTabContent,
|
||||
PlatformDemo
|
||||
],
|
||||
entryComponents: [
|
||||
DemoApp,
|
||||
|
||||
@@ -47,6 +47,7 @@ export class DemoApp {
|
||||
{name: 'Snack Bar', route: 'snack-bar'},
|
||||
{name: 'Tabs', route: 'tabs'},
|
||||
{name: 'Toolbar', route: 'toolbar'},
|
||||
{name: 'Tooltip', route: 'tooltip'}
|
||||
{name: 'Tooltip', route: 'tooltip'},
|
||||
{name: 'Platform', route: 'platform'}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {TooltipDemo} from '../tooltip/tooltip-demo';
|
||||
import {SnackBarDemo} from '../snack-bar/snack-bar-demo';
|
||||
import {ProjectionDemo} from '../projection/projection-demo';
|
||||
import {TABS_DEMO_ROUTES} from '../tabs/routes';
|
||||
import {PlatformDemo} from '../platform/platform-demo';
|
||||
|
||||
export const DEMO_APP_ROUTES: Routes = [
|
||||
{path: '', component: Home},
|
||||
@@ -62,4 +63,5 @@ export const DEMO_APP_ROUTES: Routes = [
|
||||
{path: 'dialog', component: DialogDemo},
|
||||
{path: 'tooltip', component: TooltipDemo},
|
||||
{path: 'snack-bar', component: SnackBarDemo},
|
||||
{path: 'platform', component: PlatformDemo}
|
||||
];
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {MdPlatform} from '@angular/material';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<p>Is Android: {{ platform.ANDROID }}</p>
|
||||
<p>Is iOS: {{ platform.IOS }}</p>
|
||||
<p>Is Firefox: {{ platform.FIREFOX }}</p>
|
||||
<p>Is Blink: {{ platform.BLINK }}</p>
|
||||
<p>Is Webkit: {{ platform.WEBKIT }}</p>
|
||||
<p>Is Trident: {{ platform.TRIDENT }}</p>
|
||||
<p>Is Edge: {{ platform.EDGE }}</p>
|
||||
`
|
||||
})
|
||||
export class PlatformDemo {
|
||||
constructor(public platform: MdPlatform) {}
|
||||
}
|
||||
@@ -3,17 +3,21 @@ import {By} from '@angular/platform-browser';
|
||||
import {Component} from '@angular/core';
|
||||
import {FocusTrap} from './focus-trap';
|
||||
import {InteractivityChecker} from './interactivity-checker';
|
||||
import {MdPlatform} from '../platform/platform';
|
||||
|
||||
|
||||
describe('FocusTrap', () => {
|
||||
|
||||
describe('with default element', () => {
|
||||
|
||||
let fixture: ComponentFixture<FocusTrapTestApp>;
|
||||
let focusTrapInstance: FocusTrap;
|
||||
let platform: MdPlatform = new MdPlatform();
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [FocusTrap, FocusTrapTestApp],
|
||||
providers: [InteractivityChecker]
|
||||
providers: [InteractivityChecker, MdPlatform]
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
@@ -38,8 +42,11 @@ describe('FocusTrap', () => {
|
||||
// focus event handler directly.
|
||||
focusTrapInstance.focusLastTabbableElement();
|
||||
|
||||
// In iOS button elements are never tabbable, so the last element will be the input.
|
||||
let lastElement = platform.IOS ? 'input' : 'button';
|
||||
|
||||
expect(document.activeElement.nodeName.toLowerCase())
|
||||
.toBe('button', 'Expected button element to be focused');
|
||||
.toBe(lastElement, `Expected ${lastElement} element to be focused`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,7 +57,7 @@ describe('FocusTrap', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [FocusTrap, FocusTrapTargetTestApp],
|
||||
providers: [InteractivityChecker]
|
||||
providers: [InteractivityChecker, MdPlatform]
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
|
||||
@@ -2,10 +2,12 @@ import {NgModule, ModuleWithProviders} from '@angular/core';
|
||||
import {FocusTrap} from './focus-trap';
|
||||
import {MdLiveAnnouncer} from './live-announcer';
|
||||
import {InteractivityChecker} from './interactivity-checker';
|
||||
import {PlatformModule} from '../platform/platform';
|
||||
|
||||
export const A11Y_PROVIDERS = [MdLiveAnnouncer, InteractivityChecker];
|
||||
|
||||
@NgModule({
|
||||
imports: [PlatformModule],
|
||||
declarations: [FocusTrap],
|
||||
exports: [FocusTrap],
|
||||
})
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import {InteractivityChecker} from './interactivity-checker';
|
||||
import {MdPlatform} from '../platform/platform';
|
||||
import {async} from '@angular/core/testing';
|
||||
|
||||
describe('InteractivityChecker', () => {
|
||||
let testContainerElement: HTMLElement;
|
||||
let checker: InteractivityChecker;
|
||||
let platform: MdPlatform = new MdPlatform();
|
||||
|
||||
beforeEach(() => {
|
||||
testContainerElement = document.createElement('div');
|
||||
document.body.appendChild(testContainerElement);
|
||||
|
||||
checker = new InteractivityChecker();
|
||||
checker = new InteractivityChecker(platform);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -234,52 +237,237 @@ describe('InteractivityChecker', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return true for div and span with tabindex == 0', () => {
|
||||
let elements = createElements('div', 'span');
|
||||
|
||||
elements.forEach(el => el.setAttribute('tabindex', '0'));
|
||||
appendElements(elements);
|
||||
|
||||
elements.forEach(el => {
|
||||
expect(checker.isFocusable(el))
|
||||
.toBe(true, `Expected <${el.nodeName} tabindex="0"> to be focusable`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTabbable', () => {
|
||||
it('should return true for native form controls and anchor without tabindex attribute', () => {
|
||||
let elements = createElements('input', 'textarea', 'select', 'button', 'a');
|
||||
appendElements(elements);
|
||||
|
||||
elements.forEach(el => {
|
||||
expect(checker.isTabbable(el)).toBe(true, `Expected <${el.nodeName}> to be tabbable`);
|
||||
it('should respect the tabindex for video elements with controls',
|
||||
// Do not run for Blink, Firefox and iOS because those treat video elements
|
||||
// with controls different and are covered in other tests.
|
||||
runIf(!platform.BLINK && !platform.FIREFOX && !platform.IOS, () => {
|
||||
let video = createFromTemplate('<video controls>', true);
|
||||
|
||||
expect(checker.isTabbable(video)).toBe(true);
|
||||
|
||||
video.tabIndex = -1;
|
||||
|
||||
expect(checker.isTabbable(video)).toBe(false);
|
||||
})
|
||||
);
|
||||
|
||||
it('should always mark video elements with controls as tabbable (BLINK & FIREFOX)',
|
||||
// Only run this spec for Blink and Firefox, because those always treat video
|
||||
// elements with controls as tabbable.
|
||||
runIf(platform.BLINK || platform.FIREFOX, () => {
|
||||
let video = createFromTemplate('<video controls>', true);
|
||||
|
||||
expect(checker.isTabbable(video)).toBe(true);
|
||||
|
||||
video.tabIndex = -1;
|
||||
|
||||
expect(checker.isTabbable(video)).toBe(true);
|
||||
})
|
||||
);
|
||||
|
||||
// Some tests should not run inside of iOS browsers, because those only allow specific
|
||||
// elements to be tabbable and cause the tests to always fail.
|
||||
describe('for non-iOS browsers', runIf(!platform.IOS, () => {
|
||||
|
||||
it('should mark form controls and anchors without tabindex attribute as tabbable', () => {
|
||||
let elements = createElements('input', 'textarea', 'select', 'button', 'a');
|
||||
appendElements(elements);
|
||||
|
||||
elements.forEach(el => {
|
||||
expect(checker.isTabbable(el)).toBe(true, `Expected <${el.nodeName}> to be tabbable`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false for native form controls and anchor with tabindex == -1', () => {
|
||||
let elements = createElements('input', 'textarea', 'select', 'button', 'a');
|
||||
it('should return true for div and span with tabindex == 0', () => {
|
||||
let elements = createElements('div', 'span');
|
||||
|
||||
elements.forEach(el => el.setAttribute('tabindex', '-1'));
|
||||
appendElements(elements);
|
||||
elements.forEach(el => el.setAttribute('tabindex', '0'));
|
||||
appendElements(elements);
|
||||
|
||||
elements.forEach(el => {
|
||||
expect(checker.isTabbable(el))
|
||||
elements.forEach(el => {
|
||||
expect(checker.isFocusable(el))
|
||||
.toBe(true, `Expected <${el.nodeName} tabindex="0"> to be focusable`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false for native form controls and anchor with tabindex == -1', () => {
|
||||
let elements = createElements('input', 'textarea', 'select', 'button', 'a');
|
||||
|
||||
elements.forEach(el => el.setAttribute('tabindex', '-1'));
|
||||
appendElements(elements);
|
||||
|
||||
elements.forEach(el => {
|
||||
expect(checker.isTabbable(el))
|
||||
.toBe(false, `Expected <${el.nodeName} tabindex="-1"> not to be tabbable`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return true for div and span with tabindex == 0', () => {
|
||||
let elements = createElements('div', 'span');
|
||||
it('should return true for div and span with tabindex == 0', () => {
|
||||
let elements = createElements('div', 'span');
|
||||
|
||||
elements.forEach(el => el.setAttribute('tabindex', '0'));
|
||||
appendElements(elements);
|
||||
elements.forEach(el => el.setAttribute('tabindex', '0'));
|
||||
appendElements(elements);
|
||||
|
||||
elements.forEach(el => {
|
||||
expect(checker.isTabbable(el))
|
||||
elements.forEach(el => {
|
||||
expect(checker.isTabbable(el))
|
||||
.toBe(true, `Expected <${el.nodeName} tabindex="0"> to be tabbable`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect the inherited tabindex inside of frame elements', () => {
|
||||
let iframe = createFromTemplate('<iframe>', true) as HTMLFrameElement;
|
||||
let button = createFromTemplate('<button tabindex="0">Not Tabbable</button>');
|
||||
|
||||
appendElements([iframe]);
|
||||
|
||||
iframe.tabIndex = -1;
|
||||
iframe.contentDocument.body.appendChild(button);
|
||||
|
||||
expect(checker.isTabbable(iframe)).toBe(false);
|
||||
expect(checker.isTabbable(button)).toBe(false);
|
||||
|
||||
iframe.tabIndex = null;
|
||||
|
||||
expect(checker.isTabbable(iframe)).toBe(false);
|
||||
expect(checker.isTabbable(button)).toBe(true);
|
||||
});
|
||||
|
||||
it('should mark elements which are contentEditable as tabbable', async(() => {
|
||||
let editableEl = createFromTemplate('<div contenteditable="true">', true);
|
||||
|
||||
// Wait one tick, because the browser takes some time to update the tabIndex
|
||||
// according to the contentEditable attribute.
|
||||
setTimeout(() => {
|
||||
|
||||
expect(checker.isTabbable(editableEl)).toBe(true);
|
||||
|
||||
editableEl.tabIndex = -1;
|
||||
|
||||
expect(checker.isTabbable(editableEl)).toBe(false);
|
||||
|
||||
}, 0);
|
||||
|
||||
}));
|
||||
|
||||
it('should never mark iframe elements as tabbable', () => {
|
||||
let iframe = createFromTemplate('<iframe>', true);
|
||||
|
||||
// iFrame elements will be never marked as tabbable, because it depends on the content
|
||||
// which is mostly not detectable due to CORS and also the checks will be not reliable.
|
||||
expect(checker.isTabbable(iframe)).toBe(false);
|
||||
});
|
||||
|
||||
it('should always mark audio elements without controls as not tabbable', () => {
|
||||
let audio = createFromTemplate('<audio>', true);
|
||||
|
||||
expect(checker.isTabbable(audio)).toBe(false);
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
describe('for Blink and Webkit browsers', runIf(platform.BLINK || platform.WEBKIT, () => {
|
||||
|
||||
it('should not mark elements inside of object frames as tabbable', () => {
|
||||
let objectEl = createFromTemplate('<object>', true) as HTMLObjectElement;
|
||||
let button = createFromTemplate('<button tabindex="0">Not Tabbable</button>');
|
||||
|
||||
appendElements([objectEl]);
|
||||
|
||||
// This is a hack to create an empty contentDocument for the frame element.
|
||||
objectEl.type = 'text/html';
|
||||
objectEl.contentDocument.body.appendChild(button);
|
||||
|
||||
expect(checker.isTabbable(objectEl)).toBe(false);
|
||||
expect(checker.isTabbable(button)).toBe(false);
|
||||
});
|
||||
|
||||
it('should not mark elements inside of invisible frames as tabbable', () => {
|
||||
let iframe = createFromTemplate('<iframe>', true) as HTMLFrameElement;
|
||||
let button = createFromTemplate('<button tabindex="0">Not Tabbable</button>');
|
||||
|
||||
appendElements([iframe]);
|
||||
|
||||
iframe.style.display = 'none';
|
||||
iframe.contentDocument.body.appendChild(button);
|
||||
|
||||
expect(checker.isTabbable(iframe)).toBe(false);
|
||||
expect(checker.isTabbable(button)).toBe(false);
|
||||
});
|
||||
|
||||
it('should never mark object frame elements as tabbable', () => {
|
||||
let objectEl = createFromTemplate('<object>', true);
|
||||
|
||||
expect(checker.isTabbable(objectEl)).toBe(false);
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
describe('for Blink browsers', runIf(platform.BLINK, () => {
|
||||
|
||||
it('should always mark audio elements with controls as tabbable', () => {
|
||||
let audio = createFromTemplate('<audio controls>', true);
|
||||
|
||||
expect(checker.isTabbable(audio)).toBe(true);
|
||||
|
||||
audio.tabIndex = -1;
|
||||
|
||||
// The audio element will be still tabbable because Blink always
|
||||
// considers them as tabbable.
|
||||
expect(checker.isTabbable(audio)).toBe(true);
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
describe('for Internet Explorer', runIf(platform.TRIDENT, () => {
|
||||
|
||||
it('should never mark video elements without controls as tabbable', () => {
|
||||
// In Internet Explorer video elements without controls are never tabbable.
|
||||
let video = createFromTemplate('<video>', true);
|
||||
|
||||
expect(checker.isTabbable(video)).toBe(false);
|
||||
|
||||
video.tabIndex = 0;
|
||||
|
||||
expect(checker.isTabbable(video)).toBe(false);
|
||||
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
describe('for iOS browsers', runIf(platform.IOS && platform.WEBKIT, () => {
|
||||
|
||||
it('should never allow div elements to be tabbable', () => {
|
||||
let divEl = createFromTemplate('<div tabindex="0">', true);
|
||||
|
||||
expect(checker.isTabbable(divEl)).toBe(false);
|
||||
});
|
||||
|
||||
it('should never allow span elements to be tabbable', () => {
|
||||
let spanEl = createFromTemplate('<span tabindex="0">Text</span>', true);
|
||||
|
||||
expect(checker.isTabbable(spanEl)).toBe(false);
|
||||
});
|
||||
|
||||
it('should never allow button elements to be tabbable', () => {
|
||||
let buttonEl = createFromTemplate('<button tabindex="0">', true);
|
||||
|
||||
expect(checker.isTabbable(buttonEl)).toBe(false);
|
||||
});
|
||||
|
||||
it('should never allow anchor elements to be tabbable', () => {
|
||||
let anchorEl = createFromTemplate('<a tabindex="0">Link</a>', true);
|
||||
|
||||
expect(checker.isTabbable(anchorEl)).toBe(false);
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
|
||||
});
|
||||
|
||||
/** Creates an array of elements with the given node names. */
|
||||
@@ -287,10 +475,34 @@ describe('InteractivityChecker', () => {
|
||||
return nodeNames.map(name => document.createElement(name));
|
||||
}
|
||||
|
||||
function createFromTemplate(template: string, append = false) {
|
||||
let tmpRoot = document.createElement('div');
|
||||
tmpRoot.innerHTML = template;
|
||||
|
||||
let element = tmpRoot.firstElementChild;
|
||||
|
||||
tmpRoot.removeChild(element);
|
||||
|
||||
if (append) {
|
||||
appendElements([element]);
|
||||
}
|
||||
|
||||
return element as HTMLElement;
|
||||
}
|
||||
|
||||
/** Appends elements to the testContainerElement. */
|
||||
function appendElements(elements: Element[]) {
|
||||
for (let e of elements) {
|
||||
testContainerElement.appendChild(e);
|
||||
}
|
||||
}
|
||||
|
||||
function runIf(condition: boolean, runFn: Function): () => void {
|
||||
return (...args: any[]) => {
|
||||
if (condition) {
|
||||
runFn.apply(this, args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {MdPlatform} from '../platform/platform';
|
||||
|
||||
/* The InteractivityChecker leans heavily on the ally.js accessibility utilities.
|
||||
* Methods like `isTabbable` are only covering specific edge-cases for the browsers which are
|
||||
* supported.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Utility for checking the interactivity of an element, such as whether is is focusable or
|
||||
* tabbable.
|
||||
*
|
||||
* NOTE: Currently does not capture any special element behaviors, browser quirks, or edge cases.
|
||||
* This is a basic/naive starting point onto which further behavior will be added.
|
||||
*
|
||||
* This class uses instance methods instead of static functions so that alternate implementations
|
||||
* can be injected.
|
||||
*
|
||||
* TODO(jelbourn): explore using ally.js directly for its significantly more robust
|
||||
* checks (need to evaluate payload size, performance, and compatibility with tree-shaking).
|
||||
*/
|
||||
@Injectable()
|
||||
export class InteractivityChecker {
|
||||
|
||||
constructor(private _platform: MdPlatform) {}
|
||||
|
||||
/** Gets whether an element is disabled. */
|
||||
isDisabled(element: HTMLElement) {
|
||||
// This does not capture some cases, such as a non-form control with a disabled attribute or
|
||||
@@ -30,18 +29,7 @@ export class InteractivityChecker {
|
||||
* being clipped by an `overflow: hidden` parent or being outside the viewport.
|
||||
*/
|
||||
isVisible(element: HTMLElement) {
|
||||
// There are additional special cases that this does not capture, but this will work for
|
||||
// the most common cases.
|
||||
|
||||
// Use logic from jQuery to check for `display: none`.
|
||||
// See https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js#L12
|
||||
if (!(element.offsetWidth || element.offsetHeight || element.getClientRects().length)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for css `visibility` property.
|
||||
// TODO(jelbourn): do any browsers we support return an empty string instead of 'visible'?
|
||||
return getComputedStyle(element).getPropertyValue('visibility') == 'visible';
|
||||
return hasGeometry(element) && getComputedStyle(element).visibility === 'visible';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +37,73 @@ export class InteractivityChecker {
|
||||
* Assumes that the element has already been checked with isFocusable.
|
||||
*/
|
||||
isTabbable(element: HTMLElement) {
|
||||
// Again, naive approach that does not capture many special cases and browser quirks.
|
||||
|
||||
let frameElement = getWindow(element).frameElement as HTMLElement;
|
||||
|
||||
if (frameElement) {
|
||||
|
||||
let frameType = frameElement && frameElement.nodeName.toLowerCase();
|
||||
|
||||
// Frame elements inherit their tabindex onto all child elements.
|
||||
if (getTabIndexValue(frameElement) === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Webkit and Blink consider anything inside of an <object> element as non-tabbable.
|
||||
if ((this._platform.BLINK || this._platform.WEBKIT) && frameType === 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Webkit and Blink disable tabbing to an element inside of an invisible frame.
|
||||
if ((this._platform.BLINK || this._platform.WEBKIT) && !this.isVisible(frameElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let nodeName = element.nodeName.toLowerCase();
|
||||
let tabIndexValue = getTabIndexValue(element);
|
||||
|
||||
if (element.hasAttribute('contenteditable')) {
|
||||
return tabIndexValue !== -1;
|
||||
}
|
||||
|
||||
if (nodeName === 'iframe') {
|
||||
// The frames may be tabbable depending on content, but it's not possibly to reliably
|
||||
// investigate the content of the frames.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nodeName === 'audio') {
|
||||
if (!element.hasAttribute('controls')) {
|
||||
// By default an <audio> element without the controls enabled is not tabbable.
|
||||
return false;
|
||||
} else if (this._platform.BLINK) {
|
||||
// In Blink <audio controls> elements are always tabbable.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeName === 'video') {
|
||||
if (!element.hasAttribute('controls') && this._platform.TRIDENT) {
|
||||
// In Trident a <video> element without the controls enabled is not tabbable.
|
||||
return false;
|
||||
} else if (this._platform.BLINK || this._platform.FIREFOX) {
|
||||
// In Chrome and Firefox <video controls> elements are always tabbable.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeName === 'object' && (this._platform.BLINK || this._platform.WEBKIT)) {
|
||||
// In all Blink and WebKit based browsers <object> elements are never tabbable.
|
||||
return false;
|
||||
}
|
||||
|
||||
// In iOS the browser only considers some specific elements as tabbable.
|
||||
if (this._platform.WEBKIT && this._platform.IOS && !isPotentiallyTabbableIOS(element)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return element.tabIndex >= 0;
|
||||
}
|
||||
|
||||
@@ -59,6 +113,14 @@ export class InteractivityChecker {
|
||||
// Again, naive approach that does not capture many edge cases and browser quirks.
|
||||
return isPotentiallyFocusable(element) && !this.isDisabled(element) && this.isVisible(element);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** Checks whether the specified element has any geometry / rectangles. */
|
||||
function hasGeometry(element: HTMLElement): boolean {
|
||||
// Use logic from jQuery to check for an invisible element.
|
||||
// See https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js#L12
|
||||
return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
|
||||
}
|
||||
|
||||
/** Gets whether an element's */
|
||||
@@ -106,6 +168,32 @@ function hasValidTabIndex(element: HTMLElement): boolean {
|
||||
return !!(tabIndex && !isNaN(parseInt(tabIndex, 10)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parsed tabindex from the element attributes instead of returning the
|
||||
* evaluated tabindex from the browsers defaults.
|
||||
*/
|
||||
function getTabIndexValue(element: HTMLElement): number {
|
||||
if (!hasValidTabIndex(element)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// See browser issue in Gecko https://bugzilla.mozilla.org/show_bug.cgi?id=1128054
|
||||
const tabIndex = parseInt(element.getAttribute('tabindex'), 10);
|
||||
|
||||
return isNaN(tabIndex) ? -1 : tabIndex;
|
||||
}
|
||||
|
||||
/** Checks whether the specified element is potentially tabbable on iOS */
|
||||
function isPotentiallyTabbableIOS(element: HTMLElement): boolean {
|
||||
let nodeName = element.nodeName.toLowerCase();
|
||||
let inputType = nodeName === 'input' && (element as HTMLInputElement).type;
|
||||
|
||||
return inputType === 'text'
|
||||
|| inputType === 'password'
|
||||
|| nodeName === 'select'
|
||||
|| nodeName === 'textarea';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether an element is potentially focusable without taking current visible/disabled state
|
||||
* into account.
|
||||
@@ -122,3 +210,7 @@ function isPotentiallyFocusable(element: HTMLElement): boolean {
|
||||
hasValidTabIndex(element);
|
||||
}
|
||||
|
||||
/** Gets the parent window of a DOM node with regards of being inside of an iframe. */
|
||||
function getWindow(node: HTMLElement): Window {
|
||||
return node.ownerDocument.defaultView || window;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ export {DomPortalHost} from './portal/dom-portal-host';
|
||||
// Projection
|
||||
export * from './projection/projection';
|
||||
|
||||
// Platform
|
||||
export * from './platform/platform';
|
||||
|
||||
// Overlay
|
||||
export {Overlay, OVERLAY_PROVIDERS} from './overlay/overlay';
|
||||
export {OverlayContainer} from './overlay/overlay-container';
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import {Injectable, NgModule, ModuleWithProviders} from '@angular/core';
|
||||
|
||||
declare const window: any;
|
||||
|
||||
// Whether the current platform supports the V8 Break Iterator. The V8 check
|
||||
// is necessary to detect all Blink based browsers.
|
||||
const hasV8BreakIterator = (window.Intl && (window.Intl as any).v8BreakIterator);
|
||||
|
||||
/**
|
||||
* Service to detect the current platform by comparing the userAgent strings and
|
||||
* checking browser-specific global properties.
|
||||
*/
|
||||
@Injectable()
|
||||
export class MdPlatform {
|
||||
|
||||
/** Layout Engines */
|
||||
EDGE = /(edge)/i.test(navigator.userAgent);
|
||||
TRIDENT = /(msie|trident)/i.test(navigator.userAgent);
|
||||
|
||||
// EdgeHTML and Trident mock Blink specific things and need to excluded from this check.
|
||||
BLINK = !!(window.chrome || hasV8BreakIterator) && !!CSS && !this.EDGE && !this.TRIDENT;
|
||||
|
||||
// Webkit is part of the userAgent in EdgeHTML Blink and Trident, so we need to
|
||||
// ensure that Webkit runs standalone and is not use as another engines base.
|
||||
WEBKIT = /AppleWebKit/i.test(navigator.userAgent) && !this.BLINK && !this.EDGE && !this.TRIDENT;
|
||||
|
||||
/** Browsers and Platform Types */
|
||||
IOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
|
||||
// It's difficult to detect the plain Gecko engine, because most of the browsers identify
|
||||
// them self as Gecko-like browsers and modify the userAgent's according to that.
|
||||
// Since we only cover one explicit Firefox case, we can simply check for Firefox
|
||||
// instead of having an unstable check for Gecko.
|
||||
FIREFOX = /(firefox|minefield)/i.test(navigator.userAgent);
|
||||
|
||||
// Trident on mobile adds the android platform to the userAgent to trick detections.
|
||||
ANDROID = /android/i.test(navigator.userAgent) && !this.TRIDENT;
|
||||
|
||||
}
|
||||
|
||||
@NgModule({})
|
||||
export class PlatformModule {
|
||||
static forRoot(): ModuleWithProviders {
|
||||
return {
|
||||
ngModule: PlatformModule,
|
||||
providers: [MdPlatform],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,8 @@ import {MdDialogConfig} from './dialog-config';
|
||||
import {MdDialogRef} from './dialog-ref';
|
||||
import {DialogInjector} from './dialog-injector';
|
||||
import {MdDialogContainer} from './dialog-container';
|
||||
import {A11yModule, InteractivityChecker} from '../core';
|
||||
import {A11yModule, InteractivityChecker, MdPlatform} from '../core';
|
||||
import {extendObject} from '../core/util/object-extend';
|
||||
|
||||
export {MdDialogConfig} from './dialog-config';
|
||||
export {MdDialogRef} from './dialog-ref';
|
||||
|
||||
@@ -161,7 +160,7 @@ export class MdDialogModule {
|
||||
static forRoot(): ModuleWithProviders {
|
||||
return {
|
||||
ngModule: MdDialogModule,
|
||||
providers: [MdDialog, OVERLAY_PROVIDERS, InteractivityChecker],
|
||||
providers: [MdDialog, OVERLAY_PROVIDERS, InteractivityChecker, MdPlatform],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {MdToolbarModule} from './toolbar/index';
|
||||
import {MdTooltipModule} from './tooltip/index';
|
||||
import {MdMenuModule} from './menu/index';
|
||||
import {MdDialogModule} from './dialog/index';
|
||||
import {PlatformModule} from './core/platform/platform';
|
||||
|
||||
|
||||
const MATERIAL_MODULES = [
|
||||
@@ -62,6 +63,7 @@ const MATERIAL_MODULES = [
|
||||
PortalModule,
|
||||
RtlModule,
|
||||
A11yModule,
|
||||
PlatformModule,
|
||||
ProjectionModule,
|
||||
StyleCompatibilityModule,
|
||||
];
|
||||
@@ -97,6 +99,7 @@ const MATERIAL_MODULES = [
|
||||
MdSlideToggleModule.forRoot(),
|
||||
MdSnackBarModule.forRoot(),
|
||||
MdTooltipModule.forRoot(),
|
||||
PlatformModule.forRoot(),
|
||||
OverlayModule.forRoot(),
|
||||
StyleCompatibilityModule.forRoot(),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user