Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / docs / guides / keyboard_nav / README.md
1 # Keyboard Navigation
2 ______________________________________________
3
4 Navigating with your keyboard is often faster than using the cursor and is useful for both power users and to provide accessibility to users that find a mouse difficult to use.
5
6 We're going to convert a modified version of the relatively complicated Ext JS [complex layout example](#!/example/layout/complex.html) into an application that is fully accessible through the keyboard. We'll also add keyboard shortcuts to potentially make navigation via the keyboard faster than the cursor.
7
8 By the end of this guide you will have an understanding of where keyboard navigation is most needed and how to utilize keyboard navigation with {@link Ext.util.KeyNav KeyNav}, {@link Ext.util.KeyMap KeyMap} and the {@link Ext.FocusManager FocusManager}.
9
10 ## Getting Started
11
12 [These are the files we'll be starting from](guides/keyboard_nav/start.zip). Unzip them and open complex.html and complex.js in your preferred editor. Also unzip a copy of Ext JS 4 into the same directory and rename it 'ext'.
13
14 ## The Focus Manager
15
16 The Focus Manager provides a very quick way to enable basic keyboard navigation. What's more, it's simple to implement:
17
18     Ext.FocusManager.enable(true);
19
20 Write this line inside {@link Ext#onReady Ext.onReady}; we pass the {@link Boolean} `true` to show a "visual cue" in the form of a blue ring around the focused area (this is known as a focus frame). This focus frame makes it easy to see at a glance which area has focus. The user presses Enter to enter the application and then move up and down 'levels', with Enter to go down a level and Backspace or Escape to go up a level. The Tab key can be used to jump between sibling elements (those that are on the same level).
21
22 Experiment with navigating around the elements solely with the FocusManager. If you can, turn off your mouse too. Although adequate, you'll probably find that certain areas are either inaccessible (such as the grid) or it's quite cumbersome to get around the screen. We're going to tackle this with 'shortcuts' which will allow users to jump to certain panels of the application with ease.
23
24 When deciding which panels should have shortcuts to them it's useful to have some criteria to go by:
25
26  - Is it often used?
27  - Could it be used as an anchor - that is, could it provide a stepping stone to make other remote areas easier to get to?
28  - Does it feel intuitive to navigate to?
29
30 If the answer is yes to at least one of these, give it a keyboard shortcut and aid your end-users.
31
32 ## KeyNav
33
34 It's the job of KeyNav to provide easy keyboard navigation. It allows you to use the following keys to navigate through your Ext JS application:
35
36  - Enter
37  - Space
38  - Left
39  - Right
40  - Up
41  - Down
42  - Tab
43  - Escape
44  - Page Up
45  - Page Down
46  - Delete
47  - Backspace
48  - Home
49  - End
50
51 It's also worth keeping in mind users with limited keyboards that might not have certain keys, for example certain Apple computers don't have Page Up, Page Down, Del, Home or End keys. Let's take a look at an example of how you'd use it.
52
53     var nav = Ext.create('Ext.util.KeyNav', "my-element", {
54         "left" : function(e){
55             this.moveLeft(e.ctrlKey);
56         },
57         "right" : function(e){
58             this.moveRight(e.ctrlKey);
59         },
60         "enter" : function(e){
61             this.save();
62         },
63         scope : this
64     });
65
66 KeyNav's speciality is listening for arrow keys, so we're going to add the ability to navigate around panels with the arrow keys instead of having to use tab, enter and escape.
67
68     var nav = Ext.create('Ext.util.KeyNav', Ext.getBody(), {
69         "left" : function(){
70             var el = Ext.FocusManager.focusedCmp;
71             if (el.previousSibling()) el.previousSibling().focus();
72         },
73         "right" : function(){
74             var el = Ext.FocusManager.focusedCmp;
75             if (el.nextSibling()) el.nextSibling().focus();
76         },
77         "up" : function() {
78             var el = Ext.FocusManager.focusedCmp;
79             if (el.up()) el.up().focus();
80         },
81         "down" : function() {
82             var el = Ext.FocusManager.focusedCmp;
83             if (el.items) el.items.items[0].focus();
84         },
85         scope : this
86     });
87
88 We get the currently focused component with {@link Ext.FocusManager#focusedCmp focusedCmp}. If the function has a value other than `null`, we focus on the element that we want, be it the previous sibling with the Left arrow key or the first child component with the Down key. Already we've made our application much easier to navigate. Next, we're going to look at Ext.util.KeyMap and how to add specific functionality to keys.
89
90 ## KeyMap
91
92 You'll notice that there are many regions to our Ext application: North, South, East, and West. We're going to create a {@link Ext.util.KeyMap KeyMap} that will not only focus on these elements but, if the region is collapsed, expand it too. Let's have a look at what a typical KeyMap object looks like.
93
94     var map = Ext.create('Ext.util.KeyMap', "my-element", {
95         key: 13, // or Ext.EventObject.ENTER
96         ctrl: true,
97         shift: false,
98         fn: myHandler,
99         scope: myObject
100     });
101
102 The first property, `key` is the numeric keycode that maps a key. A useful document that maps which numbers correlate to which keys can be [found here](ext-js/4-0/source/EventObject.html). The next two, `ctrl` and `shift`, specify if the respective key is required to be held down to activate the function. In our case ctrl does, so ctrl+Enter will invoke `myHandler`. `fn` is the function to be called. This can either be inline or a reference to a function. Finally, `scope` defines where this `KeyMap` will be effective.
103
104 `KeyMap` is versatile in that it allows you to specify either one key that carries out a function or an array of keys that carry out the same function. If we wanted a number of keys to invoke `myHandler` we'd write it like `key: [10, 13]`.
105
106 We'll start by concentrating on the main panels: north, south, east and west.
107
108     var map = Ext.create('Ext.util.KeyMap', Ext.getBody(), [
109         {
110             key: Ext.EventObject.E, // E for east
111             shift: true,
112             ctrl: false, // explicitly set as false to avoid collisions
113             fn: function() {
114                 var parentPanel = eastPanel;
115                 expand(parentPanel);
116             }
117         },
118         {
119             key: Ext.EventObject.W, // W for west
120             shift: true,
121             ctrl: false,
122             fn: function() {
123                 var parentPanel = westPanel;
124                 expand(parentPanel);
125             }
126         },
127         {
128             key: Ext.EventObject.S, // S for south
129             shift: true,
130             ctrl: false,
131             fn: function() {
132                 var parentPanel = southPanel;
133                 expand(parentPanel);
134             }
135         }
136     ]);
137
138 We use {@link Ext.EventObject Ext.EventObject.X} to make it obvious which key we're listening for, the rest should be clear from the example above. Then we write the `expand()` function underneath:
139
140     function expand(parentPanel) {
141         parentPanel.toggleCollapse();
142         parentPanel.on('expand', function(){
143             parentPanel.el.focus();
144         });
145         parentPanel.on('collapse', function(){
146             viewport.el.focus();
147         });
148     }
149
150 This function toggles the collapsing of the panel and focuses on it if it's been expanded, or collapses it if it's already expanded, returning focus to the next level up, the `viewport`.
151
152 Now that all of the code is in place, try expanding and collapsing the panels with a key press versus clicking on the small button that expands or collapses them.  It's much faster with the keyboard!
153
154 Next, we'll go through a similar process with the Navigation, Settings and Information tabs on the West Panel. I've called them subPanels because they're children to the other `parentPanels` that we've seen already.
155
156     {
157         key: Ext.EventObject.S, // S for settings
158         ctrl: true,
159         fn: function() {
160             var parentPanel = westPanel;
161             var subPanel = settings;
162             expand(parentPanel, subPanel);
163         }
164     },
165     {
166         key: Ext.EventObject.I, // I for information
167         ctrl: true,
168         fn: function() {
169             var parentPanel = westPanel;
170             var subPanel = information;
171             expand(parentPanel, subPanel);
172         }
173     },
174     {
175         key: Ext.EventObject.N, // N for navigation
176         ctrl: true,
177         fn: function(){
178             var parentPanel = westPanel;
179             var subPanel = navigation;
180             expand(parentPanel, subPanel);
181         }
182     }
183
184 We follow the same pattern as we've used before but added a variable called `subPanel`. Our `expand` function won't know what to do with these so we'll refactor it to act accordingly depending on whether `subPanel` is declared or not.
185
186     function expand(parentPanel, subPanel) {
187
188         if (subPanel) {
189             function subPanelExpand(subPanel) {
190                 // set listener for expand function
191                 subPanel.on('expand', function() {
192                     setTimeout(function() { subPanel.focus(); }, 200);
193                 });
194                 // expand the subPanel
195                 subPanel.expand();
196             }
197
198             if (parentPanel.collapsed) {
199                 // enclosing panel is collapsed, open it
200                 parentPanel.expand();
201                 subPanelExpand(subPanel);
202             }
203             else if (!subPanel.collapsed) {
204                 // subPanel is open and just needs focusing
205                 subPanel.focus();
206             }
207             else {
208                 // parentPanel isn't collapsed but subPanel is
209                 subPanelExpand(subPanel);
210             }
211         }
212         else {
213             // no subPanel detected
214             parentPanel.toggleCollapse();
215             parentPanel.on('expand', function(){
216                 parentPanel.el.focus();
217             });
218             parentPanel.on('collapse', function(){
219                 viewport.el.focus();
220             });
221         }
222     }
223
224 Despite `focus` being in the `expand` event listener, which is meant to fire _after_ the panel has been expanded, it needs wrapping in a `setTimeout` because otherwise it focuses too early resulting in a focus frame smaller than the panel (that is, it focuses while it's expanding). Compensating 200 milliseconds gets around this; this problem isn't present with the `parentPanel`s.
225
226 At this point you can open and close panels, as well as focus them, purely with the keyboard (e.g. shift+e or ctrl+s). Naturally, any function of Ext JS can be triggered by a key press leading to many possibilities for a more native-feeling application.
227
228 There's one last object to add to our `KeyMap`, there are two tabs, one on the center panel and the other next to the 'Eye Data' tab. It would be useful to be able to close these as you would a tab in a browser with ctrl+w.
229
230     {
231         key: Ext.EventObject.W, // W to close
232         ctrl: true,
233         fn: function(){
234             var el = Ext.FocusManager.focusedCmp;
235             if (el.xtype === 'tab' && el.closable) {
236                 el.up().focus();
237                 el.destroy();
238             }
239         },
240         scope: this
241     }
242
243 We've configured which key presses we're listening for and get which component is currently focused with the Focus Manager's {@link Ext.FocusManager#focusedCmp focusedCmp} property. If the currently focused component is a tab and it's closable, then we set the focus to the parent panel and destroy the tab.
244
245 ### Fixing the Grid
246
247 You may have noticed that when we try to focus a row in the grid it isn't possible without the mouse. If you look in the console we get a clue as to why this is, it reports that "pos is undefined". The click event passes information on the record, including what its position in the grid is. However, using the FocusManager, it doesn't pass this information so we need to emulate it by passing an object that specifies the `row` and `column` properties. Do the following underneath the `viewport` variable:
248
249     var easttab = Ext.getCmp('easttab');
250
251     var gridMap = Ext.create('Ext.util.KeyMap', 'eastPanel', [
252         {
253             key: '\r', // Return key
254             fn: function() {
255                 easttab.getSelectionModel().setCurrentPosition({row: 0, column: 1});
256             },
257             scope: 'eastPanel'
258         },
259         {
260             key: Ext.EventObject.ESC,
261             fn: function() {
262                 easttab.el.focus();
263             },
264             scope: 'eastPanel'
265         }
266     ]);
267
268 Try this and you'll see that we can successfully enter and exit the grid using the keyboard. It's important that we specify the `scope` on these keys otherwise you won't be able to escape from the grid.
269
270 ### Toggling Keyboard Mapping
271
272 A useful feature of `KeyMap` is that it can easily be enabled or disabled. It could be that you don't want most users of your application to have a focus frame when they click on an element, so you could make a button (or another `KeyMap`) that enables this behavior.
273
274 If you wanted to add a global key press that would turn on and off keyboard navigation you could do so with the following:
275
276     var initMap = Ext.create('Ext.util.KeyMap', Ext.getBody(), {
277         key: Ext.EventObject.T, // T for toggle
278         shift: true,
279         fn: function(){
280             map.enabled ? map.disable() : map.enable();
281             Ext.FocusManager.enabled ? Ext.FocusManager.disable() : Ext.FocusManager.enable(true);
282         }
283     });
284
285 We've created a new `KeyMap` that will turn off keyboard navigation with shift+t and turn it back on with the same. We aren't able to use the existing `KeyMap` because it would be turning itself off and wouldn't be able to be reinitialized.
286
287 ## Conclusion
288
289 We've converted a complex series of panels that would have otherwise been inaccessible to use the keyboard. We've also encountered some examples where we've had to add some custom functionality on top of the Focus Manager.
290
291 With `KeyMap`, we've learnt that we can jump to different Panels as well as invoke any functionality that we'd usually write with a keystroke. Finally, with `KeyNav` we've seen how easy it is to move around an application with arrow keys.