/**
* 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 tabbable by default (tabIndex=0)
*
* Some examples: Toolbars, Tab bars, Panel headers, Menus
*/
Ext.define('Ext.mixin.FocusableContainer', {
extend: 'Ext.Mixin',
requires: [
'Ext.util.KeyNav'
],
mixinConfig: {
id: 'focusablecontainer'
},
isFocusableContainer: true,
/**
* @cfg {Boolean} [focusableContainer=false] Enable or disable navigation
* with arrow keys for this FocusableContainer. This option may be useful
* with nested FocusableContainers, when only the root container should
* handle keyboard events.
*/
focusableContainer: false,
/**
* @cfg {Boolean} [resetFocusPosition=false] When `true`, FocusableContainer
* will reset last focused position whenever focus leaves the container.
* Subsequent tabbing into the container will always focus the first eligible
* child item.
*
* When `false`, subsequent tabbing into the container will focus the child
* item that was last focused before.
*/
resetFocusPosition: false,
/**
* @cfg {Number} [activeChildTabIndex=0] DOM tabIndex attribute to set on the
* active Focusable child of this container when using the "Roaming tabindex"
* technique.
*/
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,
/**
* @cfg {Boolean} [allowFocusingDisabledChildren=false] Set this to `true`
* to enable focusing disabled child items via keyboard.
*/
allowFocusingDisabledChildren: false,
/**
* @property {String/Ext.dom.Element} [focusableContainerEl="el"] The name of the element
* that FocusableContainer should bind its keyboard handler to. Similar to {@link #ariaEl},
* this name is resolved to the {@link Ext.dom.Element} instance after rendering.
*/
focusableContainerEl: 'el',
privates: {
initFocusableContainer: function(clearChildren) {
var items, i, len;
// Allow nested containers to optionally disable
// children containers' behavior
if (this.focusableContainer) {
clearChildren = clearChildren != null ? clearChildren : true;
this.doInitFocusableContainer(clearChildren);
}
// A FocusableContainer instance such as a toolbar could have decided
// to opt out of FC behavior for some reason; it could have happened
// after all or almost all child items have been initialized with
// ownerFocusableContainer reference. We need to clean this up
// if we're not going to behave like a FocusableContainer after all.
else {
items = this.getFocusables();
for (i = 0, len = items.length; i < len; i++) {
items[i].ownerFocusableContainer = null;
}
}
},
doInitFocusableContainer: function(clearChildren) {
var me = this,
el = me.focusableContainerEl,
child;
// Resolve property name to the actual Element instance if it hasn't been
// resolved already; doInitFocusableContainer can be called more than once
if (!el.isElement) {
el = me.focusableContainerEl = me[el];
}
if (me.initFocusableContainerKeyNav) {
me.initFocusableContainerKeyNav(el);
}
// This flag allows post factum initialization of the focusable container,
// i.e. when container was empty initially and then some tabbable children
// were added and we need to clear their tabIndices.
// This is useful for Panel and Window headers that might have tools
// added dynamically.
if (clearChildren) {
me.clearFocusables();
if (!me.isDisabled()) {
child = me.findNextFocusableChild({ step: 1 }) ||
me.findNextFocusableChild({ beforeRender: true });
if (child) {
me.activateFocusable(child);
}
}
}
child = me.findNextFocusableChild({ firstTabbable: true });
// If we have no potentially focusable children we need to disable arrow key
// handlers so that they don't interfere with event bubbling.
me.activateFocusableContainer(!!child && !me.isDisabled());
},
initFocusableContainerKeyNav: function(el) {
var me = this;
if (!me.focusableKeyNav) {
el = el || me.focusableContainerEl;
me.focusableKeyNav = new Ext.util.KeyNav({
target: el,
disabled: true,
eventName: 'keydown',
// Our event processing filters out events from input fields.
processEvent: me.processFocusableContainerKeyEvent,
processEventScope: me,
scope: me,
tab: me.onFocusableContainerTabKey,
enter: {
handler: me.onFocusableContainerEnterKey,
ctrl: false,
shift: false,
alt: false
},
space: {
handler: me.onFocusableContainerSpaceKey,
ctrl: false,
shift: false,
alt: false
},
up: {
handler: me.onFocusableContainerUpKey,
ctrl: false,
shift: false,
alt: false
},
down: {
handler: me.onFocusableContainerDownKey,
ctrl: false,
shift: false,
alt: false
},
left: {
handler: me.onFocusableContainerLeftKey,
ctrl: false,
shift: false,
alt: false
},
right: {
handler: me.onFocusableContainerRightKey,
ctrl: false,
shift: false,
alt: false
}
});
}
},
destroyFocusableContainer: function() {
this.focusableKeyNav = Ext.destroy(this.focusableKeyNav);
},
activateFocusableContainer: function(enable) {
var keyNav = this.focusableKeyNav;
if (keyNav) {
keyNav.setDisabled(!enable);
}
},
isFocusableContainerActive: function() {
var keyNav = this.focusableKeyNav;
return keyNav ? !keyNav.disabled : false;
},
// Default FocusableContainer implies a flat list of focusable children
getFocusables: function() {
return this.items.items;
},
initDefaultFocusable: function() {
var me = this,
haveFocusable = false,
items, item, i, len;
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.isDisabled() && item.isFocusable()) {
haveFocusable = true;
// DO NOT return an item here! We want to fall through
// to findNextFocusableChild below.
break;
}
}
if (!haveFocusable) {
return;
}
item = me.findNextFocusableChild({
items: items,
step: true
});
if (item) {
me.activateFocusable(item);
}
return item;
},
clearFocusables: function(skipFocused) {
var me = this,
items = me.getFocusables(),
len = items.length,
item, i;
for (i = 0; i < len; i++) {
item = items[i];
if (!item.destroyed && item.focusable && item.isTabbable()) {
me.deactivateFocusable(item);
}
}
},
/**
* @template
* An overrideable function which preprocesses all key events from within this
* FocusableContainer. The base implementation vetoes processing of key events from input
* fields by returning `undefined`. This may be overridden in subclasses with special
* requirements.
* @param {Ext.util.Event} e A keyboard event that is significant to the operation
* of a FocusableContainer.
* @returns {Ext.util.Event} The event if it is to be processed as a FocusableContainer
* navigation keystroke, or `undefined` if it is to be ignore by the FocusableContainer
* navigation machinery.
*/
processFocusableContainerKeyEvent: function(e) {
if (!Ext.fly(e.target).isInputField()) {
return e;
}
},
activateFocusable: function(child) {
child.setTabIndex(this.activeChildTabIndex);
},
deactivateFocusable: function(child) {
child.setTabIndex(this.inactiveChildTabIndex);
},
onFocusableContainerTabKey: function() {
return true;
},
onFocusableContainerEnterKey: function() {
return true;
},
onFocusableContainerSpaceKey: function() {
return true;
},
onFocusableContainerUpKey: function(e) {
// Default action is to scroll the nearest vertically scrollable container
e.preventDefault();
return this.moveChildFocus(e, false);
},
onFocusableContainerDownKey: function(e) {
// Ditto
e.preventDefault();
return this.moveChildFocus(e, true);
},
onFocusableContainerLeftKey: function(e) {
// Default action is to scroll the nearest horizontally scrollable container
e.preventDefault();
return this.moveChildFocus(e, false);
},
onFocusableContainerRightKey: function(e) {
// Ditto
e.preventDefault();
return this.moveChildFocus(e, true);
},
getFocusableFromEvent: function(e) {
var child = Ext.Component.from(e);
//<debug>
if (!child) {
Ext.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: child,
step: forward
});
if (nextChild) {
nextChild.focus();
}
return nextChild;
},
findNextFocusableChild: function(options) {
// This method is private, so options should always be provided
var beforeRender = options.beforeRender,
firstTabbable = options.firstTabbable,
items, item, child, step, idx, i, len, allowDisabled;
items = options.items || this.getFocusables();
step = options.step != null ? options.step : 1;
child = options.child;
// Some containers such as Menus need to support arrowing over disabled children
allowDisabled = !!this.allowFocusingDisabledChildren;
// 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 || (item.isDisabled() && !allowDisabled)) {
continue;
}
if (firstTabbable) {
if (item.isTabbable && item.isTabbable()) {
return item;
}
}
// 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 isFocusable and return
// the first potentially focusable child.
else if (beforeRender || (item.isFocusable && item.isFocusable())) {
return item;
}
}
return null; // eslint-disable-line no-unreachable
},
onFocusEnter: function(e) {
var me = this,
target = e.toComponent,
child;
// Some FocusableContainers such as Menus are focusable by themselves;
// when a container becomes focused we need to switch focus to a child
// item instead, if there are eligible items.
// Note that when the target is an actual child item, we will activate
// it in beforeFocusableChildFocus().
if (target === me) {
child = me.initDefaultFocusable();
if (child) {
child.focus();
}
}
// Make sure arrow key handlers are enabled.
me.activateFocusableContainer(true);
},
onFocusLeave: function(e) {
var me = this;
if (me.resetFocusPosition) {
me.clearFocusables();
me.initDefaultFocusable();
}
},
beforeFocusableChildBlur: Ext.privateFn,
afterFocusableChildBlur: Ext.privateFn,
beforeFocusableChildFocus: function(child) {
var me = this;
if (!me.focusableContainer || me.destroying || me.destroyed) {
return;
}
// Ensure this child is the only one tabbable
me.clearFocusables();
me.activateFocusable(child);
},
afterFocusableChildFocus: function(child) {
var me = this;
if (!me.focusableContainer || me.destroying || me.destroyed) {
return;
}
me.lastFocusedChild = child;
},
onFocusableChildAdd: function(child) {
var me = this;
if (child.focusable) {
child.ownerFocusableContainer = me;
}
},
onFocusableChildRemove: function(child) {
var me = this,
next;
child.ownerFocusableContainer = null;
// If the focused child is being removed, we need to find another item
// to make it default focusable.
if (child === me.lastFocusedChild) {
me.lastFocusedChild = null;
next = me.initDefaultFocusable();
// When currently focused child is about to be removed, it can be
// hidden or destroyed as well and that could cause focus to be
// lost to the document body. We don't want that.
if (child.hasFocus) {
next = next || child.findFocusTarget();
if (next) {
// Delaying focus event for FireFox as this focus is not getting updated.
next.focus(null, Ext.isGecko);
}
}
}
child = next || me.findNextFocusableChild({ step: 1, beforeRender: true });
if (!child) {
me.activateFocusableContainer(false);
}
},
beforeFocusableChildEnable: Ext.privateFn,
onFocusableChildEnable: function(child) {
var me = this,
active;
if (!me.focusableContainer || me.destroying || me.destroyed) {
return;
}
// Having currently focused child trumps any config
if (me.containsFocus) {
active = Ext.ComponentManager.getActiveComponent();
me.clearFocusables();
me.activateFocusable(active);
}
else if (me.resetFocusPosition || me.lastFocusedChild == null) {
me.clearFocusables();
if (child.hasFocus) {
me.activateFocusable(child);
active = child;
}
}
else {
me.deactivateFocusable(child);
// Some Components like Buttons do not render tabIndex attribute
// when they start their lifecycle disabled, or remove tabIndex
// if they get disabled later. Subsequently, such Components will
// reset their tabIndex to default configured value upon enabling.
// We don't want these children to be tabbable so we reset their
// tabIndex yet again, unless this child is the last focused one.
if (child === me.lastFocusedChild) {
me.clearFocusables();
me.activateFocusable(child);
}
active = me.findNextFocusableChild({ firstTabbable: true });
}
if (!active) {
me.initDefaultFocusable();
}
// Also make sure arrow key handlers are enabled
me.activateFocusableContainer(true);
},
beforeFocusableChildDisable: function(child) {
var me = this,
next;
if (!me.focusableContainer || me.destroying || me.destroyed) {
return;
}
// When currently focused child is about to be disabled,
// it may lose the focus as well. For example, Classic buttons
// will remove tabIndex attribute upon disabling, which
// in turn will throw focus to the document body and cause
// onFocusLeave to fire on the FocusableContainer.
// We're focusing the next sibling to prevent that.
if (child.hasFocus) {
next = me.findNextFocusableChild({ child: child }) || child.findFocusTarget();
// Note that it is entirely possible not to find the nextTarget,
// e.g. when we're disabling the last button in a toolbar rendered
// directly into document body. We don't have a good way to handle
// such cases at present.
if (next) {
next.focus();
}
}
},
onFocusableChildDisable: function(child) {
var me = this,
next;
if (!me.focusableContainer || me.destroying || me.destroyed) {
return;
}
// If the disabled child was focused before disabling, we should have
// switched focus to another child and activated it already.
next = me.findNextFocusableChild({ firstTabbable: true });
// If not then activate first found child.
if (!next) {
next = me.initDefaultFocusable();
}
// It is possible that the disabled child was the last focusable child
// of this container, in which case we need to deactivate keyboard
// event handlers.
if (!next) {
me.activateFocusableContainer(false);
}
},
beforeFocusableChildHide: function(child) {
return this.beforeFocusableChildDisable(child);
},
onFocusableChildHide: function(child) {
return this.onFocusableChildDisable(child);
},
beforeFocusableChildShow: function(child) {
return this.beforeFocusableChildEnable(child);
},
onFocusableChildShow: function(child) {
return this.onFocusableChildEnable(child);
},
// TODO
onFocusableChildMasked: Ext.privateFn,
onFocusableChildDestroy: Ext.privateFn,
onFocusableChildUpdate: Ext.privateFn
},
deprecated: {
7: {
configs: {
enableFocusableContainer: 'focusableContainer'
}
}
}
});