Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

Commit

Permalink
fix(dialog): cross-browser layout, a11y issues
Browse files Browse the repository at this point in the history
fix(dialog): clickOutSideToClose defaults to false
fix(dialog): a11y support for dialog types
fix(dialog): modal dialog traps interaction
fix(dialog): ensure aria-label is not empty
feat(dialog): allow override of focus on open

Closes #1759, #1415, #1547, #1892

Dialogs should require action to close. The option to override is still available but set to false by default.

Alert dialogs require different roles and focus behaviors than confirmation dialogs; both are now supported.

Closes #1340, #1582, fixes #1282
  • Loading branch information
Marcy Sutton committed Mar 16, 2015
1 parent a61def3 commit 0b0ed23
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 56 deletions.
24 changes: 13 additions & 11 deletions src/components/dialog/demoBasicUsage/dialog1.tmpl.html
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
<md-dialog aria-label="Mango (Fruit)">

<md-content>
<md-content class="sticky-container">
<md-subheader class="md-sticky-no-effect">Mango (Fruit)</md-subheader>
<p>
The mango is a juicy stone fruit belonging to the genus Mangifera, consisting of numerous tropical fruiting trees, cultivated mostly for edible fruit. The majority of these species are found in nature as wild mangoes. They all belong to the flowering plant family Anacardiaceae. The mango is native to South and Southeast Asia, from where it has been distributed worldwide to become one of the most cultivated fruits in the tropics.
</p>
<div class="dialog-content">
<p>
The mango is a juicy stone fruit belonging to the genus Mangifera, consisting of numerous tropical fruiting trees, cultivated mostly for edible fruit. The majority of these species are found in nature as wild mangoes. They all belong to the flowering plant family Anacardiaceae. The mango is native to South and Southeast Asia, from where it has been distributed worldwide to become one of the most cultivated fruits in the tropics.
</p>

<img style="margin: auto; max-width: 100%;" src="img/mangues.jpg">
<img style="margin: auto; max-width: 100%;" alt="Lush mango tree" src="img/mangues.jpg">

<p>
The highest concentration of Mangifera genus is in the western part of Malesia (Sumatra, Java and Borneo) and in Burma and India. While other Mangifera species (e.g. horse mango, M. foetida) are also grown on a more localized basis, Mangifera indica—the "common mango" or "Indian mango"—is the only mango tree commonly cultivated in many tropical and subtropical regions.
</p>
<p>
It originated in Indian subcontinent (present day India and Pakistan) and Burma. It is the national fruit of India, Pakistan, and the Philippines, and the national tree of Bangladesh. In several cultures, its fruit and leaves are ritually used as floral decorations at weddings, public celebrations, and religious ceremonies.
</p>
<p>
The highest concentration of Mangifera genus is in the western part of Malesia (Sumatra, Java and Borneo) and in Burma and India. While other Mangifera species (e.g. horse mango, M. foetida) are also grown on a more localized basis, Mangifera indica—the "common mango" or "Indian mango"—is the only mango tree commonly cultivated in many tropical and subtropical regions.
</p>
<p>
It originated in Indian subcontinent (present day India and Pakistan) and Burma. It is the national fruit of India, Pakistan, and the Philippines, and the national tree of Bangladesh. In several cultures, its fruit and leaves are ritually used as floral decorations at weddings, public celebrations, and religious ceremonies.
</p>
</div>
</md-content>

<div class="md-actions" layout="row">
Expand Down
8 changes: 7 additions & 1 deletion src/components/dialog/demoBasicUsage/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,24 @@ angular.module('dialogDemo1', ['ngMaterial'])
$scope.alert = '';

$scope.showAlert = function(ev) {
// Appending dialog to document.body to cover sidenav in docs app
// Modal dialogs should fully cover application
// to prevent interaction outside of dialog
$mdDialog.show(
$mdDialog.alert()
.parent(angular.element(document.body))
.title('This is an alert title')
.content('You can specify some description text in here.')
.ariaLabel('Password notification')
.ariaLabel('Alert Dialog Demo')
.ok('Got it!')
.targetEvent(ev)
);
};

