Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / FocusManager.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.FocusManager
17
18 The FocusManager is responsible for globally:
19
20 1. Managing component focus
21 2. Providing basic keyboard navigation
22 3. (optional) Provide a visual cue for focused components, in the form of a focus ring/frame.
23
24 To activate the FocusManager, simply call `Ext.FocusManager.enable();`. In turn, you may
25 deactivate the FocusManager by subsequently calling `Ext.FocusManager.disable();.  The
26 FocusManager is disabled by default.
27
28 To enable the optional focus frame, pass `true` or `{focusFrame: true}` to {@link #enable}.
29
30 Another feature of the FocusManager is to provide basic keyboard focus navigation scoped to any {@link Ext.container.Container}
31 that would like to have navigation between its child {@link Ext.Component}'s. The {@link Ext.container.Container} can simply
32 call {@link #subscribe Ext.FocusManager.subscribe} to take advantage of this feature, and can at any time call
33 {@link #unsubscribe Ext.FocusManager.unsubscribe} to turn the navigation off.
34
35  * @singleton
36  * @author Jarred Nicholls <jarred@sencha.com>
37  * @docauthor Jarred Nicholls <jarred@sencha.com>
38  */
39 Ext.define('Ext.FocusManager', {
40     singleton: true,
41     alternateClassName: 'Ext.FocusMgr',
42
43     mixins: {
44         observable: 'Ext.util.Observable'
45     },
46
47     requires: [
48         'Ext.ComponentManager',
49         'Ext.ComponentQuery',
50         'Ext.util.HashMap',
51         'Ext.util.KeyNav'
52     ],
53
54     /**
55      * @property {Boolean} enabled
56      * Whether or not the FocusManager is currently enabled
57      */
58     enabled: false,
59
60     /**
61      * @property {Ext.Component} focusedCmp
62      * The currently focused component. Defaults to `undefined`.
63      */
64
65     focusElementCls: Ext.baseCSSPrefix + 'focus-element',
66
67     focusFrameCls: Ext.baseCSSPrefix + 'focus-frame',
68
69     /**
70      * @property {String[]} whitelist
71      * A list of xtypes that should ignore certain navigation input keys and
72      * allow for the default browser event/behavior. These input keys include:
73      *
74      * 1. Backspace
75      * 2. Delete
76      * 3. Left
77      * 4. Right
78      * 5. Up
79      * 6. Down
80      *
81      * The FocusManager will not attempt to navigate when a component is an xtype (or descendents thereof)
82      * that belongs to this whitelist. E.g., an {@link Ext.form.field.Text} should allow
83      * the user to move the input cursor left and right, and to delete characters, etc.
84      */
85     whitelist: [
86         'textfield'
87     ],
88
89     tabIndexWhitelist: [
90         'a',
91         'button',
92         'embed',
93         'frame',
94         'iframe',
95         'img',
96         'input',
97         'object',
98         'select',
99         'textarea'
100     ],
101
102     constructor: function() {
103         var me = this,
104             CQ = Ext.ComponentQuery;
105
106         me.addEvents(
107             /**
108              * @event beforecomponentfocus
109              * Fires before a component becomes focused. Return `false` to prevent
110              * the component from gaining focus.
111              * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
112              * @param {Ext.Component} cmp The component that is being focused
113              * @param {Ext.Component} previousCmp The component that was previously focused,
114              * or `undefined` if there was no previously focused component.
115              */
116             'beforecomponentfocus',
117
118             /**
119              * @event componentfocus
120              * Fires after a component becomes focused.
121              * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
122              * @param {Ext.Component} cmp The component that has been focused
123              * @param {Ext.Component} previousCmp The component that was previously focused,
124              * or `undefined` if there was no previously focused component.
125              */
126             'componentfocus',
127
128             /**
129              * @event disable
130              * Fires when the FocusManager is disabled
131              * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
132              */
133             'disable',
134
135             /**
136              * @event enable
137              * Fires when the FocusManager is enabled
138              * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
139              */
140             'enable'
141         );
142
143         // Setup KeyNav that's bound to document to catch all
144         // unhandled/bubbled key events for navigation
145         me.keyNav = Ext.create('Ext.util.KeyNav', Ext.getDoc(), {
146             disabled: true,
147             scope: me,
148
149             backspace: me.focusLast,
150             enter: me.navigateIn,
151             esc: me.navigateOut,
152             tab: me.navigateSiblings
153
154             //space: me.navigateIn,
155             //del: me.focusLast,
156             //left: me.navigateSiblings,
157             //right: me.navigateSiblings,
158             //down: me.navigateSiblings,
159             //up: me.navigateSiblings
160         });
161
162         me.focusData = {};
163         me.subscribers = Ext.create('Ext.util.HashMap');
164         me.focusChain = {};
165
166         // Setup some ComponentQuery pseudos
167         Ext.apply(CQ.pseudos, {
168             focusable: function(cmps) {
169                 var len = cmps.length,
170                     results = [],
171                     i = 0,
172                     c,
173
174                     isFocusable = function(x) {
175                         return x && x.focusable !== false && CQ.is(x, '[rendered]:not([destroying]):not([isDestroyed]):not([disabled]){isVisible(true)}{el && c.el.dom && c.el.isVisible()}');
176                     };
177
178                 for (; i < len; i++) {
179                     c = cmps[i];
180                     if (isFocusable(c)) {
181                         results.push(c);
182                     }
183                 }
184
185                 return results;
186             },
187
188             nextFocus: function(cmps, idx, step) {
189                 step = step || 1;
190                 idx = parseInt(idx, 10);
191
192                 var len = cmps.length,
193                     i = idx + step,
194                     c;
195
196                 for (; i != idx; i += step) {
197                     if (i >= len) {
198                         i = 0;
199                     } else if (i < 0) {
200                         i = len - 1;
201                     }
202
203                     c = cmps[i];
204                     if (CQ.is(c, ':focusable')) {
205                         return [c];
206                     } else if (c.placeholder && CQ.is(c.placeholder, ':focusable')) {
207                         return [c.placeholder];
208                     }
209                 }
210
211                 return [];
212             },
213
214             prevFocus: function(cmps, idx) {
215                 return this.nextFocus(cmps, idx, -1);
216             },
217
218             root: function(cmps) {
219                 var len = cmps.length,
220                     results = [],
221                     i = 0,
222                     c;
223
224                 for (; i < len; i++) {
225                     c = cmps[i];
226                     if (!c.ownerCt) {
227                         results.push(c);
228                     }
229                 }
230
231                 return results;
232             }
233         });
234     },
235
236     /**
237      * Adds the specified xtype to the {@link #whitelist}.
238      * @param {String/String[]} xtype Adds the xtype(s) to the {@link #whitelist}.
239      */
240     addXTypeToWhitelist: function(xtype) {
241         var me = this;
242
243         if (Ext.isArray(xtype)) {
244             Ext.Array.forEach(xtype, me.addXTypeToWhitelist, me);
245             return;
246         }
247
248         if (!Ext.Array.contains(me.whitelist, xtype)) {
249             me.whitelist.push(xtype);
250         }
251     },
252
253     clearComponent: function(cmp) {
254         clearTimeout(this.cmpFocusDelay);
255         if (!cmp.isDestroyed) {
256             cmp.blur();
257         }
258     },
259
260     /**
261      * Disables the FocusManager by turning of all automatic focus management and keyboard navigation
262      */
263     disable: function() {
264         var me = this;
265
266         if (!me.enabled) {
267             return;
268         }
269
270         delete me.options;
271         me.enabled = false;
272
273         Ext.ComponentManager.all.un('add', me.onComponentCreated, me);
274
275         me.removeDOM();
276
277         // Stop handling key navigation
278         me.keyNav.disable();
279
280         // disable focus for all components
281         me.setFocusAll(false);
282
283         me.fireEvent('disable', me);
284     },
285
286     /**
287      * Enables the FocusManager by turning on all automatic focus management and keyboard navigation
288      * @param {Boolean/Object} options Either `true`/`false` to turn on the focus frame, or an object of the following options:
289         - focusFrame : Boolean
290             `true` to show the focus frame around a component when it is focused. Defaults to `false`.
291      * @markdown
292      */
293     enable: function(options) {
294         var me = this;
295
296         if (options === true) {
297             options = { focusFrame: true };
298         }
299         me.options = options = options || {};
300
301         if (me.enabled) {
302             return;
303         }
304
305         // Handle components that are newly added after we are enabled
306         Ext.ComponentManager.all.on('add', me.onComponentCreated, me);
307
308         me.initDOM(options);
309
310         // Start handling key navigation
311         me.keyNav.enable();
312
313         // enable focus for all components
314         me.setFocusAll(true, options);
315
316         // Finally, let's focus our global focus el so we start fresh
317         me.focusEl.focus();
318         delete me.focusedCmp;
319
320         me.enabled = true;
321         me.fireEvent('enable', me);
322     },
323
324     focusLast: function(e) {
325         var me = this;
326
327         if (me.isWhitelisted(me.focusedCmp)) {
328             return true;
329         }
330
331         // Go back to last focused item
332         if (me.previousFocusedCmp) {
333             me.previousFocusedCmp.focus();
334         }
335     },
336
337     getRootComponents: function() {
338         var me = this,
339             CQ = Ext.ComponentQuery,
340             inline = CQ.query(':focusable:root:not([floating])'),
341             floating = CQ.query(':focusable:root[floating]');
342
343         // Floating items should go to the top of our root stack, and be ordered
344         // by their z-index (highest first)
345         floating.sort(function(a, b) {
346             return a.el.getZIndex() > b.el.getZIndex();
347         });
348
349         return floating.concat(inline);
350     },
351
352     initDOM: function(options) {
353         var me = this,
354             sp = '&#160',
355             cls = me.focusFrameCls;
356
357         if (!Ext.isReady) {
358             Ext.onReady(me.initDOM, me);
359             return;
360         }
361
362         // Create global focus element
363         if (!me.focusEl) {
364             me.focusEl = Ext.getBody().createChild({
365                 tabIndex: '-1',
366                 cls: me.focusElementCls,
367                 html: sp
368             });
369         }
370
371         // Create global focus frame
372         if (!me.focusFrame && options.focusFrame) {
373             me.focusFrame = Ext.getBody().createChild({
374                 cls: cls,
375                 children: [
376                     { cls: cls + '-top' },
377                     { cls: cls + '-bottom' },
378                     { cls: cls + '-left' },
379                     { cls: cls + '-right' }
380                 ],
381                 style: 'top: -100px; left: -100px;'
382             });
383             me.focusFrame.setVisibilityMode(Ext.Element.DISPLAY);
384             me.focusFrameWidth = 2;
385             me.focusFrame.hide().setLeftTop(0, 0);
386         }
387     },
388
389     isWhitelisted: function(cmp) {
390         return cmp && Ext.Array.some(this.whitelist, function(x) {
391             return cmp.isXType(x);
392         });
393     },
394
395     navigateIn: function(e) {
396         var me = this,
397             focusedCmp = me.focusedCmp,
398             rootCmps,
399             firstChild;
400
401         if (!focusedCmp) {
402             // No focus yet, so focus the first root cmp on the page
403             rootCmps = me.getRootComponents();
404             if (rootCmps.length) {
405                 rootCmps[0].focus();
406             }
407         } else {
408             // Drill into child ref items of the focused cmp, if applicable.
409             // This works for any Component with a getRefItems implementation.
410             firstChild = Ext.ComponentQuery.query('>:focusable', focusedCmp)[0];
411             if (firstChild) {
412                 firstChild.focus();
413             } else {
414                 // Let's try to fire a click event, as if it came from the mouse
415                 if (Ext.isFunction(focusedCmp.onClick)) {
416                     e.button = 0;
417                     focusedCmp.onClick(e);
418                     focusedCmp.focus();
419                 }
420             }
421         }
422     },
423
424     navigateOut: function(e) {
425         var me = this,
426             parent;
427
428         if (!me.focusedCmp || !(parent = me.focusedCmp.up(':focusable'))) {
429             me.focusEl.focus();
430         } else {
431             parent.focus();
432         }
433
434         // In some browsers (Chrome) FocusManager can handle this before other
435         // handlers. Ext Windows have their own Esc key handling, so we need to
436         // return true here to allow the event to bubble.
437         return true;
438     },
439
440     navigateSiblings: function(e, source, parent) {
441         var me = this,
442             src = source || me,
443             key = e.getKey(),
444             EO = Ext.EventObject,
445             goBack = e.shiftKey || key == EO.LEFT || key == EO.UP,
446             checkWhitelist = key == EO.LEFT || key == EO.RIGHT || key == EO.UP || key == EO.DOWN,
447             nextSelector = goBack ? 'prev' : 'next',
448             idx, next, focusedCmp;
449
450         focusedCmp = (src.focusedCmp && src.focusedCmp.comp) || src.focusedCmp;
451         if (!focusedCmp && !parent) {
452             return;
453         }
454
455         if (checkWhitelist && me.isWhitelisted(focusedCmp)) {
456             return true;
457         }
458
459         parent = parent || focusedCmp.up();
460         if (parent) {
461             idx = focusedCmp ? Ext.Array.indexOf(parent.getRefItems(), focusedCmp) : -1;
462             next = Ext.ComponentQuery.query('>:' + nextSelector + 'Focus(' + idx + ')', parent)[0];
463             if (next && focusedCmp !== next) {
464                 next.focus();
465                 return next;
466             }
467         }
468     },
469
470     onComponentBlur: function(cmp, e) {
471         var me = this;
472
473         if (me.focusedCmp === cmp) {
474             me.previousFocusedCmp = cmp;
475             delete me.focusedCmp;
476         }
477
478         if (me.focusFrame) {
479             me.focusFrame.hide();
480         }
481     },
482
483     onComponentCreated: function(hash, id, cmp) {
484         this.setFocus(cmp, true, this.options);
485     },
486
487     onComponentDestroy: function(cmp) {
488         this.setFocus(cmp, false);
489     },
490
491     onComponentFocus: function(cmp, e) {
492         var me = this,
493             chain = me.focusChain;
494
495         if (!Ext.ComponentQuery.is(cmp, ':focusable')) {
496             me.clearComponent(cmp);
497
498             // Check our focus chain, so we don't run into a never ending recursion
499             // If we've attempted (unsuccessfully) to focus this component before,
500             // then we're caught in a loop of child->parent->...->child and we
501             // need to cut the loop off rather than feed into it.
502             if (chain[cmp.id]) {
503                 return;
504             }
505
506             // Try to focus the parent instead
507             var parent = cmp.up();
508             if (parent) {
509                 // Add component to our focus chain to detect infinite focus loop
510                 // before we fire off an attempt to focus our parent.
511                 // See the comments above.
512                 chain[cmp.id] = true;
513                 parent.focus();
514             }
515
516             return;
517         }
518
519         // Clear our focus chain when we have a focusable component
520         me.focusChain = {};
521
522         // Defer focusing for 90ms so components can do a layout/positioning
523         // and give us an ability to buffer focuses
524         clearTimeout(me.cmpFocusDelay);
525         if (arguments.length !== 2) {
526             me.cmpFocusDelay = Ext.defer(me.onComponentFocus, 90, me, [cmp, e]);
527             return;
528         }
529
530         if (me.fireEvent('beforecomponentfocus', me, cmp, me.previousFocusedCmp) === false) {
531             me.clearComponent(cmp);
532             return;
533         }
534
535         me.focusedCmp = cmp;
536
537         // If we have a focus frame, show it around the focused component
538         if (me.shouldShowFocusFrame(cmp)) {
539             var cls = '.' + me.focusFrameCls + '-',
540                 ff = me.focusFrame,
541                 fw = me.focusFrameWidth,
542                 box = cmp.el.getPageBox(),
543
544             // Size the focus frame's t/b/l/r according to the box
545             // This leaves a hole in the middle of the frame so user
546             // interaction w/ the mouse can continue
547                 bt = box.top,
548                 bl = box.left,
549                 bw = box.width,
550                 bh = box.height,
551                 ft = ff.child(cls + 'top'),
552                 fb = ff.child(cls + 'bottom'),
553                 fl = ff.child(cls + 'left'),
554                 fr = ff.child(cls + 'right');
555
556             ft.setWidth(bw).setLeftTop(bl, bt);
557             fb.setWidth(bw).setLeftTop(bl, bt + bh - fw);
558             fl.setHeight(bh - fw - fw).setLeftTop(bl, bt + fw);
559             fr.setHeight(bh - fw - fw).setLeftTop(bl + bw - fw, bt + fw);
560
561             ff.show();
562         }
563
564         me.fireEvent('componentfocus', me, cmp, me.previousFocusedCmp);
565     },
566
567     onComponentHide: function(cmp) {
568         var me = this,
569             CQ = Ext.ComponentQuery,
570             cmpHadFocus = false,
571             focusedCmp,
572             parent;
573
574         if (me.focusedCmp) {
575             focusedCmp = CQ.query('[id=' + me.focusedCmp.id + ']', cmp)[0];
576             cmpHadFocus = me.focusedCmp.id === cmp.id || focusedCmp;
577
578             if (focusedCmp) {
579                 me.clearComponent(focusedCmp);
580             }
581         }
582
583         me.clearComponent(cmp);
584
585         if (cmpHadFocus) {
586             parent = CQ.query('^:focusable', cmp)[0];
587             if (parent) {
588                 parent.focus();
589             }
590         }
591     },
592
593     removeDOM: function() {
594         var me = this;
595
596         // If we are still enabled globally, or there are still subscribers
597         // then we will halt here, since our DOM stuff is still being used
598         if (me.enabled || me.subscribers.length) {
599             return;
600         }
601
602         Ext.destroy(
603             me.focusEl,
604             me.focusFrame
605         );
606         delete me.focusEl;
607         delete me.focusFrame;
608         delete me.focusFrameWidth;
609     },
610
611     /**
612      * Removes the specified xtype from the {@link #whitelist}.
613      * @param {String/String[]} xtype Removes the xtype(s) from the {@link #whitelist}.
614      */
615     removeXTypeFromWhitelist: function(xtype) {
616         var me = this;
617
618         if (Ext.isArray(xtype)) {
619             Ext.Array.forEach(xtype, me.removeXTypeFromWhitelist, me);
620             return;
621         }
622
623         Ext.Array.remove(me.whitelist, xtype);
624     },
625
626     setFocus: function(cmp, focusable, options) {
627         var me = this,
628             el, dom, data,
629
630             needsTabIndex = function(n) {
631                 return !Ext.Array.contains(me.tabIndexWhitelist, n.tagName.toLowerCase())
632                     && n.tabIndex <= 0;
633             };
634
635         options = options || {};
636
637         // Come back and do this after the component is rendered
638         if (!cmp.rendered) {
639             cmp.on('afterrender', Ext.pass(me.setFocus, arguments, me), me, { single: true });
640             return;
641         }
642
643         el = cmp.getFocusEl();
644         dom = el.dom;
645
646         // Decorate the component's focus el for focus-ability
647         if ((focusable && !me.focusData[cmp.id]) || (!focusable && me.focusData[cmp.id])) {
648             if (focusable) {
649                 data = {
650                     focusFrame: options.focusFrame
651                 };
652
653                 // Only set -1 tabIndex if we need it
654                 // inputs, buttons, and anchor tags do not need it,
655                 // and neither does any DOM that has it set already
656                 // programmatically or in markup.
657                 if (needsTabIndex(dom)) {
658                     data.tabIndex = dom.tabIndex;
659                     dom.tabIndex = -1;
660                 }
661
662                 el.on({
663                     focus: data.focusFn = Ext.bind(me.onComponentFocus, me, [cmp], 0),
664                     blur: data.blurFn = Ext.bind(me.onComponentBlur, me, [cmp], 0),
665                     scope: me
666                 });
667                 cmp.on({
668                     hide: me.onComponentHide,
669                     close: me.onComponentHide,
670                     beforedestroy: me.onComponentDestroy,
671                     scope: me
672                 });
673
674                 me.focusData[cmp.id] = data;
675             } else {
676                 data = me.focusData[cmp.id];
677                 if ('tabIndex' in data) {
678                     dom.tabIndex = data.tabIndex;
679                 }
680                 el.un('focus', data.focusFn, me);
681                 el.un('blur', data.blurFn, me);
682                 cmp.un('hide', me.onComponentHide, me);
683                 cmp.un('close', me.onComponentHide, me);
684                 cmp.un('beforedestroy', me.onComponentDestroy, me);
685
686                 delete me.focusData[cmp.id];
687             }
688         }
689     },
690
691     setFocusAll: function(focusable, options) {
692         var me = this,
693             cmps = Ext.ComponentManager.all.getArray(),
694             len = cmps.length,
695             cmp,
696             i = 0;
697
698         for (; i < len; i++) {
699             me.setFocus(cmps[i], focusable, options);
700         }
701     },
702
703     setupSubscriberKeys: function(container, keys) {
704         var me = this,
705             el = container.getFocusEl(),
706             scope = keys.scope,
707             handlers = {
708                 backspace: me.focusLast,
709                 enter: me.navigateIn,
710                 esc: me.navigateOut,
711                 scope: me
712             },
713
714             navSiblings = function(e) {
715                 if (me.focusedCmp === container) {
716                     // Root the sibling navigation to this container, so that we
717                     // can automatically dive into the container, rather than forcing
718                     // the user to hit the enter key to dive in.
719                     return me.navigateSiblings(e, me, container);
720                 } else {
721                     return me.navigateSiblings(e);
722                 }
723             };
724
725         Ext.iterate(keys, function(key, cb) {
726             handlers[key] = function(e) {
727                 var ret = navSiblings(e);
728
729                 if (Ext.isFunction(cb) && cb.call(scope || container, e, ret) === true) {
730                     return true;
731                 }
732
733                 return ret;
734             };
735         }, me);
736
737         return Ext.create('Ext.util.KeyNav', el, handlers);
738     },
739
740     shouldShowFocusFrame: function(cmp) {
741         var me = this,
742             opts = me.options || {};
743
744         if (!me.focusFrame || !cmp) {
745             return false;
746         }
747
748         // Global trumps
749         if (opts.focusFrame) {
750             return true;
751         }
752
753         if (me.focusData[cmp.id].focusFrame) {
754             return true;
755         }
756
757         return false;
758     },
759
760     /**
761      * Subscribes an {@link Ext.container.Container} to provide basic keyboard focus navigation between its child {@link Ext.Component}'s.
762      * @param {Ext.container.Container} container A reference to the {@link Ext.container.Container} on which to enable keyboard functionality and focus management.
763      * @param {Boolean/Object} options An object of the following options
764      * @param {Array/Object} options.keys
765      * An array containing the string names of navigation keys to be supported. The allowed values are:
766      *
767      *   - 'left'
768      *   - 'right'
769      *   - 'up'
770      *   - 'down'
771      *
772      * Or, an object containing those key names as keys with `true` or a callback function as their value. A scope may also be passed. E.g.:
773      *
774      *     {
775      *         left: this.onLeftKey,
776      *         right: this.onRightKey,
777      *         scope: this
778      *     }
779      *
780      * @param {Boolean} options.focusFrame
781      * `true` to show the focus frame around a component when it is focused. Defaults to `false`.
782      */
783     subscribe: function(container, options) {
784         var me = this,
785             EA = Ext.Array,
786             data = {},
787             subs = me.subscribers,
788
789             // Recursively add focus ability as long as a descendent container isn't
790             // itself subscribed to the FocusManager, or else we'd have unwanted side
791             // effects for subscribing a descendent container twice.
792             safeSetFocus = function(cmp) {
793                 if (cmp.isContainer && !subs.containsKey(cmp.id)) {
794                     EA.forEach(cmp.query('>'), safeSetFocus);
795                     me.setFocus(cmp, true, options);
796                     cmp.on('add', data.onAdd, me);
797                 } else if (!cmp.isContainer) {
798                     me.setFocus(cmp, true, options);
799                 }
800             };
801
802         // We only accept containers
803         if (!container || !container.isContainer) {
804             return;
805         }
806
807         if (!container.rendered) {
808             container.on('afterrender', Ext.pass(me.subscribe, arguments, me), me, { single: true });
809             return;
810         }
811
812         // Init the DOM, incase this is the first time it will be used
813         me.initDOM(options);
814
815         // Create key navigation for subscriber based on keys option
816         data.keyNav = me.setupSubscriberKeys(container, options.keys);
817
818         // We need to keep track of components being added to our subscriber
819         // and any containers nested deeply within it (omg), so let's do that.
820         // Components that are removed are globally handled.
821         // Also keep track of destruction of our container for auto-unsubscribe.
822         data.onAdd = function(ct, cmp, idx) {
823             safeSetFocus(cmp);
824         };
825         container.on('beforedestroy', me.unsubscribe, me);
826
827         // Now we setup focusing abilities for the container and all its components
828         safeSetFocus(container);
829
830         // Add to our subscribers list
831         subs.add(container.id, data);
832     },
833
834     /**
835      * Unsubscribes an {@link Ext.container.Container} from keyboard focus management.
836      * @param {Ext.container.Container} container A reference to the {@link Ext.container.Container} to unsubscribe from the FocusManager.
837      */
838     unsubscribe: function(container) {
839         var me = this,
840             EA = Ext.Array,
841             subs = me.subscribers,
842             data,
843
844             // Recursively remove focus ability as long as a descendent container isn't
845             // itself subscribed to the FocusManager, or else we'd have unwanted side
846             // effects for unsubscribing an ancestor container.
847             safeSetFocus = function(cmp) {
848                 if (cmp.isContainer && !subs.containsKey(cmp.id)) {
849                     EA.forEach(cmp.query('>'), safeSetFocus);
850                     me.setFocus(cmp, false);
851                     cmp.un('add', data.onAdd, me);
852                 } else if (!cmp.isContainer) {
853                     me.setFocus(cmp, false);
854                 }
855             };
856
857         if (!container || !subs.containsKey(container.id)) {
858             return;
859         }
860
861         data = subs.get(container.id);
862         data.keyNav.destroy();
863         container.un('beforedestroy', me.unsubscribe, me);
864         subs.removeAtKey(container.id);
865         safeSetFocus(container);
866         me.removeDOM();
867     }
868 });