skypefacebook-workplaceoutlookemailmicrosoft-teamsdiscordmessengercustom-servicesmacoslinuxwindowsinboxwhatsappicloudtweetdeckhipchattelegramhangoutsslackgmail
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
450 lines
15 KiB
450 lines
15 KiB
/** |
|
* A mixin for groups of Focusable things (Components, Widgets, etc) that |
|
* should respond to arrow keys to navigate among the peers, but keep only |
|
* one of the peers focusable by default (tabIndex=0) |
|
* |
|
* Some examples: Toolbars, Radio groups, Tab bars |
|
*/ |
|
|
|
Ext.define('Ext.util.FocusableContainer', { |
|
extend: 'Ext.Mixin', |
|
|
|
requires: [ |
|
'Ext.util.KeyNav' |
|
], |
|
|
|
mixinConfig: { |
|
id: 'focusablecontainer', |
|
|
|
before: { |
|
onAdd: 'onFocusableChildAdd', |
|
onRemove: 'onFocusableChildRemove', |
|
destroy: 'destroyFocusableContainer', |
|
onFocusEnter: 'onFocusEnter' |
|
}, |
|
|
|
after: { |
|
afterRender: 'initFocusableContainer', |
|
onFocusLeave: 'onFocusLeave' |
|
} |
|
}, |
|
|
|
isFocusableContainer: true, |
|
|
|
/** |
|
* @cfg {Boolean} [enableFocusableContainer=true] Enable or disable |
|
* navigation with arrow keys for this FocusableContainer. This option may |
|
* be useful with nested FocusableContainers such as Grid column headers, |
|
* when only the root container should handle keyboard events. |
|
*/ |
|
enableFocusableContainer: true, |
|
|
|
/** |
|
* @cfg {Number} [activeChildTabIndex=0] DOM tabIndex attribute to set on the |
|
* active Focusable child of this container when using the "Roaming tabindex" |
|
* technique. Set this value to > 0 to precisely control the tabbing order |
|
* of the components/containers on the page. |
|
*/ |
|
activeChildTabIndex: 0, |
|
|
|
/** |
|
* @cfg {Number} [inactiveChildTabIndex=-1] DOM tabIndex attribute to set on |
|
* inactive Focusable children of this container when using the "Roaming tabindex" |
|
* technique. This value rarely needs to be changed from its default. |
|
*/ |
|
inactiveChildTabIndex: -1, |
|
|
|
privates: { |
|
initFocusableContainer: function() { |
|
// Allow nested containers to optionally disable |
|
// children containers' behavior |
|
if (this.enableFocusableContainer) { |
|
this.doInitFocusableContainer(); |
|
} |
|
}, |
|
|
|
doInitFocusableContainer: function() { |
|
var me = this, |
|
el; |
|
|
|
el = me.getFocusableContainerEl(); |
|
|
|
// We set tabIndex on the focusable container el so that the user |
|
// could tab into it; we catch its focus event and focus a child instead |
|
me.activateFocusableContainerEl(el); |
|
|
|
me.mon(el, 'mousedown', me.onFocusableContainerMousedown, me); |
|
|
|
me.focusableKeyNav = me.createFocusableContainerKeyNav(el); |
|
}, |
|
|
|
createFocusableContainerKeyNav: function(el) { |
|
var me = this; |
|
|
|
return new Ext.util.KeyNav(el, { |
|
ignoreInputFields: true, |
|
scope: me, |
|
|
|
tab: me.onFocusableContainerTabKey, |
|
enter: me.onFocusableContainerEnterKey, |
|
space: me.onFocusableContainerSpaceKey, |
|
up: me.onFocusableContainerUpKey, |
|
down: me.onFocusableContainerDownKey, |
|
left: me.onFocusableContainerLeftKey, |
|
right: me.onFocusableContainerRightKey |
|
}); |
|
}, |
|
|
|
destroyFocusableContainer: function() { |
|
if (this.enableFocusableContainer) { |
|
this.doDestroyFocusableContainer(); |
|
} |
|
}, |
|
|
|
doDestroyFocusableContainer: function() { |
|
var keyNav = this.focusableKeyNav; |
|
|
|
if (keyNav) { |
|
keyNav.destroy(); |
|
delete this.focusableKeyNav; |
|
} |
|
}, |
|
|
|
// Default FocusableContainer implies a flat list of focusable children |
|
getFocusables: function() { |
|
return this.items.items; |
|
}, |
|
|
|
initDefaultFocusable: function(beforeRender) { |
|
var me = this, |
|
activeIndex = me.activeChildTabIndex, |
|
haveFocusable = false, |
|
items, item, i, len, tabIdx; |
|
|
|
items = me.getFocusables(); |
|
len = items.length; |
|
|
|
if (!len) { |
|
return; |
|
} |
|
|
|
// Check if any child Focusable is already active. |
|
// Note that we're not determining *which* focusable child |
|
// to focus here, only that we have some focusables. |
|
for (i = 0; i < len; i++) { |
|
item = items[i]; |
|
|
|
if (item.focusable) { |
|
haveFocusable = true; |
|
tabIdx = item.getTabIndex(); |
|
|
|
if (tabIdx != null && tabIdx >= activeIndex) { |
|
return item; |
|
} |
|
} |
|
} |
|
|
|
// No interactive children found, no point in going further |
|
if (!haveFocusable) { |
|
return; |
|
} |
|
|
|
// No child is focusable by default, so the first *interactive* |
|
// one gets initial childTabIndex. We are not looking for a focusable |
|
// child here because it may not be focusable yet if this happens |
|
// before rendering; we assume that an interactive child will become |
|
// focusable later and now activateFocusable() will just assign it |
|
// the respective tabIndex. |
|
item = me.findNextFocusableChild(null, true, items, beforeRender); |
|
|
|
if (item) { |
|
me.activateFocusable(item); |
|
} |
|
|
|
return item; |
|
}, |
|
|
|
clearFocusables: function() { |
|
var me = this, |
|
items = me.getFocusables(), |
|
len = items.length, |
|
item, i; |
|
|
|
for (i = 0; i < len; i++) { |
|
item = items[i]; |
|
|
|
if (item.focusable) { |
|
me.deactivateFocusable(item); |
|
} |
|
} |
|
}, |
|
|
|
activateFocusable: function(child, /* optional */ newTabIndex) { |
|
var activeIndex = newTabIndex != null ? newTabIndex : this.activeChildTabIndex; |
|
|
|
child.setTabIndex(activeIndex); |
|
}, |
|
|
|
deactivateFocusable: function(child, /* optional */ newTabIndex) { |
|
var inactiveIndex = newTabIndex != null ? newTabIndex : this.inactiveChildTabIndex; |
|
|
|
child.setTabIndex(inactiveIndex); |
|
}, |
|
|
|
onFocusableContainerTabKey: function() { |
|
return true; |
|
}, |
|
|
|
onFocusableContainerEnterKey: function() { |
|
return true; |
|
}, |
|
|
|
onFocusableContainerSpaceKey: function() { |
|
return true; |
|
}, |
|
|
|
onFocusableContainerUpKey: function(e) { |
|
return this.moveChildFocus(e, false); |
|
}, |
|
|
|
onFocusableContainerLeftKey: function(e) { |
|
return this.moveChildFocus(e, false); |
|
}, |
|
|
|
onFocusableContainerRightKey: function(e) { |
|
return this.moveChildFocus(e, true); |
|
}, |
|
|
|
onFocusableContainerDownKey: function(e) { |
|
return this.moveChildFocus(e, true); |
|
}, |
|
|
|
getFocusableFromEvent: function(e) { |
|
var child = Ext.Component.fromElement(e.getTarget()); |
|
|
|
//<debug> |
|
if (!child) { |
|
Ext.Error.raise("No focusable child found for keyboard event!"); |
|
} |
|
//</debug> |
|
|
|
return child; |
|
}, |
|
|
|
moveChildFocus: function(e, forward) { |
|
var child = this.getFocusableFromEvent(e); |
|
|
|
return this.focusChild(child, forward, e); |
|
}, |
|
|
|
focusChild: function(child, forward) { |
|
var nextChild = this.findNextFocusableChild(child, forward); |
|
|
|
if (nextChild) { |
|
nextChild.focus(); |
|
} |
|
|
|
return nextChild; |
|
}, |
|
|
|
findNextFocusableChild: function(child, step, items, beforeRender) { |
|
var item, idx, i, len; |
|
|
|
items = items || this.getFocusables(); |
|
|
|
// If the child is null or undefined, idx will be -1. |
|
// The loop below will account for that, trying to find |
|
// the first focusable child from either end (depending on step) |
|
idx = Ext.Array.indexOf(items, child); |
|
|
|
// It's often easier to pass a boolean for 1/-1 |
|
step = step === true ? 1 : step === false ? -1 : step; |
|
|
|
len = items.length; |
|
i = step > 0 ? (idx < len ? idx + step : 0) : (idx > 0 ? idx + step : len - 1); |
|
|
|
for (;; i += step) { |
|
// We're looking for the first or last focusable child |
|
// and we've reached the end of the items, so punt |
|
if (idx < 0 && (i >= len || i < 0)) { |
|
return null; |
|
} |
|
|
|
// Loop over forward |
|
else if (i >= len) { |
|
i = -1; // Iterator will increase it once more |
|
continue; |
|
} |
|
|
|
// Loop over backward |
|
else if (i < 0) { |
|
i = len; |
|
continue; |
|
} |
|
|
|
// Looped to the same item, give up |
|
else if (i === idx) { |
|
return null; |
|
} |
|
|
|
item = items[i]; |
|
|
|
if (!item || !item.focusable) { |
|
continue; |
|
} |
|
|
|
// This loop can be run either at FocusableContainer init time, |
|
// or later when we need to navigate upon pressing an arrow key. |
|
// When we're navigating, we have to know exactly if the child is |
|
// focusable or not, hence only rendered children will make the cut. |
|
// At the init time item.isFocusable() may return false incorrectly |
|
// just because the item has not been rendered yet and its focusEl |
|
// is not defined, so we don't bother to call isFocus and return |
|
// the first potentially focusable child. |
|
if (beforeRender || (item.isFocusable && item.isFocusable())) { |
|
return item; |
|
} |
|
} |
|
|
|
return null; |
|
}, |
|
|
|
getFocusableContainerEl: function() { |
|
return this.el; |
|
}, |
|
|
|
onFocusableChildAdd: function(child) { |
|
return this.doFocusableChildAdd(child); |
|
}, |
|
|
|
activateFocusableContainerEl: function(el) { |
|
el = el || this.getFocusableContainerEl(); |
|
|
|
el.set({ tabindex: this.activeChildTabIndex }); |
|
}, |
|
|
|
deactivateFocusableContainerEl: function(el) { |
|
el = el || this.getFocusableContainerEl(); |
|
|
|
el.set({ tabindex: this.inactiveChildTabIndex }); |
|
}, |
|
|
|
doFocusableChildAdd: function(child) { |
|
if (child.focusable) { |
|
child.focusableContainer = this; |
|
this.deactivateFocusable(child); |
|
} |
|
}, |
|
|
|
onFocusableChildRemove: function(child) { |
|
return this.doFocusableChildRemove(child); |
|
}, |
|
|
|
doFocusableChildRemove: function(child) { |
|
// If the focused child is being removed, we deactivate the FocusableContainer |
|
// So that it returns to the tabbing order. |
|
// For example, locking a grid column must return the owning HeaderContainer to tabbability |
|
if (child === this.lastFocusedChild) { |
|
this.lastFocusedChild = null; |
|
this.activateFocusableContainerEl(); |
|
} |
|
delete child.focusableContainer; |
|
}, |
|
|
|
onFocusableContainerMousedown: function(e, target) { |
|
var targetCmp = Ext.Component.fromElement(target); |
|
|
|
// Capture the timestamp for the mousedown. If we're navigating into the container itself |
|
// via the mouse we don't want to default focus the first child like we would when using |
|
// the keyboard. By the time we get to the focusenter handling, we don't know what has caused |
|
// the focus to be triggered, so if the timestamp falls within some small epsilon, the focus enter |
|
// has been caused via the mouse and we can react accordingly. |
|
this.mousedownTimestamp = targetCmp === this ? Ext.Date.now() : 0; |
|
}, |
|
|
|
onFocusEnter: function(e) { |
|
var me = this, |
|
target = e.toComponent, |
|
mousedownTimestamp = me.mousedownTimestamp, |
|
epsilon = 50, |
|
child; |
|
|
|
me.mousedownTimestamp = 0; |
|
if (target === me) { |
|
if (!mousedownTimestamp || Ext.Date.now() - mousedownTimestamp > epsilon) { |
|
child = me.initDefaultFocusable(); |
|
|
|
if (child) { |
|
me.deactivateFocusableContainerEl(); |
|
child.focus(); |
|
} |
|
} |
|
} else { |
|
me.deactivateFocusableContainerEl(); |
|
} |
|
|
|
return target; |
|
}, |
|
|
|
onFocusLeave: function(e) { |
|
var me = this, |
|
lastFocused = me.lastFocusedChild; |
|
|
|
if (!me.isDestroyed) { |
|
me.clearFocusables(); |
|
|
|
if (lastFocused) { |
|
me.activateFocusable(lastFocused); |
|
} |
|
else { |
|
me.activateFocusableContainerEl(); |
|
} |
|
} |
|
}, |
|
|
|
beforeFocusableChildBlur: Ext.privateFn, |
|
afterFocusableChildBlur: Ext.privateFn, |
|
|
|
beforeFocusableChildFocus: function(child) { |
|
var me = this; |
|
|
|
me.clearFocusables(); |
|
me.activateFocusable(child); |
|
|
|
if (child.needArrowKeys) { |
|
me.guardFocusableChild(child); |
|
} |
|
}, |
|
|
|
guardFocusableChild: function(child) { |
|
var me = this, |
|
index = me.activeChildTabIndex, |
|
guard; |
|
|
|
guard = me.findNextFocusableChild(child, -1); |
|
|
|
if (guard) { |
|
guard.setTabIndex(index); |
|
} |
|
|
|
guard = me.findNextFocusableChild(child, 1); |
|
|
|
if (guard) { |
|
guard.setTabIndex(index); |
|
} |
|
}, |
|
|
|
afterFocusableChildFocus: function(child) { |
|
this.lastFocusedChild = child; |
|
}, |
|
|
|
// TODO |
|
onFocusableChildShow: Ext.privateFn, |
|
onFocusableChildHide: Ext.privateFn, |
|
onFocusableChildEnable: Ext.privateFn, |
|
onFocusableChildDisable: Ext.privateFn, |
|
onFocusableChildMasked: Ext.privateFn, |
|
onFocusableChildDestroy: Ext.privateFn, |
|
onFocusableChildUpdate: Ext.privateFn |
|
} |
|
});
|
|
|