/** * Prevents DOM focusability while modal windows are visible. */Ext.define('Ext.plugin.TabGuard', { extend: 'Ext.plugin.Abstract', alias: 'plugin.tabguard', /** * When set to `true`, two elements are added to the container's element. These are the * `{@link #tabGuardBeforeEl}` and `{@link #tabGuardAfterEl}`. * @cfg {Boolean} tabGuard * @private * @since 6.5.1 */ tabGuard: true, /** * @cfg {Number} [tabGuardBeforeIndex] Top tab guard tabIndex. Use this when * there are elements with tabIndex > 0 within the dialog. * @private * @since 6.5.1 */ /** * @cfg {Number} [tabGuardAfterIndex] Bottom tab guard tabIndex. Use this * when there are elements with tabIndex > 0 within the dialog. * @private * @since 6.5.1 */ /** * @property {Array} tabGuardTemplate * This template is used to generate the `tabGuard` elements. It is used once per * element (see `{@link #tabGuardBeforeEl}` and `{@link #tabGuardAfterEl}`). * @private * @since 6.5.1 */ tabGuardTemplate: [{ // We use span instead of div because of IE bug/misfeature: it will focus // block elements upon clicking or calling node.focus() regardless of // tabIndex attribute. It doesn't do that with inline elements, hence span. tag: 'span', 'aria-hidden': 'true', cls: Ext.baseCSSPrefix + 'tab-guard-el' }], /** * @property {Object} tabGuardElements * Read only object containing property names for tab guard elements, keyed by position. * @private * @since 6.5.1 */ tabGuardElements: { before: 'tabGuardBeforeEl', after: 'tabGuardAfterEl' }, init: function(cmp) { var me = this; me.decorateComponent(cmp); if (cmp.addTool) { cmp.addTool = Ext.Function.createSequence(cmp.addTool, me.maybeInitTabGuards, me); } if (cmp.add) { cmp.add = Ext.Function.createSequence(cmp.add, me.maybeInitTabGuards, me); } if (cmp.remove) { cmp.remove = Ext.Function.createSequence(cmp.remove, me.maybeInitTabGuards, me); } cmp.onRender = Ext.Function.createSequence(cmp.onRender, me.maybeInitTabGuards, me); cmp.getTabGuard = me.getTabGuard.bind(me); cmp.on('show', me.initTabGuards, me); }, destroy: function() { var cmp = this.getCmp(); if (cmp) { delete cmp.addTool; delete cmp.add; delete cmp.remove; } this.callParent(); }, privates: { decorateComponent: function(cmp) { var me = this, tpl = me.tabGuardTemplate; cmp = cmp || me.getCmp(); cmp[me.tabGuardElements.before] = cmp.el.insertFirst(tpl); cmp[me.tabGuardElements.after] = cmp.el.createChild(tpl); }, getTabGuard: function(position) { var cmp = this.getCmp(), prop = this.tabGuardElements[position]; return cmp[prop]; }, maybeInitTabGuards: function() { var cmp = this.getCmp(); if (cmp.rendered && cmp.initialized && cmp.tabGuard) { this.initTabGuards(); } }, initTabGuards: function() { var me = this, cmp = me.getCmp(), minTabIndex = me.tabGuardBeforeIndex || 0, maxTabIndex = me.tabGuardAfterIndex || 0, beforeGuard = me.getTabGuard('before'), afterGuard = me.getTabGuard('after'), i, tabIndex, nodes; if (!cmp.rendered || !cmp.tabGuard) { return; } nodes = cmp.el.findTabbableElements({ skipSelf: true }); // Both tab guards may be in the list, disregard them if (nodes[0] === beforeGuard.dom) { nodes.shift(); } if (nodes[nodes.length - 1] === afterGuard.dom) { nodes.pop(); } if (nodes && nodes.length) { // In some cases it might be desirable to configure before and after // guard elements' tabIndex explicitly but if it is missing we try to // infer it from the DOM. If we don't and there are elements with // tabIndex > 0 within the container then tab order will be very // unintuitive. if (minTabIndex == null || maxTabIndex == null) { for (i = 0; i < nodes.length; i++) { // Can't use node.tabIndex property here tabIndex = +nodes[i].getAttribute('tabIndex'); if (tabIndex > 0) { minTabIndex = Math.min(minTabIndex, tabIndex); maxTabIndex = Math.max(maxTabIndex, tabIndex); } } } beforeGuard.dom.setAttribute('tabIndex', minTabIndex); afterGuard.dom.setAttribute('tabIndex', maxTabIndex); } else { // We don't want the guards to participate in tab flow // if there are no tabbable children in the container beforeGuard.dom.removeAttribute('tabIndex'); afterGuard.dom.removeAttribute('tabIndex'); } if (!beforeGuard.hasListeners.focusenter) { beforeGuard.on('focusenter', me.onTabGuardFocusEnter, cmp); } if (!afterGuard.hasListeners.focusenter) { afterGuard.on('focusenter', me.onTabGuardFocusEnter, cmp); } }, onTabGuardFocusEnter: function(e, target) { var cmp = this, el = cmp.el, beforeGuard = cmp.getTabGuard('before'), afterGuard = cmp.getTabGuard('after'), from = e.relatedTarget, nodes, forward, nextFocus; nodes = el.findTabbableElements({ skipSelf: true }); // Tabbables might include two tab guards, so remove them if (nodes[0] === beforeGuard.dom) { nodes.shift(); } if (nodes[nodes.length - 1] === afterGuard.dom) { nodes.pop(); } // Totally possible not to have anything tabbable within the window // but we have to do something so focus back the window el. At least // in that case the user will be able to press Escape key to close it. if (nodes.length === 0) { nextFocus = el; } // The window itself was focused, possibly by clicking or programmatically; // but this time we do have something tabbable to choose from. else if (from === el.dom) { forward = target === beforeGuard.dom; } // Focus was within the window and is trying to escape; // for topmost guard we need to bounce focus back to the last tabbable // element in the window, and vice versa for the bottom guard. else if (el.contains(from)) { forward = !!e.forwardTab; } // It is entirely possible that focus was outside the window and // the user tabbed into the window. In that case we forward the focus // to the next available element in the natural tab order, i.e. the element // after the topmost guard, or the element before the bottom guard. else { forward = target === beforeGuard.dom; } nextFocus = nextFocus || (forward ? nodes[0] : nodes[nodes.length - 1]); if (nextFocus) { // If there is only one focusable node in the window, focusing it // while we're in focusenter handler for the tab guard might cause // race condition where the focusable node will be refocused first // and then its original blur handler will kick in, removing focus // styling erroneously. Ext.fly(nextFocus).focus(nodes.length === 1 ? 1 : 0); } } }});