1 | import $ from './jquery';
|
2 | import globalize from './internal/globalize';
|
3 | import keyCodes from './key-code';
|
4 | import {getTrigger} from './trigger';
|
5 |
|
6 | (function initSelectors () {
|
7 | |
8 |
|
9 |
|
10 |
|
11 |
|
12 | function visible (element) {
|
13 | return ($.css(element, 'visibility') === 'visible') && $(element).is(':visible');
|
14 | }
|
15 |
|
16 | function focusable (element, isTabIndexNotNaN) {
|
17 | var nodeName = element.nodeName.toLowerCase();
|
18 |
|
19 | if (nodeName === 'aui-select') {
|
20 | return true;
|
21 | }
|
22 |
|
23 | if (nodeName === 'area') {
|
24 | var map = element.parentNode;
|
25 | var mapName = map.name;
|
26 | var imageMap = $('img[usemap=#' + mapName + ']').get();
|
27 |
|
28 | if (!element.href || !mapName || map.nodeName.toLowerCase() !== 'map') {
|
29 | return false;
|
30 | }
|
31 | return imageMap && visible(imageMap);
|
32 | }
|
33 | var isFormElement = /input|select|textarea|button|object|iframe/.test(nodeName);
|
34 | var isAnchor = nodeName === 'a';
|
35 | var isAnchorTabbable = (element.href || isTabIndexNotNaN);
|
36 |
|
37 | return (
|
38 | isFormElement ? !element.disabled :
|
39 | (isAnchor ? isAnchorTabbable : isTabIndexNotNaN)
|
40 | ) && visible(element);
|
41 | }
|
42 |
|
43 | function tabbable (element) {
|
44 | var tabIndex = $.attr(element, 'tabindex');
|
45 | var isTabIndexNaN = isNaN(tabIndex);
|
46 | var hasTabIndex = (isTabIndexNaN || tabIndex >= 0);
|
47 |
|
48 | return hasTabIndex && focusable(element, !isTabIndexNaN);
|
49 | }
|
50 |
|
51 | $.extend($.expr.pseudos, {
|
52 | 'aui-focusable': element => focusable(element, !isNaN($.attr(element, 'tabindex'))),
|
53 | 'aui-tabbable': tabbable
|
54 | });
|
55 | }());
|
56 |
|
57 | var RESTORE_FOCUS_DATA_KEY = '_aui-focus-restore';
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 | function setLastFocus ($el, lastFocussedEl = document.activeElement) {
|
66 | $el.data(RESTORE_FOCUS_DATA_KEY, lastFocussedEl);
|
67 | }
|
68 |
|
69 | function getLastFocus ($el) {
|
70 | return $($el.data(RESTORE_FOCUS_DATA_KEY));
|
71 | }
|
72 |
|
73 | function elementTrapsFocus ($el) {
|
74 | return $el.is('.aui-dialog2');
|
75 | }
|
76 |
|
77 | function FocusManager() {
|
78 | this._focusTrapStack = [];
|
79 | this._handler;
|
80 | }
|
81 | FocusManager.defaultFocusSelector = ':aui-tabbable';
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 | FocusManager.prototype.enter = function ($el, $lastFocused) {
|
88 | setLastFocus($el, $lastFocused);
|
89 |
|
90 |
|
91 | if ($el.attr('data-aui-focus') !== 'false') {
|
92 | var focusSelector = $el.attr('data-aui-focus-selector') || FocusManager.defaultFocusSelector;
|
93 | var $focusEl = $el.is(focusSelector) ? $el : $el.find(focusSelector);
|
94 | $focusEl.first().trigger('focus');
|
95 | }
|
96 |
|
97 | if (elementTrapsFocus($el)) {
|
98 | this._focusTrapStack.push($el);
|
99 | if (!this._handler) {
|
100 | this._handler = focusTrapHandler.bind(undefined, this._focusTrapStack);
|
101 | $(document).on('keydown.aui-focus-manager', this._handler);
|
102 | }
|
103 | }
|
104 | };
|
105 |
|
106 | FocusManager.prototype.exit = function ($el) {
|
107 | if (elementTrapsFocus($el)) {
|
108 | this._focusTrapStack.pop();
|
109 | if (!this._focusTrapStack.length) {
|
110 | $(document).off('.aui-focus-manager', this._handler);
|
111 | delete this._handler;
|
112 | }
|
113 | }
|
114 |
|
115 |
|
116 |
|
117 | var activeElement = document.activeElement;
|
118 | if ($el[0] === activeElement || $el.has(activeElement).length) {
|
119 | $(activeElement).trigger('blur');
|
120 | var $restoreFocus = getLastFocus($el);
|
121 | if ($restoreFocus.length) {
|
122 | $el.removeData(RESTORE_FOCUS_DATA_KEY);
|
123 | $restoreFocus.trigger('focus');
|
124 | }
|
125 | }
|
126 | };
|
127 |
|
128 | function focusTrapHandler(focusTrapStack, event) {
|
129 | if (focusTrapStack.length === 0) {
|
130 | return;
|
131 | }
|
132 |
|
133 | if (event.keyCode !== keyCodes.TAB) {
|
134 | return;
|
135 | }
|
136 |
|
137 | const backwards = event.shiftKey;
|
138 | const offset = backwards ? -1 : 1;
|
139 |
|
140 | |
141 |
|
142 |
|
143 |
|
144 | const focusOrigin = event.target;
|
145 |
|
146 | const $focusTrapElement = focusTrapStack[focusTrapStack.length - 1];
|
147 | const $tabbableElements = $focusTrapElement.find(':aui-tabbable');
|
148 |
|
149 |
|
150 | if (!$tabbableElements.length) {
|
151 | return;
|
152 | }
|
153 |
|
154 | const originIdx = $tabbableElements.index(focusOrigin);
|
155 | let newFocusIdx = -1;
|
156 |
|
157 | if (originIdx > -1) {
|
158 |
|
159 |
|
160 | newFocusIdx = originIdx;
|
161 | } else {
|
162 |
|
163 |
|
164 |
|
165 |
|
166 | let $controlledElementWithFocus;
|
167 |
|
168 |
|
169 | $controlledElementWithFocus = $(focusOrigin).closest('.aui-layer');
|
170 |
|
171 | if (!$controlledElementWithFocus.length) {
|
172 |
|
173 |
|
174 | const $controllingElements = $focusTrapElement.find('[aria-controls]');
|
175 | const $controlledElements = $controllingElements.map(function () {
|
176 | return document.getElementById(this.getAttribute('aria-controls'));
|
177 | });
|
178 |
|
179 |
|
180 | $controlledElementWithFocus = $controlledElements.has(focusOrigin);
|
181 | }
|
182 |
|
183 | if ($controlledElementWithFocus.length) {
|
184 |
|
185 | const $subTabbable = $controlledElementWithFocus.find(':aui-tabbable');
|
186 | const subOriginIdx = $subTabbable.index(focusOrigin);
|
187 | const subMove = subOriginIdx + offset;
|
188 | if (subMove < 0 || subMove >= $subTabbable.length) {
|
189 |
|
190 |
|
191 | const triggerEl = getTrigger($controlledElementWithFocus.get(0));
|
192 | newFocusIdx = $tabbableElements.index(triggerEl);
|
193 | } else {
|
194 |
|
195 | return;
|
196 | }
|
197 | }
|
198 | }
|
199 |
|
200 | if (newFocusIdx > -1) {
|
201 |
|
202 | newFocusIdx = (newFocusIdx + offset) % $tabbableElements.length;
|
203 | } else {
|
204 |
|
205 | newFocusIdx = 0;
|
206 | }
|
207 |
|
208 | if ($tabbableElements.get(newFocusIdx).nodeName !== 'IFRAME') {
|
209 | $tabbableElements.eq(newFocusIdx).trigger('focus');
|
210 | event.preventDefault();
|
211 | }
|
212 | }
|
213 |
|
214 |
|
215 |
|
216 |
|
217 | let instance;
|
218 | function getFocusManager() {
|
219 | if (!instance) {
|
220 | instance = new FocusManager();
|
221 | }
|
222 | return instance;
|
223 | }
|
224 | getFocusManager.global = getFocusManager();
|
225 |
|
226 | globalize('FocusManager', getFocusManager);
|
227 |
|
228 | export default getFocusManager;
|