/** * This mixin implements focus management functionality in Widgets and Components */Ext.define('Ext.mixin.Focusable', { mixinId: 'focusable', $isFocusableEntity: true, // tabIndex config is now defined in Ext.Component /** * @property {Boolean} focusable * @readonly * * `true` for keyboard interactive Components or Widgets, `false` otherwise. * For Containers, this property reflects interactiveness of the * Container itself, not its children. See {@link #isFocusable}. * * **Note:** It is not enough to set this property to `true` to make * a component keyboard interactive. You also need to make sure that * the component's {@link #focusEl} is reachable via Tab key (tabbable). * See also {@link #tabIndex}. */ focusable: false, /** * @property {Boolean} hasFocus `true` if this component's {@link #focusEl} is focused. * See also {@link #containsFocus}. * * @readonly */ hasFocus: false, /** * @property {Boolean} containsFocus `true` if this currently focused element * is within this Component's or Container's hierarchy. This property is set separately * from {@link #hasFocus}, and can be `true` when `hasFocus` is `false`. * * Examples: * * + Text field with input element focused would be: * focusable: true, * hasFocus: true, * containsFocus: true * * + Date field with drop-down picker currently focused would be: * focusable: true, * hasFocus: false, * containsFocus: true * * + Form Panel with a child input field currently focused would be: * focusable: false, * hasFocus: false, * containsFocus: true * * See also {@link #hasFocus}. * * @readonly */ containsFocus: false, /** * @cfg {String} [focusCls='x-focused'] CSS class that will be added to focused * component's {@link #focusClsEl}, and removed when component blurs. */ focusCls: Ext.baseCSSPrefix + 'focused', /** * @property {Ext.dom.Element} focusEl The element that will be focused * when {@link #method!focus} method is called on this component. Usually this is * the same element that receives focus via mouse clicks, taps, and pressing * Tab key. */ focusEl: 'el', /** * @property {Ext.dom.Element} focusClsEl The element that will have the * {@link #focusCls} applied when component's {@link #focusEl} is focused. */ /** * @event focus * Fires when this Component's {@link #focusEl} receives focus. * @param {Ext.Component/Ext.Widget} this * @param {Ext.event.Event} event The focus event. */ /** * @event blur * Fires when this Component's {@link #focusEl} loses focus. * @param {Ext.Component} this * @param {Ext.event.Event} event The blur event. */ /** * @event focusenter * Fires when focus enters this Component's hierarchy. * @param {Ext.Component} this * @param {Ext.event.Event} event The focusenter event. */ /** * @event focusleave * Fires when focus leaves this Component's hierarchy. * @param {Ext.Component} this * @param {Ext.event.Event} event The focusleave event. */ /** * Returns the main focus holder element associated with this Focusable, i.e. * the element that will be focused when Focusable's {@link #method!focus} method is * called. For most Focusables, this will be the {@link #focusEl}. * * @return {Ext.dom.Element} * @protected */ getFocusEl: function(/* private e */) { var focusEl = this.focusEl; return focusEl && focusEl.dom ? focusEl : null; }, /** * Returns the element used to apply focus styling CSS class when Focusable's * {@link #focusEl} becomes focused. By default it is {@link #focusEl}. * * @param {Ext.dom.Element} [focusEl] Return focus styling element for the given * focused element. This is used by Components implementing multiple focusable * elements. * * @return {Ext.dom.Element} The focus styling element. * @protected */ getFocusClsEl: function() { return this.getFocusEl(); }, /** * Template method to do any Focusable related initialization that * does not involve event listeners creation. * @protected */ initFocusable: Ext.emptyFn, /** * Template method to do any event listener initialization for a Focusable. * This generally happens after the focusEl is available. * @protected */ initFocusableEvents: function(force) { // If *not* naturally focusable, then we look for the tabIndex property // to be defined which indicates that the element should be made focusable. this.initFocusableElement(force); }, enableFocusable: Ext.emptyFn, disableFocusable: function() { var me = this; // If this is disabled while focused, by default, focus would return to document.body. // This must be avoided, both for the convenience of keyboard users, and also // for when focus is tracked within a tree, such as below an expanded ComboBox. if (me.hasFocus) { me.revertFocus(); } me.removeFocusCls(); }, destroyFocusable: function() { var me = this; Ext.destroy(me.focusListeners); me.focusListeners = me.focusEnterEvent = me.focusTask = null; me.focusEl = me.ariaEl = null; }, /** * Determine if this Focusable can receive focus at this time. * * Note that Containers can be non-focusable themselves while delegating * focus treatment to a child Component; see {@link Ext.Container #defaultFocus} * for more information. * * @param {Boolean} [deep=false] Optionally determine if the container itself * is focusable, or if container's focus is delegated to a child component * and that child is focusable. * * @return {Boolean} True if component is focusable, false if not. */ isFocusable: function(deep) { var me = this, focusEl; if (!me.focusable && (!me.isContainer || !deep)) { return false; } focusEl = me.getFocusEl(); if (focusEl && me.canFocus()) { // getFocusEl might return a Component if a Container wishes to // delegate focus to a descendant. Both Component and Element // implement isFocusable, so always ask that. return focusEl && !focusEl.destroyed && focusEl.isFocusable(deep); } return false; }, /** * Determines if this Component is inside a Component tree which is destroyed, *or * is being destroyed*. * @return {boolean} `true` if this Component, or any ancestor is destroyed, or * is being destroyed. * @private */ isDestructing: function() { for (var c = this; c; c = c.getRefOwner()) { if (c.destroying || c.destroyed) { return true; } } return false; }, canFocus: function(skipVisibility, includeFocusTarget) { var me = this, ownerFC = me.ownerFocusableContainer, focusableIfDisabled = ownerFC && ownerFC.allowFocusingDisabledChildren, canFocus; // Containers may have focusable children while being non-focusable // themselves; this is why we only account for me.focusable for // ordinary Components here and below. // MenuItems must accept focus when disabled. canFocus = !me.destroyed && me.rendered && !me.isDestructing() && (me.isContainer || me.focusable) && (!me.isDisabled() || focusableIfDisabled) && (skipVisibility || me.isVisible(true)); return canFocus || (includeFocusTarget && !!me.findFocusTarget()); }, /** * Try to focus this component. * * If this component is disabled or otherwise not focusable, a close relation * will be targeted for focus instead to keep focus localized for keyboard users. * * @param {Boolean/Number[]} [selectText] If applicable, `true` to also select all the text * in this component, or an array consisting of start and end (defaults to start) * position of selection. * * @return {Boolean} `true` if focus target was found and focusing was attempted, * `false` if no focusing attempt was made. */ focus: function(selectText) { var me = this, focusTarget, focusElDom; if ((!me.focusable && !me.isContainer) || me.destroyed || me.destroying) { return false; } // Assignment in conditional here to fall through to else block // if me.canFocus() returns true but there is no focus target if (me.canFocus() && (focusTarget = me.getFocusEl())) { // getFocusEl might return a child Widget or Component if a Container wishes // to delegate focus to a descendant via its defaultFocus configuration. if (focusTarget.$isFocusableEntity) { return focusTarget.focus.apply(focusTarget, arguments); } focusElDom = focusTarget.dom; // This is an Element instance if (focusElDom) { // NOT focusElDom.focus() here! Gotta run through possible Element overrides! focusTarget.focus(); if (selectText && (me.selectText || focusElDom.select)) { if (me.selectText) { if (Ext.isArray(selectText)) { me.selectText.apply(me, selectText); } else { me.selectText(); } } else { focusElDom.select(); } } } // Could be a Widget or something else with no dom property but focusable else if (focusTarget.focus) { focusTarget.focus(); } else { return false; } } else { // If we are asked to focus while not able to focus though disablement/invisibility etc, // focus may revert to document.body if the current focus is being hidden or destroyed. // This must be avoided, both for the convenience of keyboard users, and also // for when focus is tracked within a tree, such as below an expanded ComboBox. focusTarget = me.findFocusTarget(); if (focusTarget && focusTarget != me) { return focusTarget.focus.apply(focusTarget, arguments); } else { return false; } } return true; }, /** * @private */ onBlur: function(e) { var me = this, container = me.ownerFocusableContainer; me.hasFocus = false; if (me.beforeBlur && !me.beforeBlur.$emptyFn) { me.beforeBlur(e); } if (container) { container.beforeFocusableChildBlur(me, e); } me.removeFocusCls(e); if (me.hasListeners.blur) { me.fireEvent('blur', me, e); } if (me.postBlur && !me.postBlur.$emptyFn) { me.postBlur(e); } if (container) { container.afterFocusableChildBlur(me, e); } }, /** * @private */ onFocus: function(e) { var me = this, container = me.ownerFocusableContainer; if (me.canFocus()) { if (me.beforeFocus && !me.beforeFocus.$emptyFn) { me.beforeFocus(e); } if (container) { container.beforeFocusableChildFocus(me, e); } me.addFocusCls(e); if (!me.hasFocus) { me.hasFocus = true; me.fireEvent('focus', me, e); } if (me.postFocus && !me.postFocus.$emptyFn) { me.postFocus(e); } if (container) { container.afterFocusableChildFocus(me, e); } } }, /** * Return the actual tabIndex for this Focusable. * * @return {Number} tabIndex attribute value */ getTabIndex: function() { var me = this, el, index; if (!me.focusable) { return; } el = me.getFocusEl(); if (el) { // getFocusEl may return a child Widget or Component if (el.$isFocusableEntity) { index = el.getTabIndex(); } else if (el.isElement && el.dom) { // We can't query el.dom.tabIndex because IE8 will return 0 // when tabIndex attribute is not present, and Chrome will // return -1. Can't trust a browser to do a simplest thing. :/ index = el.dom.getAttribute('tabIndex'); // This contraption is here because we can't simply coerce // the returned attribute value to a number. If the attribute // is not present, the value returned will be null, and coercing // null gives 0. Wondrous JavaScript. :/ if (index !== null) { index -= 0; } } // A component can be configured with el: '#id' to look up // its main element from the DOM rather than render it; in // such case getTabIndex() may happen to be called before // said lookup has happened; indeterminate result follows. else { return; } } if (typeof index !== 'number') { index = me.tabIndex; } return index; }, /** * Set the tabIndex property for this Focusable. If the focusEl * is available, set tabIndex attribute on it, too. * * @param {Number} newTabIndex new tabIndex to set * @param {HTMLElement} [focusEl] (private) */ setTabIndex: function(newTabIndex, focusEl) { var me = this, ownerFC = me.ownerFocusableContainer, focusableIfDisabled = ownerFC && ownerFC.allowFocusingDisabledChildren, el; // See comments for definition of forceTabIndex as to why this is needed if (!me.focusable && !me.forceTabIndex) { return; } me.tabIndex = newTabIndex; // We must not do this if we are destroyed, or if we are incapable of being focused. // Handle our owning FocusableContainer having allowFocusingDisabledChildren if (me.destroying || me.destroyed || (me.isDisabled() && !focusableIfDisabled)) { return; } el = focusEl || me.getFocusEl(); if (el) { // getFocusEl may return a child Widget or Component if (el.$isFocusableEntity) { el.setTabIndex(newTabIndex); } // Or if a component is configured with el: '#id', it may // still be a string by the time setTabIndex is called from // owner FocusableContainer. else if (el.isElement && el.dom) { // setTabIndex is aware of saved tabbable state el.setTabIndex(newTabIndex); } } }, /** * @template * @protected * Called when focus enters this Component's hierarchy * @param {Object} e * @param {Ext.event.Event} e.event The underlying DOM event. * @param {HTMLElement} e.target The element gaining focus. * @param {HTMLElement} e.relatedTarget The element losing focus. * @param {Ext.Component} e.toComponent The Component gaining focus. * @param {Ext.Component} e.fromComponent The Component losing focus. * @param {Boolean} e.backwards `true` if the `fromComponent` is *after* the `toComponent* * in the DOM tree, indicating that the user used `SHIFT+TAB` to move focus. Note that setting * `tabIndex` values to affect tabbing order can cause this to be incorrect. Setting * `tabIndex` values is not advised. */ onFocusEnter: function(e) { var me = this; // We DO NOT check if `me` is focusable here. The reason is that // non-focusable containers need to track focus entering their // children so that revertFocus would work if these children // become unavailable. if (me.destroying || me.destroyed) { return; } // Save all information about how we received focus so that // we can do appropriate things when asked to revertFocus me.focusEnterEvent = e; me.containsFocus = true; if (me.hasListeners.focusenter) { me.fireEvent('focusenter', me, e); } }, /** * @template * @protected * Called when focus exits from this Component's hierarchy * @param {Ext.event.Event} e * @param {Ext.event.Event} e.event The underlying DOM event. * @param {HTMLElement} e.target The element gaining focus. * @param {HTMLElement} e.relatedTarget The element losing focus. * @param {Ext.Component} e.toComponent The Component gaining focus. * @param {Ext.Component} e.fromComponent The Component losing focus. */ onFocusLeave: function(e) { var me = this; // Same as in onFocusEnter if (me.destroying || me.destroyed) { return; } me.focusEnterEvent = null; me.containsFocus = false; if (me.hasListeners.focusleave) { me.fireEvent('focusleave', me, e); } }, /** * @template * @method * @protected * Called when focus moves *within* this Component's hierarchy * @param {Object} info * @param {Ext.event.Event} info.event The underlying Event object. * @param {HTMLElement} info.toElement The element gaining focus. * @param {HTMLElement} info.fromElement The element losing focus. * @param {Ext.Component} info.toComponent The Component gaining focus. * @param {Ext.Component} info.fromComponent The Component losing focus. * @param {Boolean} info.backwards `true` if the focus movement is backward in DOM order */ onFocusMove: Ext.emptyFn, privates: { // This private flag was introduced to work around an issue where // the tab index would not be stamped onto the component. // Consider a tool, which is focusable by default and has tabIndex: 0 // on the class definition. If a non-focusable tool is required, setting // focusable: false is not enough, the tabIndex also needs to be considered. // However, the default behaviour is to not modify the tabIndex on non // focusable items. This config can go away if that behaviour is changed. // Arguably, a non-focusable widget probably shouldn't retain a tab index // if it's explicitly configured. forceTabIndex: false, /** * Returns focus to the Component or element found in the cached * focusEnterEvent. * * @private */ revertFocus: function() { var me = this, focusEvent = me.focusEnterEvent, activeElement = Ext.Element.getActiveElement(), focusTarget, fromComponent, reverted; // If we have a record of where focus arrived from, // and have not been told to avoid refocusing, // and we contain the activeElement. // Then, before hiding, restore focus to what was focused before we were focused. if (focusEvent && !me.preventRefocus && me.el.contains(activeElement)) { fromComponent = focusEvent.fromComponent; // If the default focus reversion target is in shifting ground, fall back to document.body if (fromComponent && (fromComponent.destroyed || fromComponent.isDestructing())) { focusTarget = document.body; } // Preferred focus target is the actual element from which focus entered this component. // It will be up to its encapsulating component to handle this in an appropriate way. // For example, a grid, upon having focus pushed to a certain cell will set its // navigation position to that cell and highlight it as focused. // Likewise an input field must handle its field acquiring focus. else { focusTarget = focusEvent.relatedTarget; } // If focus was from the body, try to keep it closer than that if (focusTarget === document.body) { fromComponent = me.findFocusTarget(); if (fromComponent) { focusTarget = fromComponent.getFocusEl(); } } if (focusTarget && focusTarget.$isFocusableEntity) { if (!focusTarget.destroyed && focusTarget.isFocusable()) { focusTarget.focus(); } } // If the element is in the document and focusable, then we're good. The owning component will handle it. else if (Ext.getDoc().contains(focusTarget) && Ext.fly(focusTarget).isFocusable()) { fromComponent = Ext.Component.from(focusTarget); // Allow the focus recieving component to modify the focus sequence. if (fromComponent) { fromComponent.revertFocusTo(focusTarget); } else { focusTarget.focus(); } } // If the element has gone, or is hidden, we will have to rely on the intelligent focus diversion // of components to send focus back to somewhere that is least surprising for the user. else if (focusEvent.fromComponent && focusEvent.fromComponent.focus) { reverted = focusEvent.fromComponent.focus(); // The component was not able to find a suitable target. // Important: Touch platforms do not blur programatically focused elements // when they become hidden, so we must force the issue in order to maintain // focus tracking. if (!reverted) { activeElement.blur(); } } } }, /** * This field is on the recieving end of a call from {@link #method!revertFocus}. * * It is called when focus is being pushed back into this Component from a Component * that is focused and is being hidden or disabled. * * We must focus the passed element. * * Subclasses may perform some extra processing to prepare for refocusing. * @param target */ revertFocusTo: function(target) { target.focus(); }, /** * Finds an alternate Component to focus if this Component is disabled while focused, or * focused while disabled, or otherwise unable to focus. * * In both cases, focus must not be lost to document.body, but must move to an intuitively * connectible Component, either a sibling, or uncle or nephew. * * This is both for the convenience of keyboard users, and also for when focus is tracked * within a Component tree such as for ComboBoxes and their dropdowns. * * For example, a ComboBox with a PagingToolbar in is BoundList. If the "Next Page" * button is hit, the LoadMask shows and focuses, the next page is the last page, so * the "Next Page" button is disabled. When the LoadMask hides, it attempt to focus the * last focused Component which is the disabled "Next Page" button. In this situation, * focus should move to a sibling within the PagingToolbar. * * @return {Ext.Component} A closely related focusable Component to which focus can move. * @private */ findFocusTarget: function() { var me = this, parentAxis, candidate, len, i, focusTargets, focusIndex; if (me.preventRefocus) { return null; } // Create an axis of visible, enabled, stable parent components which we // will walk up looking for ancestors to revert focus to. // // First, find all enabled parents. for (parentAxis = [], candidate = me.getRefOwner(); candidate; candidate = candidate.getRefOwner()) { if (!candidate.isDisabled()) { parentAxis.unshift(candidate); } } // Then walk downwards until we encounter a non-targetable parent // which means hidden of destroying. All candidates *above* that // are potential sources of focus targets. for (i = 0, len = parentAxis.length; i < len; i++) { candidate = parentAxis[i]; if (candidate.destroying || !candidate.isVisible()) { parentAxis.length = i; break; } } // Walk up the parent axis checking each parent for focus targets. for (i = parentAxis.length - 1; i >=0; i--) { candidate = parentAxis[i]; // Use CQ to find a target that is fully focusable (:canfocus, NOT the theoretical :focusable) // Cannot use :focusable(true) because that consults findFocusTarget and would cause infinite recursion. // Exclude the component which currently has focus. // Cannot use candidate.child() because the parent might not be a Container. // Non-Container Components may still have ownership relationships with // other Components. eg: BoundList with PagingToolbar focusTargets = Ext.ComponentQuery.query(':canfocus()', candidate); if (focusTargets.length) { focusIndex = Ext.Array.indexOf(focusTargets, Ext.ComponentManager.getActiveComponent()); // Return the next focusable, or the previous focusable, or the first focusable return focusTargets[focusIndex + 1] || focusTargets[focusIndex - 1] || focusTargets[0]; } // We found no focusable siblings in our candidate, but the candidate may itself be focusable, // it is not always a Container - could be the owning Field of a BoundList. if (candidate.isFocusable && candidate.isFocusable()) { return candidate; } } }, /** * Sets up the focus listener on this Component's {@link #getFocusEl focusEl} if it has one. * * Form Components which must implicitly participate in tabbing order usually have a naturally * focusable element as their {@link #getFocusEl focusEl}, and it is the DOM event of that * receiving focus which drives the Component's `onFocus` handling, and the DOM event of it * being blurred which drives the `onBlur` handling. * @private */ initFocusableElement: function(force) { var me = this, tabIndex = me.getTabIndex(), focusEl = me.getFocusEl(); if (focusEl && !focusEl.$isFocusableEntity) { // focusEl is not available until after rendering, and rendering tabIndex // into focusEl is not always convenient. So we apply it here if Component's // tabIndex property is set and Component is otherwise focusable. // Note that for Widgets and Modern Components we might not have the rendered // flag set yet but can force setting the property. if (tabIndex != null && (force || me.canFocus(true))) { me.setTabIndex(tabIndex, focusEl); } // This attribute is a shortcut to look up a Component by its Elements // It only makes sense on focusable elements, so we set it here unless // our focusEl is delegated to the focusEl of an owned Component and it // already has ownership stamped into it. if (!focusEl.dom.hasAttribute('data-componentid')) { focusEl.dom.setAttribute('data-componentid', me.id); } } }, addFocusCls: function(e) { var focusCls = this.focusCls, el; el = this.getFocusClsEl(); if (focusCls) { el = this.getFocusClsEl(e); if (el) { el.addCls(focusCls); } } }, removeFocusCls: function(e) { var focusCls = this.focusCls, el; if (focusCls) { el = this.getFocusClsEl(e); if (el) { el.removeCls(focusCls); } } }, /** * @private */ handleFocusEvent: function(e) { var me = this, event; if (!me.focusable || me.destroying || me.destroyed) { return; } // handleFocusEvent and handleBlurEvent are called by ComponentManager // passing the normalized element event that might or might not cause // component focus or blur. The component itself makes the decision // whether focus/blur happens or not. This is necessary for components // that might have more than one focusable element within the component's // DOM structure, like Ext.button.Split. if (me.isFocusing(e)) { event = new Ext.event.Event(e.event); event.type = 'focus'; event.relatedTarget = e.fromElement; event.target = e.toElement; me.onFocus(event); } }, /** * @private */ handleBlurEvent: function(e) { var me = this, event; if (!me.focusable || me.destroying || me.destroyed) { return; } // Must process blur if toElement is document.body // Blurs caused by the app losing focus are processed synchronously // for obvious reasons, so the activeElement will still be the // focusEl, so isBlurring will return false, and reject this op. if (e.toElement === document.body || me.isBlurring(e)) { event = new Ext.event.Event(e.event); event.type = 'blur'; event.target = e.fromElement; event.relatedTarget = e.toElement; me.onBlur(event); } }, /** * @private */ isFocusing: function(e) { var focusEl = this.getFocusEl(); if (focusEl) { if (focusEl.isFocusing) { return focusEl.isFocusing(e); } else { // Sometimes focusing an element may cause reaction from other entities // that will focus something else instead. So before unraveling the // event chain we better make sure our focusEl is *indeed* focused. return focusEl.dom === document.activeElement && e.toElement === focusEl.dom && e.fromElement !== e.toElement; } } return false; }, /** * @private */ isBlurring: function(e) { var focusEl = this.getFocusEl(); if (focusEl) { if (focusEl.isFocusing) { return focusEl.isBlurring(e); } else { // Ditto for blurring: in some cases freshly blurred element can be // refocused by external forces so we need to check if our focusEl // *indeed* is not focused before we can call it blurring. return focusEl.dom !== document.activeElement && e.fromElement === focusEl.dom && e.fromElement !== e.toElement; } } return false; }, /** * @private */ blur: function() { var me = this, focusEl; if (!me.focusable || !me.canFocus()) { return; } focusEl = me.getFocusEl(); if (focusEl) { me.blurring = true; focusEl.blur(); delete me.blurring; } }, isTabbable: function() { var me = this, focusEl; if (me.focusable) { focusEl = me.getFocusEl(); if (focusEl && focusEl.isTabbable()) { return focusEl.isTabbable(); } } return false; }, disableTabbing: function() { var me = this, el = me.el, focusEl; // We DO NOT check for me.focusable here, because this should work // for containers with focus delegates, too! if (me.destroying || me.destroyed) { return; } // We're disabling tabbability for all elements of a given Component; // focusEl might be outside of the hierarchy which is checked below. if (el) { el.saveTabbableState(); } focusEl = me.getFocusEl(); if (focusEl) { // focusEl may happen to be a focus delegate for a container if (focusEl.$isFocusableEntity) { focusEl.disableTabbing(); } // Alternatively focusEl may happen to be outside of the main el, // or else it can be a string reference to an element that // has not been resolved yet else if (focusEl.isElement && el && !el.contains(focusEl)) { focusEl.saveTabbableState(); } } }, enableTabbing: function(reset) { var me = this, el = me.el, focusEl; // We DO NOT check for me.focusable here, because this should work // for containers with focus delegates, too! if (me.destroying || me.destroyed) { return; } focusEl = me.getFocusEl(); if (focusEl) { if (focusEl.$isFocusableEntity) { focusEl.enableTabbing(); } else if (focusEl.isElement && el && !el.contains(focusEl)) { focusEl.restoreTabbableState(); } } if (el) { el.restoreTabbableState({ reset: reset }); } } }}, function() { var keyboardModeCls = Ext.baseCSSPrefix + 'keyboard-mode', keyboardMode = false; /** * @cfg {Boolean} enableKeyboardMode * When set to `true`, focus styling will be applied to focused elements based on the * user interaction mode: when keyboard was used to focus an element, focus styling * will be visible but not when element was focused otherwise (e.g. with mouse, touch, * or programmatically). The {@link #keyboardMode} property will then reflect the last * user interaction mode. * Setting this option to `false` disables keyboard mode tracking and results in focus * styling always being applied to focused elements, which is pre-Ext JS 6.5 behavior. * * Defaults to `false` in desktop environments, `true` on mobile devices. * @member Ext * @bindable * @since 6.6.0 */ Ext.enableKeyboardMode = Ext.isModern || !Ext.os.is.Desktop; /** * @property {Boolean} keyboardMode * @member Ext * A flag which indicates that the last UI interaction from the user was a keyboard gesture * @since 6.5.0 */ /** * @property {Boolean} touchMode * @member Ext * A flag which indicates that the last UI interaction from the user was a touch gesture * @since 6.5.0 */ Ext.setKeyboardMode = Ext.setKeyboardMode || function (keyboardMode) { Ext.keyboardMode = keyboardMode; Ext.getBody().toggleCls(keyboardModeCls, keyboardMode); }; Ext.isTouchMode = function () { return (Ext.now() - Ext.lastTouchTime) < 500; }; /** * @private */ Ext.syncKeyboardMode = function(e) { if (!Ext.enableKeyboardMode) { return; } var type = e.type; if (type === 'pointermove') { // On pointer move we want to track that the user has switched from keyboard // to mouse, but we do not remove the visual focus style until a pointerdown // occurs or the focus changes. This accomodates menus which change focus // based on mouseenter of a menu item keyboardMode = false; } else { keyboardMode = (type === 'keydown'); Ext.lastTouchTime = e.pointerType === 'touch' && Ext.now(); Ext.setKeyboardMode(keyboardMode); } }; function keyboardModeFocusHandler() { // NOT Ext.keyboardMode here; closing over variable local to class callback fn if (keyboardMode !== Ext.getBody().hasCls(keyboardModeCls)) { Ext.setKeyboardMode(keyboardMode); } } Ext.getEnableKeyboardMode = function() { return Ext.enableKeyboardMode; }; Ext.setEnableKeyboardMode = function(enable) { var listeners = { pointerdown: Ext.syncKeyboardMode, pointermove: Ext.syncKeyboardMode, keydown: Ext.syncKeyboardMode, capture: true, delegated: false }; Ext.enableKeyboardMode = !!enable; if (Ext.enableKeyboardMode) { Ext.getWin().on(listeners); Ext.on('focus', keyboardModeFocusHandler); } else { Ext.getWin().un(listeners); Ext.un('focus', keyboardModeFocusHandler); } }; Ext.onReady(function() { // Add the CSS class once upfront if enableKeyboardMode is disabled if (!Ext.enableKeyboardMode) { Ext.getBody().addCls(keyboardModeCls); } Ext.setEnableKeyboardMode(Ext.enableKeyboardMode); });});