Skip to content

Commit

Permalink
MDL-35926 notification dialog: trap tab focus within dialog modal.
Browse files Browse the repository at this point in the history
  • Loading branch information
rwijaya committed Oct 7, 2013
1 parent 56cc9b3 commit 586d393
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 7 deletions.
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
}
},

Expand Down Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -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();
Expand Down Expand Up @@ -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,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Expand Up @@ -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.
Expand Down Expand Up @@ -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();
}
},

Expand Down Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -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();
Expand Down Expand Up @@ -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,
Expand Down
45 changes: 43 additions & 2 deletions lib/yui/src/notification/js/dialogue.js
Expand Up @@ -10,7 +10,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.
Expand Down Expand Up @@ -112,6 +113,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();
}
},

Expand Down Expand Up @@ -167,10 +169,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;
Expand All @@ -179,6 +182,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.
Expand All @@ -192,6 +196,7 @@ Y.extend(DIALOGUE, Y.Panel, {
Y.one(titlebar).setStyle('cursor', 'move');
}
}
this.keyDelegation();
}
if (this.get('center') && !e.prevVal && e.newVal) {
this.centerDialogue();
Expand Down Expand Up @@ -295,6 +300,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,
Expand Down

0 comments on commit 586d393

Please sign in to comment.