]>
Commit | Line | Data |
---|---|---|
f0cfe83e AD |
1 | define("dijit/a11yclick", [ |
2 | "dojo/on", | |
3 | "dojo/_base/array", // array.forEach | |
4 | "dojo/keys", // keys.ENTER keys.SPACE | |
5 | "dojo/_base/declare", // declare | |
6 | "dojo/has", // has("dom-addeventlistener") | |
7 | "dojo/_base/unload", // unload.addOnWindowUnload | |
8 | "dojo/_base/window" // win.doc.addEventListener win.doc.attachEvent win.doc.detachEvent | |
9 | ], function(on, array, keys, declare, has, unload, win){ | |
10 | ||
11 | // module: | |
12 | // dijit/a11yclick | |
13 | ||
14 | // Keep track of where the last keydown event was, to help avoid generating | |
15 | // spurious ondijitclick events when: | |
16 | // 1. focus is on a <button> or <a> | |
17 | // 2. user presses then releases the ENTER key | |
18 | // 3. onclick handler fires and shifts focus to another node, with an ondijitclick handler | |
19 | // 4. onkeyup event fires, causing the ondijitclick handler to fire | |
20 | var lastKeyDownNode = null; | |
21 | if(has("dom-addeventlistener")){ | |
22 | win.doc.addEventListener('keydown', function(evt){ | |
23 | lastKeyDownNode = evt.target; | |
24 | }, true); | |
25 | }else{ | |
26 | // Fallback path for IE6-8 | |
27 | (function(){ | |
28 | var keydownCallback = function(evt){ | |
29 | lastKeyDownNode = evt.srcElement; | |
30 | }; | |
31 | win.doc.attachEvent('onkeydown', keydownCallback); | |
32 | unload.addOnWindowUnload(function(){ | |
33 | win.doc.detachEvent('onkeydown', keydownCallback); | |
34 | }); | |
35 | })(); | |
36 | } | |
37 | ||
38 | function clickKey(/*Event*/ e){ | |
39 | return (e.keyCode === keys.ENTER || e.keyCode === keys.SPACE) && | |
40 | !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey; | |
41 | } | |
42 | ||
43 | return function(node, listener){ | |
44 | // summary: | |
45 | // Custom a11yclick (a.k.a. ondijitclick) event | |
46 | // which triggers on a mouse click, touch, or space/enter keyup. | |
47 | ||
48 | if(/input|button/i.test(node.nodeName)){ | |
49 | // pass through, the browser already generates click event on SPACE/ENTER key | |
50 | return on(node, "click", listener); | |
51 | }else{ | |
52 | // Don't fire the click event unless both the keydown and keyup occur on this node. | |
53 | // Avoids problems where focus shifted to this node or away from the node on keydown, | |
54 | // either causing this node to process a stray keyup event, or causing another node | |
55 | // to get a stray keyup event. | |
56 | ||
57 | var handles = [ | |
58 | on(node, "keydown", function(e){ | |
59 | //console.log(this.id + ": onkeydown, e.target = ", e.target, ", lastKeyDownNode was ", lastKeyDownNode, ", equality is ", (e.target === lastKeyDownNode)); | |
60 | if(clickKey(e)){ | |
61 | // needed on IE for when focus changes between keydown and keyup - otherwise dropdown menus do not work | |
62 | lastKeyDownNode = e.target; | |
63 | ||
64 | // Prevent viewport scrolling on space key in IE<9. | |
65 | // (Reproducible on test_Button.html on any of the first dijit/form/Button examples) | |
66 | e.preventDefault(); | |
67 | } | |
68 | }), | |
69 | ||
70 | on(node, "keyup", function(e){ | |
71 | //console.log(this.id + ": onkeyup, e.target = ", e.target, ", lastKeyDownNode was ", lastKeyDownNode, ", equality is ", (e.target === lastKeyDownNode)); | |
72 | if(clickKey(e) && e.target == lastKeyDownNode){ // === breaks greasemonkey | |
73 | //need reset here or have problems in FF when focus returns to trigger element after closing popup/alert | |
74 | lastKeyDownNode = null; | |
75 | on.emit(e.target, "click", { | |
76 | cancelable: true, | |
77 | bubbles: true | |
78 | }); | |
79 | } | |
80 | }), | |
81 | ||
82 | on(node, "click", function(e){ | |
83 | // catch mouse clicks, plus the on.emit() calls from above and below | |
84 | listener.call(this, e); | |
85 | }) | |
86 | ]; | |
87 | ||
88 | if(has("touch")){ | |
89 | // touchstart-->touchend will automatically generate a click event, but there are problems | |
90 | // on iOS after focus has been programatically shifted (#14604, #14918), so setup a failsafe | |
91 | // if click doesn't fire naturally. | |
92 | ||
93 | var clickTimer; | |
94 | handles.push( | |
95 | on(node, "touchend", function(e){ | |
96 | var target = e.target; | |
97 | clickTimer = setTimeout(function(){ | |
98 | clickTimer = null; | |
99 | on.emit(target, "click", { | |
100 | cancelable: true, | |
101 | bubbles: true | |
102 | }); | |
103 | }, 600); | |
104 | }), | |
105 | on(node, "click", function(e){ | |
106 | // If browser generates a click naturally, clear the timer to fire a synthetic click event | |
107 | if(clickTimer){ | |
108 | clearTimeout(clickTimer); | |
109 | } | |
110 | }) | |
111 | // TODO: if the touchstart and touchend were <100ms apart, and then there's another touchstart | |
112 | // event <300ms after the touchend event, then clear the synthetic click timer, because user | |
113 | // is doing a zoom. Alternately monitor screen.deviceXDPI (or something similar) to see if | |
114 | // zoom level has changed. | |
115 | ); | |
116 | } | |
117 | ||
118 | return { | |
119 | remove: function(){ | |
120 | array.forEach(handles, function(h){ h.remove(); }); | |
121 | if(clickTimer){ | |
122 | clearTimeout(clickTimer); | |
123 | clickTimer = null; | |
124 | } | |
125 | } | |
126 | }; | |
127 | } | |
128 | }; | |
129 | ||
130 | return ret; | |
131 | }); |