/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {Platform} from '@angular/cdk/platform'; import {Injectable} from '@angular/core'; // 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. */ @Injectable({providedIn: 'root'}) export class InteractivityChecker { constructor(private _platform: Platform) {} /** * Gets whether an element is disabled. * * @param element Element to be checked. * @returns Whether the element is disabled. */ isDisabled(element: HTMLElement): boolean { // This does not capture some cases, such as a non-form control with a disabled attribute or // a form control inside of a disabled form, but should capture the most common cases. return element.hasAttribute('disabled'); } /** * Gets whether an element is visible for the purposes of interactivity. * * This will capture states like `display: none` and `visibility: hidden`, but not things like * being clipped by an `overflow: hidden` parent or being outside the viewport. * * @returns Whether the element is visible. */ isVisible(element: HTMLElement): boolean { return hasGeometry(element) && getComputedStyle(element).visibility === 'visible'; } /** * Gets whether an element can be reached via Tab key. * Assumes that the element has already been checked with isFocusable. * * @param element Element to be checked. * @returns Whether the element is tabbable. */ isTabbable(element: HTMLElement): boolean { // Nothing is tabbable on the server 😎 if (!this._platform.isBrowser) { return false; } const frameElement = getFrameElement(getWindow(element)); if (frameElement) { const 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 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