From 586d393fd3d72f99dd16dfba42d1d2889fcd09f6 Mon Sep 17 00:00:00 2001 From: rwijaya Date: Mon, 7 Oct 2013 12:40:16 +0800 Subject: [PATCH] MDL-35926 notification dialog: trap tab focus within dialog modal. --- ...moodle-core-notification-dialogue-debug.js | 45 ++++++++++++++++++- .../moodle-core-notification-dialogue-min.js | 2 +- .../moodle-core-notification-dialogue.js | 45 ++++++++++++++++++- lib/yui/src/notification/js/dialogue.js | 45 ++++++++++++++++++- 4 files changed, 130 insertions(+), 7 deletions(-) diff --git a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js index e5024950533..e91c1ba03c9 100644 --- a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js +++ b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js @@ -41,7 +41,8 @@ var DIALOGUE_NAME = 'Moodle dialogue', DIALOGUE_FULLSCREEN_CLASS = DIALOGUE_PREFIX + '-fullscreen', DIALOGUE_HIDDEN_CLASS = DIALOGUE_PREFIX + '-hidden', DIALOGUE_SELECTOR =' [role=dialog]', - MENUBAR_SELECTOR = '[role=menubar]'; + MENUBAR_SELECTOR = '[role=menubar]', + CAN_RECEIVE_FOCUS_SELECTOR = 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]'; /** * A re-usable dialogue box with Moodle classes applied. @@ -143,6 +144,7 @@ Y.extend(DIALOGUE, Y.Panel, { // been positioned it will scroll back to the top of the page. if (config.visible) { this.show(); + this.keyDelegation(); } }, @@ -198,10 +200,11 @@ Y.extend(DIALOGUE, Y.Panel, { * @return void */ visibilityChanged : function(e) { - var titlebar; + var titlebar, bb; if (e.attrName === 'visible') { this.get('maskNode').addClass(CSS.LIGHTBOX); if (e.prevVal && !e.newVal) { + bb = this.get('boundingBox'); if (this._resizeevent) { this._resizeevent.detach(); this._resizeevent = null; @@ -210,6 +213,7 @@ Y.extend(DIALOGUE, Y.Panel, { this._orientationevent.detach(); this._orientationevent = null; } + bb.detach('key', this.keyDelegation); } if (!e.prevVal && e.newVal) { // This needs to be done each time the dialog is shown as new dialogs may have been opened. @@ -223,6 +227,7 @@ Y.extend(DIALOGUE, Y.Panel, { Y.one(titlebar).setStyle('cursor', 'move'); } } + this.keyDelegation(); } if (this.get('center') && !e.prevVal && e.newVal) { this.centerDialogue(); @@ -326,6 +331,42 @@ Y.extend(DIALOGUE, Y.Panel, { content.focus(); } return result; + }, + /** + * Setup key delegation to keep tabbing within the open dialogue. + * + * @method keyDelegation + */ + keyDelegation : function() { + var bb = this.get('boundingBox'); + bb.delegate('key', function(e){ + var target = e.target; + var direction = 'forward'; + if (e.shiftKey) { + direction = 'backward'; + } + if (this.trapFocus(target, direction)) { + e.preventDefault(); + } + }, 'down:9', CAN_RECEIVE_FOCUS_SELECTOR, this); + }, + /** + * Trap the tab focus within the open modal. + * + * @param string target the element target + * @param string direction tab key for forward and tab+shift for backward + * @returns bool + */ + trapFocus : function(target, direction) { + var bb = this.get('boundingBox'), + firstitem = bb.one(CAN_RECEIVE_FOCUS_SELECTOR), + lastitem = bb.all(CAN_RECEIVE_FOCUS_SELECTOR).pop(); + + if (target === lastitem && direction === 'forward') { // Tab key. + return firstitem.focus(); + } else if (target === firstitem && direction === 'backward') { // Tab+shift key. + return lastitem.focus(); + } } }, { NAME : DIALOGUE_NAME, diff --git a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js index d9fd2f4748a..a576293ad3a 100644 --- a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js +++ b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js @@ -1 +1 @@ -YUI.add("moodle-core-notification-dialogue",function(e,t){var n,r,i,s,o,u,a,f;n="moodle-dialogue",r="notificationBase",i=0,s="yesLabel",o="noLabel",u="title",a="question",f={BASE:"moodle-dialogue-base",WRAP:"moodle-dialogue-wrap",HEADER:"moodle-dialogue-hd",BODY:"moodle-dialogue-bd",CONTENT:"moodle-dialogue-content",FOOTER:"moodle-dialogue-ft",HIDDEN:"hidden",LIGHTBOX:"moodle-dialogue-lightbox"},M.core=M.core||{};var l="Moodle dialogue",c,h=n+"-fullscreen",p=n+"-hidden",d=" [role=dialog]",v="[role=menubar]";c=function(t){i++;var n="moodle-dialogue-"+i;t.notificationBase=e.Node.create('
').append(e.Node.create('').append(e.Node.create('
')).append(e.Node.create('
')).append(e.Node.create('
'))),e.one(document.body).append(t.notificationBase),t.additionalBaseClass&&t.notificationBase.addClass(t.additionalBaseClass),t.srcNode="#"+n,t.width=t.width||"400px",t.visible=t.visible||!1,t.center=t.centered&&!0,t.centered=!1,t.COUNT=i,t.width==="auto"&&delete t.width,t.lightbox!==!1&&(t.modal=!0),delete t.lightbox,t.closeButton===!1?t.buttons=null:t.buttons=[{section:e.WidgetStdMod.HEADER,classNames:"closebutton",action:function(){this.hide()}}],c.superclass.constructor.apply(this,[t]),t.closeButton!==!1&&this.get("buttons").header[0].setAttribute("title",this.get("closeButtonTitle"))},e.extend(c,e.Panel,{_resizeevent:null,_orientationevent:null,initializer:function(t){var n;this.render(),this.makeResponsive(),this.after("visibleChange",this.visibilityChanged,this),t.center&&this.centerDialogue(),this.set("COUNT",i),n=this.get("boundingBox"),t.extraClasses&&e.Array.each(t.extraClasses,n.addClass,n),t.visible&&this.applyZIndex(),t.visible&&this.show()},applyZIndex:function(){var t=0,n=this.get("boundingBox"),r=this.get("zIndex");r?n.setStyle("zIndex",r):(e.all(d+", "+v).each(function(e){var n=this.findZIndex(e);n>t&&(t=n)},this),t>0&&n.setStyle("zIndex",(t+1).toString()))},findZIndex:function(e){var t=e.getStyle("zIndex")||e.ancestor().getStyle("zIndex");return t?parseInt(t,10):0},visibilityChanged:function(t){var n;t.attrName==="visible"&&(this.get("maskNode").addClass(f.LIGHTBOX),t.prevVal&&!t.newVal&&(this._resizeevent&&(this._resizeevent.detach(),this._resizeevent=null),this._orientationevent&&(this._orientationevent.detach(),this._orientationevent=null)),!t.prevVal&&t.newVal&&(this.applyZIndex(),this.makeResponsive(),this.shouldResizeFullscreen()||this.get("draggable")&&(n="#"+this.get("id")+" ."+f.HEADER,this.plug(e.Plugin.Drag,{handles:[n]}),e.one(n).setStyle("cursor","move"))),this.get("center")&&!t.prevVal&&t.newVal&&this.centerDialogue())},makeResponsive:function(){var t=this.get("boundingBox"),n;this.shouldResizeFullscreen()?(t.addClass(h),t.setStyles({left:null,top:null,width:null,height:null,right:null,bottom:null}),n=e.one("#"+this.get("id")+" ."+f.BODY),n.setStyle("overflow","auto")):this.get("responsive")&&(t.removeClass(h).setStyles({width:this.get("width"),height:this.get("height")}),n=e.one("#"+this.get("id")+" ."+f.BODY),n.setStyle("overflow","inherit"))},centerDialogue:function(){var t=this.get("boundingBox"),n=t.hasClass(p),r,i;if(this.shouldResizeFullscreen())return;n&&t.setStyle("top","-1000px").removeClass(p),r=Math.max(Math.round((t.get("winWidth")-t.get("offsetWidth"))/2),15),i=Math.max(Math.round((t.get("winHeight")-t.get("offsetHeight"))/2),15)+e.one(window).get("scrollTop"),t.setStyles({left:r,top:i}),n&&t.addClass(p)},shouldResizeFullscreen:function(){return window===window.parent&&this.get("responsive")&&Math.floor(e.one(document.body).get("winWidth"))').append(e.Node.create('').append(e.Node.create('
')).append(e.Node.create('
')).append(e.Node.create('
'))),e.one(document.body).append(t.notificationBase),t.additionalBaseClass&&t.notificationBase.addClass(t.additionalBaseClass),t.srcNode="#"+n,t.width=t.width||"400px",t.visible=t.visible||!1,t.center=t.centered&&!0,t.centered=!1,t.COUNT=i,t.width==="auto"&&delete t.width,t.lightbox!==!1&&(t.modal=!0),delete t.lightbox,t.closeButton===!1?t.buttons=null:t.buttons=[{section:e.WidgetStdMod.HEADER,classNames:"closebutton",action:function(){this.hide()}}],c.superclass.constructor.apply(this,[t]),t.closeButton!==!1&&this.get("buttons").header[0].setAttribute("title",this.get("closeButtonTitle"))},e.extend(c,e.Panel,{_resizeevent:null,_orientationevent:null,initializer:function(t){var n;this.render(),this.makeResponsive(),this.after("visibleChange",this.visibilityChanged,this),t.center&&this.centerDialogue(),this.set("COUNT",i),n=this.get("boundingBox"),t.extraClasses&&e.Array.each(t.extraClasses,n.addClass,n),t.visible&&this.applyZIndex(),t.visible&&(this.show(),this.keyDelegation())},applyZIndex:function(){var t=0,n=this.get("boundingBox"),r=this.get("zIndex");r?n.setStyle("zIndex",r):(e.all(d+", "+v).each(function(e){var n=this.findZIndex(e);n>t&&(t=n)},this),t>0&&n.setStyle("zIndex",(t+1).toString()))},findZIndex:function(e){var t=e.getStyle("zIndex")||e.ancestor().getStyle("zIndex");return t?parseInt(t,10):0},visibilityChanged:function(t){var n,r;t.attrName==="visible"&&(this.get("maskNode").addClass(f.LIGHTBOX),t.prevVal&&!t.newVal&&(r=this.get("boundingBox"),this._resizeevent&&(this._resizeevent.detach(),this._resizeevent=null),this._orientationevent&&(this._orientationevent.detach(),this._orientationevent=null),r.detach("key",this.keyDelegation)),!t.prevVal&&t.newVal&&(this.applyZIndex(),this.makeResponsive(),this.shouldResizeFullscreen()||this.get("draggable")&&(n="#"+this.get("id")+" ."+f.HEADER,this.plug(e.Plugin.Drag,{handles:[n]}),e.one(n).setStyle("cursor","move")),this.keyDelegation()),this.get("center")&&!t.prevVal&&t.newVal&&this.centerDialogue())},makeResponsive:function(){var t=this.get("boundingBox"),n;this.shouldResizeFullscreen()?(t.addClass(h),t.setStyles({left:null,top:null,width:null,height:null,right:null,bottom:null}),n=e.one("#"+this.get("id")+" ."+f.BODY),n.setStyle("overflow","auto")):this.get("responsive")&&(t.removeClass(h).setStyles({width:this.get("width"),height:this.get("height")}),n=e.one("#"+this.get("id")+" ."+f.BODY),n.setStyle("overflow","inherit"))},centerDialogue:function(){var t=this.get("boundingBox"),n=t.hasClass(p),r,i;if(this.shouldResizeFullscreen())return;n&&t.setStyle("top","-1000px").removeClass(p),r=Math.max(Math.round((t.get("winWidth")-t.get("offsetWidth"))/2),15),i=Math.max(Math.round((t.get("winHeight")-t.get("offsetHeight"))/2),15)+e.one(window).get("scrollTop"),t.setStyles({left:r,top:i}),n&&t.addClass(p)},shouldResizeFullscreen:function(){return window===window.parent&&this.get("responsive")&&Math.floor(e.one(document.body).get("winWidth"))