|
4 | 4 | * will create its own instance of this array and the app's IDs |
5 | 5 | * will not be unique. |
6 | 6 | */ |
7 | | -var nextUniqueId = 0, isIos, isAndroid; |
| 7 | +var nextUniqueId = 0, isIos, isAndroid, isFirefox; |
8 | 8 |
|
9 | 9 | // Support material-tools builds. |
10 | 10 | if (window.navigator) { |
11 | 11 | var userAgent = window.navigator.userAgent || window.navigator.vendor || window.opera; |
12 | 12 | isIos = userAgent.match(/ipad|iphone|ipod/i); |
13 | 13 | isAndroid = userAgent.match(/android/i); |
| 14 | + isFirefox = userAgent.match(/(firefox|minefield)/i); |
14 | 15 | } |
15 | 16 |
|
16 | 17 | /** |
@@ -1028,6 +1029,332 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in |
1028 | 1029 | sanitize: function(term) { |
1029 | 1030 | if (!term) return term; |
1030 | 1031 | return term.replace(/[\\^$*+?.()|{}[]/g, '\\$&'); |
| 1032 | + }, |
| 1033 | + |
| 1034 | + /********************************************************************************************** |
| 1035 | + * The following functions were sourced from |
| 1036 | + * https://github.com/angular/components/blob/3c37e4b1c1cb74a3d0a90d173240fc730d21d9d4/src/cdk/a11y/interactivity-checker/interactivity-checker.ts |
| 1037 | + **********************************************************************************************/ |
| 1038 | + |
| 1039 | + /** |
| 1040 | + * Gets whether an element is disabled. |
| 1041 | + * @param {HTMLElement} element Element to be checked. |
| 1042 | + * @returns {boolean} Whether the element is disabled. |
| 1043 | + */ |
| 1044 | + isDisabled: function(element) { |
| 1045 | + // This does not capture some cases, such as a non-form control with a disabled attribute or |
| 1046 | + // a form control inside of a disabled form, but should capture the most common cases. |
| 1047 | + return element.hasAttribute('disabled'); |
| 1048 | + }, |
| 1049 | + |
| 1050 | + /** |
| 1051 | + * Gets whether an element is visible for the purposes of interactivity. |
| 1052 | + * |
| 1053 | + * This will capture states like `display: none` and `visibility: hidden`, but not things like |
| 1054 | + * being clipped by an `overflow: hidden` parent or being outside the viewport. |
| 1055 | + * |
| 1056 | + * @param {HTMLElement} element |
| 1057 | + * @returns {boolean} Whether the element is visible. |
| 1058 | + */ |
| 1059 | + isVisible: function(element) { |
| 1060 | + return $mdUtil.hasGeometry(element) && getComputedStyle(element).visibility === 'visible'; |
| 1061 | + }, |
| 1062 | + |
| 1063 | + /** |
| 1064 | + * Gets whether an element can be reached via Tab key. |
| 1065 | + * Assumes that the element has already been checked with isFocusable. |
| 1066 | + * @param {HTMLElement} element Element to be checked. |
| 1067 | + * @returns {boolean} Whether the element is tabbable. |
| 1068 | + */ |
| 1069 | + isTabbable: function(element) { |
| 1070 | + var frameElement = $mdUtil.getFrameElement($mdUtil.getWindow(element)); |
| 1071 | + |
| 1072 | + if (frameElement) { |
| 1073 | + // Frame elements inherit their tabindex onto all child elements. |
| 1074 | + if ($mdUtil.getTabIndexValue(frameElement) === -1) { |
| 1075 | + return false; |
| 1076 | + } |
| 1077 | + |
| 1078 | + // Browsers disable tabbing to an element inside of an invisible frame. |
| 1079 | + if (!$mdUtil.isVisible(frameElement)) { |
| 1080 | + return false; |
| 1081 | + } |
| 1082 | + } |
| 1083 | + |
| 1084 | + var nodeName = element.nodeName.toLowerCase(); |
| 1085 | + var tabIndexValue = $mdUtil.getTabIndexValue(element); |
| 1086 | + |
| 1087 | + if (element.hasAttribute('contenteditable')) { |
| 1088 | + return tabIndexValue !== -1; |
| 1089 | + } |
| 1090 | + |
| 1091 | + if (nodeName === 'iframe' || nodeName === 'object') { |
| 1092 | + // The frame or object's content may be tabbable depending on the content, but it's |
| 1093 | + // not possibly to reliably detect the content of the frames. We always consider such |
| 1094 | + // elements as non-tabbable. |
| 1095 | + return false; |
| 1096 | + } |
| 1097 | + |
| 1098 | + // In iOS, the browser only considers some specific elements as tabbable. |
| 1099 | + if (isIos && !$mdUtil.isPotentiallyTabbableIOS(element)) { |
| 1100 | + return false; |
| 1101 | + } |
| 1102 | + |
| 1103 | + if (nodeName === 'audio') { |
| 1104 | + // Audio elements without controls enabled are never tabbable, regardless |
| 1105 | + // of the tabindex attribute explicitly being set. |
| 1106 | + if (!element.hasAttribute('controls')) { |
| 1107 | + return false; |
| 1108 | + } |
| 1109 | + // Audio elements with controls are by default tabbable unless the |
| 1110 | + // tabindex attribute is set to `-1` explicitly. |
| 1111 | + return tabIndexValue !== -1; |
| 1112 | + } |
| 1113 | + |
| 1114 | + if (nodeName === 'video') { |
| 1115 | + // For all video elements, if the tabindex attribute is set to `-1`, the video |
| 1116 | + // is not tabbable. Note: We cannot rely on the default `HTMLElement.tabIndex` |
| 1117 | + // property as that one is set to `-1` in Chrome, Edge and Safari v13.1. The |
| 1118 | + // tabindex attribute is the source of truth here. |
| 1119 | + if (tabIndexValue === -1) { |
| 1120 | + return false; |
| 1121 | + } |
| 1122 | + // If the tabindex is explicitly set, and not `-1` (as per check before), the |
| 1123 | + // video element is always tabbable (regardless of whether it has controls or not). |
| 1124 | + if (tabIndexValue !== null) { |
| 1125 | + return true; |
| 1126 | + } |
| 1127 | + // Otherwise (when no explicit tabindex is set), a video is only tabbable if it |
| 1128 | + // has controls enabled. Firefox is special as videos are always tabbable regardless |
| 1129 | + // of whether there are controls or not. |
| 1130 | + return isFirefox || element.hasAttribute('controls'); |
| 1131 | + } |
| 1132 | + |
| 1133 | + return element.tabIndex >= 0; |
| 1134 | + }, |
| 1135 | + |
| 1136 | + /** |
| 1137 | + * Gets whether an element can be focused by the user. |
| 1138 | + * @param {HTMLElement} element Element to be checked. |
| 1139 | + * @returns {boolean} Whether the element is focusable. |
| 1140 | + */ |
| 1141 | + isFocusable: function(element) { |
| 1142 | + // Perform checks in order of left to most expensive. |
| 1143 | + // Again, naive approach that does not capture many edge cases and browser quirks. |
| 1144 | + return $mdUtil.isPotentiallyFocusable(element) && !$mdUtil.isDisabled(element) && |
| 1145 | + $mdUtil.isVisible(element); |
| 1146 | + }, |
| 1147 | + |
| 1148 | + /** |
| 1149 | + * Gets whether an element is potentially focusable without taking current visible/disabled |
| 1150 | + * state into account. |
| 1151 | + * @param {HTMLElement} element |
| 1152 | + * @returns {boolean} |
| 1153 | + */ |
| 1154 | + isPotentiallyFocusable: function(element) { |
| 1155 | + // Inputs are potentially focusable *unless* they're type="hidden". |
| 1156 | + if ($mdUtil.isHiddenInput(element)) { |
| 1157 | + return false; |
| 1158 | + } |
| 1159 | + |
| 1160 | + return $mdUtil.isNativeFormElement(element) || |
| 1161 | + $mdUtil.isAnchorWithHref(element) || |
| 1162 | + element.hasAttribute('contenteditable') || |
| 1163 | + $mdUtil.hasValidTabIndex(element); |
| 1164 | + }, |
| 1165 | + |
| 1166 | + /** |
| 1167 | + * Checks whether the specified element is potentially tabbable on iOS. |
| 1168 | + * @param {HTMLElement} element |
| 1169 | + * @returns {boolean} |
| 1170 | + */ |
| 1171 | + isPotentiallyTabbableIOS: function(element) { |
| 1172 | + var nodeName = element.nodeName.toLowerCase(); |
| 1173 | + var inputType = nodeName === 'input' && element.type; |
| 1174 | + |
| 1175 | + return inputType === 'text' |
| 1176 | + || inputType === 'password' |
| 1177 | + || nodeName === 'select' |
| 1178 | + || nodeName === 'textarea'; |
| 1179 | + }, |
| 1180 | + |
| 1181 | + /** |
| 1182 | + * Returns the parsed tabindex from the element attributes instead of returning the |
| 1183 | + * evaluated tabindex from the browsers defaults. |
| 1184 | + * @param {HTMLElement} element |
| 1185 | + * @returns {null|number} |
| 1186 | + */ |
| 1187 | + getTabIndexValue: function(element) { |
| 1188 | + if (!$mdUtil.hasValidTabIndex(element)) { |
| 1189 | + return null; |
| 1190 | + } |
| 1191 | + |
| 1192 | + // See browser issue in Gecko https://bugzilla.mozilla.org/show_bug.cgi?id=1128054 |
| 1193 | + var tabIndex = parseInt(element.getAttribute('tabindex') || '', 10); |
| 1194 | + |
| 1195 | + return isNaN(tabIndex) ? -1 : tabIndex; |
| 1196 | + }, |
| 1197 | + |
| 1198 | + /** |
| 1199 | + * Gets whether an element has a valid tabindex. |
| 1200 | + * @param {HTMLElement} element |
| 1201 | + * @returns {boolean} |
| 1202 | + */ |
| 1203 | + hasValidTabIndex: function(element) { |
| 1204 | + if (!element.hasAttribute('tabindex') || element.tabIndex === undefined) { |
| 1205 | + return false; |
| 1206 | + } |
| 1207 | + |
| 1208 | + var tabIndex = element.getAttribute('tabindex'); |
| 1209 | + |
| 1210 | + // IE11 parses tabindex="" as the value "-32768" |
| 1211 | + if (tabIndex == '-32768') { |
| 1212 | + return false; |
| 1213 | + } |
| 1214 | + |
| 1215 | + return !!(tabIndex && !isNaN(parseInt(tabIndex, 10))); |
| 1216 | + }, |
| 1217 | + |
| 1218 | + /** |
| 1219 | + * Checks whether the specified element has any geometry / rectangles. |
| 1220 | + * @param {HTMLElement} element |
| 1221 | + * @returns {boolean} |
| 1222 | + */ |
| 1223 | + hasGeometry: function(element) { |
| 1224 | + // Use logic from jQuery to check for an invisible element. |
| 1225 | + // See https://github.com/jquery/jquery/blob/8969732518470a7f8e654d5bc5be0b0076cb0b87/src/css/hiddenVisibleSelectors.js#L9 |
| 1226 | + return !!(element.offsetWidth || element.offsetHeight || |
| 1227 | + (typeof element.getClientRects === 'function' && element.getClientRects().length)); |
| 1228 | + }, |
| 1229 | + |
| 1230 | + /** |
| 1231 | + * Returns the frame element from a window object. Since browsers like MS Edge throw errors if |
| 1232 | + * the frameElement property is being accessed from a different host address, this property |
| 1233 | + * should be accessed carefully. |
| 1234 | + * @param {Window} window |
| 1235 | + * @returns {null|HTMLElement} |
| 1236 | + */ |
| 1237 | + getFrameElement: function(window) { |
| 1238 | + try { |
| 1239 | + return window.frameElement; |
| 1240 | + } catch (error) { |
| 1241 | + return null; |
| 1242 | + } |
| 1243 | + }, |
| 1244 | + |
| 1245 | + /** |
| 1246 | + * Gets the parent window of a DOM node with regards of being inside of an iframe. |
| 1247 | + * @param {HTMLElement} node |
| 1248 | + * @returns {Window} |
| 1249 | + */ |
| 1250 | + getWindow: function(node) { |
| 1251 | + // ownerDocument is null if `node` itself *is* a document. |
| 1252 | + return node.ownerDocument && node.ownerDocument.defaultView || window; |
| 1253 | + }, |
| 1254 | + |
| 1255 | + /** |
| 1256 | + * Gets whether an element's |
| 1257 | + * @param {Node} element |
| 1258 | + * @returns {boolean} |
| 1259 | + */ |
| 1260 | + isNativeFormElement: function(element) { |
| 1261 | + var nodeName = element.nodeName.toLowerCase(); |
| 1262 | + return nodeName === 'input' || |
| 1263 | + nodeName === 'select' || |
| 1264 | + nodeName === 'button' || |
| 1265 | + nodeName === 'textarea'; |
| 1266 | + }, |
| 1267 | + |
| 1268 | + /** |
| 1269 | + * Gets whether an element is an `<input type="hidden">`. |
| 1270 | + * @param {HTMLElement} element |
| 1271 | + * @returns {boolean} |
| 1272 | + */ |
| 1273 | + isHiddenInput: function(element) { |
| 1274 | + return $mdUtil.isInputElement(element) && element.type == 'hidden'; |
| 1275 | + }, |
| 1276 | + |
| 1277 | + /** |
| 1278 | + * Gets whether an element is an anchor that has an href attribute. |
| 1279 | + * @param {HTMLElement} element |
| 1280 | + * @returns {boolean} |
| 1281 | + */ |
| 1282 | + isAnchorWithHref: function(element) { |
| 1283 | + return $mdUtil.isAnchorElement(element) && element.hasAttribute('href'); |
| 1284 | + }, |
| 1285 | + |
| 1286 | + /** |
| 1287 | + * Gets whether an element is an input element. |
| 1288 | + * @param {HTMLElement} element |
| 1289 | + * @returns {boolean} |
| 1290 | + */ |
| 1291 | + isInputElement: function(element) { |
| 1292 | + return element.nodeName.toLowerCase() == 'input'; |
| 1293 | + }, |
| 1294 | + |
| 1295 | + /** |
| 1296 | + * Gets whether an element is an anchor element. |
| 1297 | + * @param {HTMLElement} element |
| 1298 | + * @returns {boolean} |
| 1299 | + */ |
| 1300 | + isAnchorElement: function(element) { |
| 1301 | + return element.nodeName.toLowerCase() == 'a'; |
| 1302 | + }, |
| 1303 | + |
| 1304 | + /********************************************************************************************** |
| 1305 | + * The following two functions were sourced from |
| 1306 | + * https://github.com/angular/components/blob/3c37e4b1c1cb74a3d0a90d173240fc730d21d9d4/src/cdk/a11y/focus-trap/focus-trap.ts#L268-L311 |
| 1307 | + **********************************************************************************************/ |
| 1308 | + |
| 1309 | + /** |
| 1310 | + * Get the first tabbable element from a DOM subtree (inclusive). |
| 1311 | + * @param {HTMLElement} root |
| 1312 | + * @returns {HTMLElement|null} |
| 1313 | + */ |
| 1314 | + getFirstTabbableElement: function(root) { |
| 1315 | + if ($mdUtil.isFocusable(root) && $mdUtil.isTabbable(root)) { |
| 1316 | + return root; |
| 1317 | + } |
| 1318 | + |
| 1319 | + // Iterate in DOM order. Note that IE doesn't have `children` for SVG so we fall |
| 1320 | + // back to `childNodes` which includes text nodes, comments etc. |
| 1321 | + var children = root.children || root.childNodes; |
| 1322 | + |
| 1323 | + for (var i = 0; i < children.length; i++) { |
| 1324 | + var tabbableChild = children[i].nodeType === $document[0].ELEMENT_NODE ? |
| 1325 | + $mdUtil.getFirstTabbableElement(children[i]) : null; |
| 1326 | + |
| 1327 | + if (tabbableChild) { |
| 1328 | + return tabbableChild; |
| 1329 | + } |
| 1330 | + } |
| 1331 | + |
| 1332 | + return null; |
| 1333 | + }, |
| 1334 | + |
| 1335 | + /** |
| 1336 | + * Get the last tabbable element from a DOM subtree (inclusive). |
| 1337 | + * @param {HTMLElement} root |
| 1338 | + * @returns {HTMLElement|null} |
| 1339 | + */ |
| 1340 | + getLastTabbableElement: function(root) { |
| 1341 | + if ($mdUtil.isFocusable(root) && $mdUtil.isTabbable(root)) { |
| 1342 | + return root; |
| 1343 | + } |
| 1344 | + |
| 1345 | + // Iterate in reverse DOM order. |
| 1346 | + var children = root.children || root.childNodes; |
| 1347 | + |
| 1348 | + for (var i = children.length - 1; i >= 0; i--) { |
| 1349 | + var tabbableChild = children[i].nodeType === $document[0].ELEMENT_NODE ? |
| 1350 | + $mdUtil.getLastTabbableElement(children[i]) : null; |
| 1351 | + |
| 1352 | + if (tabbableChild) { |
| 1353 | + return tabbableChild; |
| 1354 | + } |
| 1355 | + } |
| 1356 | + |
| 1357 | + return null; |
1031 | 1358 | } |
1032 | 1359 | }; |
1033 | 1360 |
|
|
0 commit comments