UNPKG

9.88 kBJavaScriptView Raw
1/**
2 * @license
3 * Copyright Google LLC All Rights Reserved.
4 *
5 * Use of this source code is governed by an MIT-style license that can be
6 * found in the LICENSE file at https://angular.io/license
7 */
8/* eslint-disable */
9import { Platform } from './platform';
10import { Injectable } from '@angular/core';
11import * as i0 from "@angular/core";
12import * as i1 from "./platform";
13/**
14 * Configuration for the isFocusable method.
15 */
16export class IsFocusableConfig {
17 constructor() {
18 /**
19 * Whether to count an element as focusable even if it is not currently visible.
20 */
21 this.ignoreVisibility = false;
22 }
23}
24// The InteractivityChecker leans heavily on the ally.js accessibility utilities.
25// Methods like `isTabbable` are only covering specific edge-cases for the browsers which are
26// supported.
27/**
28 * Utility for checking the interactivity of an element, such as whether is is focusable or
29 * tabbable.
30 */
31export class InteractivityChecker {
32 constructor(_platform) {
33 this._platform = _platform;
34 }
35 /**
36 * Gets whether an element is disabled.
37 *
38 * @param element Element to be checked.
39 * @returns Whether the element is disabled.
40 */
41 isDisabled(element) {
42 // This does not capture some cases, such as a non-form control with a disabled attribute or
43 // a form control inside of a disabled form, but should capture the most common cases.
44 return element.hasAttribute('disabled');
45 }
46 /**
47 * Gets whether an element is visible for the purposes of interactivity.
48 *
49 * This will capture states like `display: none` and `visibility: hidden`, but not things like
50 * being clipped by an `overflow: hidden` parent or being outside the viewport.
51 *
52 * @returns Whether the element is visible.
53 */
54 isVisible(element) {
55 return hasGeometry(element) && getComputedStyle(element).visibility === 'visible';
56 }
57 /**
58 * Gets whether an element can be reached via Tab key.
59 * Assumes that the element has already been checked with isFocusable.
60 *
61 * @param element Element to be checked.
62 * @returns Whether the element is tabbable.
63 */
64 isTabbable(element) {
65 // Nothing is tabbable on the server 😎
66 if (!this._platform.isBrowser) {
67 return false;
68 }
69 const frameElement = getFrameElement(getWindow(element));
70 if (frameElement) {
71 // Frame elements inherit their tabindex onto all child elements.
72 if (getTabIndexValue(frameElement) === -1) {
73 return false;
74 }
75 // Browsers disable tabbing to an element inside of an invisible frame.
76 if (!this.isVisible(frameElement)) {
77 return false;
78 }
79 }
80 let nodeName = element.nodeName.toLowerCase();
81 let tabIndexValue = getTabIndexValue(element);
82 if (element.hasAttribute('contenteditable')) {
83 return tabIndexValue !== -1;
84 }
85 if (nodeName === 'iframe' || nodeName === 'object') {
86 // The frame or object's content may be tabbable depending on the content, but it's
87 // not possibly to reliably detect the content of the frames. We always consider such
88 // elements as non-tabbable.
89 return false;
90 }
91 // In iOS, the browser only considers some specific elements as tabbable.
92 if (this._platform.WEBKIT && this._platform.IOS && !isPotentiallyTabbableIOS(element)) {
93 return false;
94 }
95 if (nodeName === 'audio') {
96 // Audio elements without controls enabled are never tabbable, regardless
97 // of the tabindex attribute explicitly being set.
98 if (!element.hasAttribute('controls')) {
99 return false;
100 }
101 // Audio elements with controls are by default tabbable unless the
102 // tabindex attribute is set to `-1` explicitly.
103 return tabIndexValue !== -1;
104 }
105 if (nodeName === 'video') {
106 // For all video elements, if the tabindex attribute is set to `-1`, the video
107 // is not tabbable. Note: We cannot rely on the default `HTMLElement.tabIndex`
108 // property as that one is set to `-1` in Chrome, Edge and Safari v13.1. The
109 // tabindex attribute is the source of truth here.
110 if (tabIndexValue === -1) {
111 return false;
112 }
113 // If the tabindex is explicitly set, and not `-1` (as per check before), the
114 // video element is always tabbable (regardless of whether it has controls or not).
115 if (tabIndexValue !== null) {
116 return true;
117 }
118 // Otherwise (when no explicit tabindex is set), a video is only tabbable if it
119 // has controls enabled. Firefox is special as videos are always tabbable regardless
120 // of whether there are controls or not.
121 return this._platform.FIREFOX || element.hasAttribute('controls');
122 }
123 return element.tabIndex >= 0;
124 }
125 /**
126 * Gets whether an element can be focused by the user.
127 *
128 * @param element Element to be checked.
129 * @param config The config object with options to customize this method's behavior
130 * @returns Whether the element is focusable.
131 */
132 isFocusable(element, config) {
133 // Perform checks in order of left to most expensive.
134 // Again, naive approach that does not capture many edge cases and browser quirks.
135 return isPotentiallyFocusable(element) && !this.isDisabled(element) &&
136 ((config === null || config === void 0 ? void 0 : config.ignoreVisibility) || this.isVisible(element));
137 }
138}
139InteractivityChecker.ɵprov = i0.ɵɵdefineInjectable({ factory: function InteractivityChecker_Factory() { return new InteractivityChecker(i0.ɵɵinject(i1.Platform)); }, token: InteractivityChecker, providedIn: "root" });
140InteractivityChecker.decorators = [
141 { type: Injectable, args: [{ providedIn: 'root' },] }
142];
143InteractivityChecker.ctorParameters = () => [
144 { type: Platform }
145];
146/**
147 * Returns the frame element from a window object. Since browsers like MS Edge throw errors if
148 * the frameElement property is being accessed from a different host address, this property
149 * should be accessed carefully.
150 */
151function getFrameElement(window) {
152 try {
153 return window.frameElement;
154 }
155 catch (_a) {
156 return null;
157 }
158}
159/** Checks whether the specified element has any geometry / rectangles. */
160function hasGeometry(element) {
161 // Use logic from jQuery to check for an invisible element.
162 // See https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js#L12
163 return !!(element.offsetWidth || element.offsetHeight ||
164 (typeof element.getClientRects === 'function' && element.getClientRects().length));
165}
166/** Gets whether an element's */
167function isNativeFormElement(element) {
168 let nodeName = element.nodeName.toLowerCase();
169 return nodeName === 'input' ||
170 nodeName === 'select' ||
171 nodeName === 'button' ||
172 nodeName === 'textarea';
173}
174/** Gets whether an element is an `<input type="hidden">`. */
175function isHiddenInput(element) {
176 return isInputElement(element) && element.type == 'hidden';
177}
178/** Gets whether an element is an anchor that has an href attribute. */
179function isAnchorWithHref(element) {
180 return isAnchorElement(element) && element.hasAttribute('href');
181}
182/** Gets whether an element is an input element. */
183function isInputElement(element) {
184 return element.nodeName.toLowerCase() == 'input';
185}
186/** Gets whether an element is an anchor element. */
187function isAnchorElement(element) {
188 return element.nodeName.toLowerCase() == 'a';
189}
190/** Gets whether an element has a valid tabindex. */
191function hasValidTabIndex(element) {
192 if (!element.hasAttribute('tabindex') || element.tabIndex === undefined) {
193 return false;
194 }
195 let tabIndex = element.getAttribute('tabindex');
196 // IE11 parses tabindex="" as the value "-32768"
197 if (tabIndex == '-32768') {
198 return false;
199 }
200 return !!(tabIndex && !isNaN(parseInt(tabIndex, 10)));
201}
202/**
203 * Returns the parsed tabindex from the element attributes instead of returning the
204 * evaluated tabindex from the browsers defaults.
205 */
206function getTabIndexValue(element) {
207 if (!hasValidTabIndex(element)) {
208 return null;
209 }
210 // See browser issue in Gecko https://bugzilla.mozilla.org/show_bug.cgi?id=1128054
211 const tabIndex = parseInt(element.getAttribute('tabindex') || '', 10);
212 return isNaN(tabIndex) ? -1 : tabIndex;
213}
214/** Checks whether the specified element is potentially tabbable on iOS */
215function isPotentiallyTabbableIOS(element) {
216 let nodeName = element.nodeName.toLowerCase();
217 let inputType = nodeName === 'input' && element.type;
218 return inputType === 'text'
219 || inputType === 'password'
220 || nodeName === 'select'
221 || nodeName === 'textarea';
222}
223/**
224 * Gets whether an element is potentially focusable without taking current visible/disabled state
225 * into account.
226 */
227function isPotentiallyFocusable(element) {
228 // Inputs are potentially focusable *unless* they're type="hidden".
229 if (isHiddenInput(element)) {
230 return false;
231 }
232 return isNativeFormElement(element) ||
233 isAnchorWithHref(element) ||
234 element.hasAttribute('contenteditable') ||
235 hasValidTabIndex(element);
236}
237/** Gets the parent window of a DOM node with regards of being inside of an iframe. */
238function getWindow(node) {
239 // ownerDocument is null if `node` itself *is* a document.
240 return node.ownerDocument && node.ownerDocument.defaultView || window;
241}
242//# sourceMappingURL=interactivity-checker.js.map
\No newline at end of file