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 */
|
9 | import { Platform } from './platform';
|
10 | import { Injectable } from '@angular/core';
|
11 | import * as i0 from "@angular/core";
|
12 | import * as i1 from "./platform";
|
13 | /**
|
14 | * Configuration for the isFocusable method.
|
15 | */
|
16 | export 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 | */
|
31 | export 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 | }
|
139 | InteractivityChecker.ɵprov = i0.ɵɵdefineInjectable({ factory: function InteractivityChecker_Factory() { return new InteractivityChecker(i0.ɵɵinject(i1.Platform)); }, token: InteractivityChecker, providedIn: "root" });
|
140 | InteractivityChecker.decorators = [
|
141 | { type: Injectable, args: [{ providedIn: 'root' },] }
|
142 | ];
|
143 | InteractivityChecker.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 | */
|
151 | function 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. */
|
160 | function 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 */
|
167 | function 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">`. */
|
175 | function isHiddenInput(element) {
|
176 | return isInputElement(element) && element.type == 'hidden';
|
177 | }
|
178 | /** Gets whether an element is an anchor that has an href attribute. */
|
179 | function isAnchorWithHref(element) {
|
180 | return isAnchorElement(element) && element.hasAttribute('href');
|
181 | }
|
182 | /** Gets whether an element is an input element. */
|
183 | function isInputElement(element) {
|
184 | return element.nodeName.toLowerCase() == 'input';
|
185 | }
|
186 | /** Gets whether an element is an anchor element. */
|
187 | function isAnchorElement(element) {
|
188 | return element.nodeName.toLowerCase() == 'a';
|
189 | }
|
190 | /** Gets whether an element has a valid tabindex. */
|
191 | function 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 | */
|
206 | function 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 */
|
215 | function 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 | */
|
227 | function 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. */
|
238 | function 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 |