$scope.showConfirm = function(ev) {
// Appending dialog to document.body to cover sidenav in docs app
var confirm = $mdDialog.confirm()
.parent(angular.element(document.body))
.title('Would you like to delete your debt?')
.content('All of the banks have agreed to forgive you your debts.')
.ariaLabel('Lucky day')
Expand Down
30 changes: 14 additions & 16 deletions src/components/dialog/demoBasicUsage/style.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.dialogDemo1 {
height:500px;
height: 500px;
}

.full {
Expand All @@ -8,31 +8,29 @@
}

.gap {
width:50px;
width: 50px;
}


.md-subheader {
background-color: #dcedc8;
margin: 0px;
md-dialog md-content.sticky-container {
padding: 0;
}
md-dialog md-content.sticky-container .dialog-content {
padding: 0 24px 24px;
}

h2.md-subheader {
margin: 0px;
margin-left: -24px;
margin-right: -24px;
margin-top: -24px;
.md-subheader {
background-color: #dcedc8;
margin: 0px;
}

h2.md-subheader.md-sticky-clone {
margin-right:0px;
margin-top:0px;
margin-right: 0px;
margin-top: 0px;

box-shadow: 0px 2px 4px 0 rgba(0,0,0,0.16);
box-shadow: 0px 2px 4px 0 rgba(0,0,0,0.16);
}

h2 .md-subheader-content {
padding-left: 10px;
padding-left: 10px;
}


121 changes: 104 additions & 17 deletions src/components/dialog/dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,8 @@ function MdDialogDirective($$rAF, $mdTheming) {
* - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option,
* the location of the click will be used as the starting point for the opening animation
* of the the dialog.
* - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new isolate scope.
* - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified,
* it will create a new isolate scope.
* This scope will be destroyed when the dialog is removed unless `preserveScope` is set to true.
* - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
* - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the dialog is open.
Expand All @@ -286,13 +287,17 @@ function MdDialogDirective($$rAF, $mdTheming) {
* close it. Default true.
* - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the dialog.
* Default true.
* - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on open. Only disable if
* focusing some other way, as focus management is required for dialogs to be accessible.
* Defaults to true.
* - `controller` - `{string=}`: The controller to associate with the dialog. The controller
* will be injected with the local `$mdDialog`, which passes along a scope for the dialog.
* - `locals` - `{object=}`: An object containing key/value pairs. The keys will be used as names
* of values to inject into the controller. For example, `locals: {three: 3}` would inject
* `three` into the controller, with the value 3. If `bindToController` is true, they will be
* copied to the controller instead.
* - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in. These values will not be available until after initialization.
* copied to the controller instead.
* - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in.
* These values will not be available until after initialization.
* - `resolve` - `{object=}`: Similar to locals, except it takes promises as values, and the
* dialog will not open until all of the promises resolve.
* - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
Expand Down Expand Up @@ -348,7 +353,7 @@ function MdDialogProvider($$interimElementProvider) {
return {
template: [
'<md-dialog md-theme="{{ dialog.theme }}" aria-label="{{ dialog.ariaLabel }}">',
'<md-content>',
'<md-content role="document" tabIndex="0">',
'<h2>{{ dialog.title }}</h2>',
'<p>{{ dialog.content }}</p>',
'</md-content>',
Expand Down Expand Up @@ -383,15 +388,24 @@ function MdDialogProvider($$interimElementProvider) {
isolateScope: true,
onShow: onShow,
onRemove: onRemove,
clickOutsideToClose: true,
clickOutsideToClose: false,
escapeToClose: true,
targetEvent: null,
focusOnOpen: true,
disableParentScroll: true,
transformTemplate: function(template) {
return '<div class="md-dialog-container">' + template + '</div>';
}
};

function trapFocus(ev) {
var dialog = document.querySelector('md-dialog');

if (dialog && !dialog.contains(ev.target)) {
ev.stopImmediatePropagation();
dialog.focus();
}
}

// On show method for dialogs
function onShow(scope, element, options) {
Expand All @@ -403,19 +417,29 @@ function MdDialogProvider($$interimElementProvider) {
options.popInTarget = angular.element((options.targetEvent || {}).target);
var closeButton = findCloseButton();

configureAria(element.find('md-dialog'));

if (options.hasBackdrop) {
// Fix for IE 10
var computeFrom = (options.parent[0] == $document[0].body && $document[0].documentElement
&& $document[0].scrollTop) ? angular.element($document[0].documentElement) : options.parent;
var computeFrom = (options.parent[0] == $document[0].body && $document[0].documentElement
&& $document[0].documentElement.scrollTop) ? angular.element($document[0].documentElement) : options.parent;
var parentOffset = computeFrom.prop('scrollTop');
options.backdrop = angular.element('<md-backdrop class="md-dialog-backdrop md-opaque">');
$mdTheming.inherit(options.backdrop, options.parent);
$animate.enter(options.backdrop, options.parent);
element.css('top', parentOffset +'px');
}

var role = 'dialog',
elementToFocus = closeButton;

if (options.$type === 'alert') {
role = 'alertdialog';
elementToFocus = element.find('md-content');
}

configureAria(element.find('md-dialog'), role, options);

document.addEventListener('focus', trapFocus, true);

if (options.disableParentScroll) {
options.lastOverflow = options.parent.css('overflow');
options.parent.css('overflow', 'hidden');
Expand All @@ -427,6 +451,9 @@ function MdDialogProvider($$interimElementProvider) {
options.popInTarget && options.popInTarget.length && options.popInTarget
)
.then(function() {

applyAriaToSiblings(element, true);

if (options.escapeToClose) {
options.rootElementKeyupCallback = function(e) {
if (e.keyCode === $mdConstant.KEY_CODE.ESCAPE) {
Expand All @@ -445,7 +472,10 @@ function MdDialogProvider($$interimElementProvider) {
};
element.on('click', options.dialogClickOutsideCallback);
}
closeButton.focus();

if (options.focusOnOpen) {
elementToFocus.focus();
}
});


Expand Down Expand Up @@ -478,6 +508,11 @@ function MdDialogProvider($$interimElementProvider) {
if (options.clickOutsideToClose) {
element.off('click', options.dialogClickOutsideCallback);
}

applyAriaToSiblings(element, false);

document.removeEventListener('focus', trapFocus, true);

return dialogPopOut(
element,
options.parent,
Expand All @@ -493,20 +528,72 @@ function MdDialogProvider($$interimElementProvider) {
/**
* Inject ARIA-specific attributes appropriate for Dialogs
*/
function configureAria(element) {
function configureAria(element, role, options) {

element.attr({
'role': 'dialog'
'role': role,
'tabIndex': '-1'
});

var dialogContent = element.find('md-content');
if (dialogContent.length === 0){
dialogContent = element;
}
$mdAria.expectAsync(element, 'aria-label', function() {
var words = dialogContent.text().split(/\s+/);
if (words.length > 3) words = words.slice(0,3).concat('...');
return words.join(' ');
});

var dialogId = element.attr('id') || ('dialog_' + $mdUtil.nextUid());
dialogContent.attr('id', dialogId);
element.attr('aria-describedby', dialogId);

if (options.ariaLabel) {
$mdAria.expect(element, 'aria-label', options.ariaLabel);
}
else {
$mdAria.expectAsync(element, 'aria-label', function() {
var words = dialogContent.text().split(/\s+/);
if (words.length > 3) words = words.slice(0,3).concat('...');
return words.join(' ');
});
}
}
/**
* Utility function to filter out raw DOM nodes
*/
function isNodeOneOf(elem, nodeTypeArray) {
if (nodeTypeArray.indexOf(elem.nodeName) !== -1) {
return true;
}
}
/**
* Walk DOM to apply or remove aria-hidden on sibling nodes
* and parent sibling nodes
*
* Prevents screen reader interaction behind modal window
* on swipe interfaces
*/
function applyAriaToSiblings(element, value) {
var attribute = 'aria-hidden';

// get raw DOM node
element = element[0];

function walkDOM(element) {
while (element.parentNode) {
if (element === document.body) {
return;
}
var children = element.parentNode.children;
for (var i = 0; i < children.length; i++) {
// skip over child if it is an ascendant of the dialog
// or a script or style tag
if (element !== children[i] && !isNodeOneOf(children[i], ['SCRIPT', 'STYLE'])) {
children[i].setAttribute(attribute, value);
}
}

walkDOM(element = element.parentNode);
}
}
walkDOM(element);
}

function dialogPopIn(container, parentElement, clickElement) {
Expand Down
Loading

0 comments on commit 0b0ed23

Please sign in to comment.