diff --git a/src/components/toast/demoBasicUsage/script.js b/src/components/toast/demoBasicUsage/script.js index 0f39479932c..8fdffe5ac17 100644 --- a/src/components/toast/demoBasicUsage/script.js +++ b/src/components/toast/demoBasicUsage/script.js @@ -1,5 +1,4 @@ - -angular.module('toastDemo1', ['ngMaterial']) +angular.module('toastBasicDemo', ['ngMaterial']) .controller('AppCtrl', function($scope, $mdToast) { var last = { @@ -36,7 +35,7 @@ angular.module('toastDemo1', ['ngMaterial']) $mdToast.show( $mdToast.simple() .textContent('Simple Toast!') - .position(pinTo ) + .position(pinTo) .hideDelay(3000) ); }; @@ -45,14 +44,18 @@ angular.module('toastDemo1', ['ngMaterial']) var pinTo = $scope.getToastPosition(); var toast = $mdToast.simple() .textContent('Marked as read') + .actionKey('z') + .actionHint('Press the Control-"z" key combination to ') .action('UNDO') + .dismissHint('Activate the Escape key to dismiss this toast.') .highlightAction(true) - .highlightClass('md-accent')// Accent is used by default, this just demonstrates the usage. - .position(pinTo); + .highlightClass('md-accent') // Accent is used by default, this just demonstrates the usage. + .position(pinTo) + .hideDelay(0); $mdToast.show(toast).then(function(response) { - if ( response == 'ok' ) { - alert('You clicked the \'UNDO\' action.'); + if (response === 'ok') { + alert('You selected the \'UNDO\' action.'); } }); }; diff --git a/src/components/toast/demoCustomUsage/index.html b/src/components/toast/demoCustomUsage/index.html index f0f76b19c78..0990c8eb9ec 100644 --- a/src/components/toast/demoCustomUsage/index.html +++ b/src/components/toast/demoCustomUsage/index.html @@ -1,13 +1,6 @@ -
-
- -

- Toast can have multiple actions: -

- - - Show Custom Toast - - -
+
+ Toast can have multiple actions: + + Show Custom Toast +
diff --git a/src/components/toast/demoCustomUsage/script.js b/src/components/toast/demoCustomUsage/script.js index 4ac7cd5a192..73cf23756f5 100644 --- a/src/components/toast/demoCustomUsage/script.js +++ b/src/components/toast/demoCustomUsage/script.js @@ -1,48 +1,100 @@ (function() { - var isDlgOpen; + var ACTION_RESOLVE = 'undo'; + var UNDO_KEY = 'z'; + var DIALOG_KEY = 'd'; + + angular.module('toastCustomDemo', ['ngMaterial']) + .controller('AppCtrl', AppCtrl) + .controller('ToastCtrl', ToastCtrl); + + function AppCtrl($mdToast, $log) { + var ctrl = this; + + ctrl.showCustomToast = function() { + $mdToast.show({ + hideDelay: 0, + position: 'top right', + controller: 'ToastCtrl', + controllerAs: 'ctrl', + templateUrl: 'toast-template.html' + }).then(function(result) { + if (result === ACTION_RESOLVE) { + $log.log('Undo action triggered by button.'); + } else if (result === 'key') { + $log.log('Undo action triggered by hot key: Control-' + UNDO_KEY + '.'); + } else if (result === false) { + $log.log('Custom toast dismissed by Escape key.'); + } else { + $log.log('Custom toast hidden automatically.'); + } + }).catch(function(error) { + $log.error('Custom toast failure:', error); + }); + }; + } + + function ToastCtrl($mdToast, $mdDialog, $document) { + var ctrl = this; + ctrl.keyListenerConfigured = false; + ctrl.undoKey = UNDO_KEY; + ctrl.dialogKey = DIALOG_KEY; + setupActionKeyListener(); + + ctrl.closeToast = function() { + if (isDlgOpen) { + return; + } + + $mdToast.hide(ACTION_RESOLVE).then(function() { + isDlgOpen = false; + }); + }; + + ctrl.openMoreInfo = function(e) { + if (isDlgOpen) { + return; + } + isDlgOpen = true; + + $mdDialog.show( + $mdDialog.alert() + .title('More info goes here.') + .textContent('Something witty.') + .ariaLabel('More info') + .ok('Got it') + .targetEvent(e) + ).then(function() { + isDlgOpen = false; + }); + }; + + /** + * @param {KeyboardEvent} event + */ + function handleKeyDown(event) { + if (event.key === 'Escape') { + $mdToast.hide(false); + } + if (event.key === UNDO_KEY && event.ctrlKey) { + $mdToast.hide('key'); + } + if (event.key === DIALOG_KEY && event.ctrlKey) { + ctrl.openMoreInfo(event); + } + } + + function setupActionKeyListener() { + if (!ctrl.keyListenerConfigured) { + $document.on('keydown', handleKeyDown); + ctrl.keyListenerConfigured = true; + } + } - angular - .module('toastDemo2', ['ngMaterial']) - .controller('AppCtrl', function($scope, $mdToast) { - $scope.showCustomToast = function() { - $mdToast.show({ - hideDelay : 3000, - position : 'top right', - controller : 'ToastCtrl', - templateUrl : 'toast-template.html' - }); - }; - }) - .controller('ToastCtrl', function($scope, $mdToast, $mdDialog) { - - $scope.closeToast = function() { - if (isDlgOpen) return; - - $mdToast - .hide() - .then(function() { - isDlgOpen = false; - }); - }; - - $scope.openMoreInfo = function(e) { - if ( isDlgOpen ) return; - isDlgOpen = true; - - $mdDialog - .show($mdDialog - .alert() - .title('More info goes here.') - .textContent('Something witty.') - .ariaLabel('More info') - .ok('Got it') - .targetEvent(e) - ) - .then(function() { - isDlgOpen = false; - }); - }; - }); + function removeActionKeyListener() { + $document.off('keydown'); + ctrl.keyListenerConfigured = false; + } + } })(); diff --git a/src/components/toast/demoCustomUsage/style.scss b/src/components/toast/demoCustomUsage/style.scss new file mode 100644 index 00000000000..75131efa74d --- /dev/null +++ b/src/components/toast/demoCustomUsage/style.scss @@ -0,0 +1,9 @@ +#custom-toast-container { + height: 300px; + padding: 25px; + + .md-button.md-raised { + padding-left: 10px; + padding-right: 10px; + } +} diff --git a/src/components/toast/demoCustomUsage/toast-template.html b/src/components/toast/demoCustomUsage/toast-template.html index a729aee3074..7959e1aa748 100644 --- a/src/components/toast/demoCustomUsage/toast-template.html +++ b/src/components/toast/demoCustomUsage/toast-template.html @@ -1,9 +1,15 @@ - - Custom toast! - + + Custom toast + + Press Escape to dismiss. Press Control-"{{ctrl.dialogKey}}" for + + More info - - Close + + Press Control-"{{ctrl.undoKey}}" to + + + Undo diff --git a/src/components/toast/toast.js b/src/components/toast/toast.js index ff8006d2550..56cf19a0c53 100644 --- a/src/components/toast/toast.js +++ b/src/components/toast/toast.js @@ -156,9 +156,30 @@ function MdToastDirective($mdToast) { * `.action(string)` * * Adds an action button.
- * If clicked, the promise (returned from `show()`) - * will resolve with the value `'ok'`; otherwise, it is resolved with `true` after a `hideDelay` - * timeout + * If clicked, the promise (returned from `show()`) will resolve with the value `'ok'`; + * otherwise, it is resolved with `true` after a `hideDelay` timeout. + * + * + * + * `.actionKey(string)` + * + * Adds a hotkey for the action button.
+ * If the `actionKey` and Control are pressed, the toast's action will be triggered.
+ * Defaults to the first character of the action if not defined. + * + * + * + * `.actionHint(string)` + * + * Text that a screen reader will announce to let the user know how to activate the + * action.
Defaults to: "Press Control-"`actionKey`" to " followed by the action. + * + * + * + * `.dismissHint(string)` + * + * Text that a screen reader will announce to let the user know how to dismiss the toast. + *
Defaults to: "Press Escape to dismiss." * * * @@ -232,9 +253,11 @@ function MdToastDirective($mdToast) { * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope. * This scope will be destroyed when the toast is removed unless `preserveScope` is set to true. * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false - * - `hideDelay` - `{number=}`: How many milliseconds the toast should stay - * active before automatically closing. Set to 0 or false to have the toast stay open until - * closed manually. Default: 3000. + * - `hideDelay` - `{number=}`: The number of milliseconds the toast should stay active before + * automatically closing. Set to `0` or `false` to have the toast stay open until closed + * manually via an action in the toast, a hotkey, or a swipe gesture. For accessibility, toasts + * should not automatically close when they contain an action.
+ * Defaults to: `3000`. * - `position` - `{string=}`: Sets the position of the toast.
* Available: any combination of `'bottom'`, `'left'`, `'top'`, `'right'`, `'end'` and `'start'`. * The properties `'end'` and `'start'` are dynamic and can be used for RTL support.
@@ -247,7 +270,7 @@ function MdToastDirective($mdToast) { * 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 * of 3. - * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in. + * - `bindToController` - `{boolean=}`: bind the locals to the controller, instead of passing them in. * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values * and the toast will not open until the promises resolve. * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope. @@ -295,7 +318,8 @@ function MdToastDirective($mdToast) { */ function MdToastProvider($$interimElementProvider) { - // Differentiate promise resolves: hide timeout (value == true) and hide action clicks (value == ok). + // Differentiate promise resolves: hide timeout (value == true) and hide action clicks + // (value == ok). var ACTION_RESOLVE = 'ok'; var activeToastContent; @@ -306,17 +330,22 @@ function MdToastProvider($$interimElementProvider) { }) .addPreset('simple', { argOption: 'textContent', - methods: ['textContent', 'content', 'action', 'highlightAction', 'highlightClass', 'theme', 'parent' ], + methods: ['textContent', 'content', 'action', 'actionKey', 'actionHint', 'highlightAction', + 'highlightClass', 'theme', 'parent', 'dismissHint' ], options: /* @ngInject */ function($mdToast, $mdTheming) { return { template: '' + - '
' + - ' ' + + '
' + + ' ' + ' {{ toast.content }}' + ' ' + + ' {{ toast.dismissHint }}' + + ' ' + + ' {{ toast.actionHint }}' + + ' ' + ' ' + + ' ng-class="highlightClasses">' + ' {{ toast.action }}' + ' ' + '
' + @@ -329,6 +358,8 @@ function MdToastProvider($$interimElementProvider) { } }) .addMethod('updateTextContent', updateTextContent) + // updateContent is deprecated. Use updateTextContent instead. + // TODO remove this in 1.2. .addMethod('updateContent', updateTextContent); function updateTextContent(newContent) { @@ -354,18 +385,31 @@ function MdToastProvider($$interimElementProvider) { ]; } + // If no actionKey is defined, use the first char of the action name. + if (self.action && !self.actionKey) { + self.actionKey = self.action.charAt(0).toLocaleLowerCase(); + } + + if (self.actionKey && !self.actionHint) { + self.actionHint = 'Press Control-"' + self.actionKey + '" to '; + } + + if (!self.dismissHint) { + self.dismissHint = 'Press Escape to dismiss.'; + } + $scope.$watch(function() { return activeToastContent; }, function() { self.content = activeToastContent; }); this.resolve = function() { - $mdToast.hide( ACTION_RESOLVE ); + $mdToast.hide(ACTION_RESOLVE); }; }; } /* @ngInject */ - function toastDefaultOptions($animate, $mdToast, $mdUtil, $mdMedia) { + function toastDefaultOptions($animate, $mdToast, $mdUtil, $mdMedia, $document) { var SWIPE_EVENTS = '$md.swipeleft $md.swiperight $md.swipeup $md.swipedown'; return { onShow: onShow, @@ -409,7 +453,9 @@ function MdToastProvider($$interimElementProvider) { }; function onShow(scope, element, options) { - activeToastContent = options.textContent || options.content; // support deprecated #content method + // support deprecated #content method + // TODO remove support for content in 1.2. + activeToastContent = options.textContent || options.content; var isSmScreen = !$mdMedia('gt-sm'); @@ -423,8 +469,8 @@ function MdToastProvider($$interimElementProvider) { // If the swipe direction is down/up but the toast came from top/bottom don't fade away // Unless the screen is small, then the toast always on bottom - if ((direction === 'down' && options.position.indexOf('top') != -1 && !isSmScreen) || - (direction === 'up' && (options.position.indexOf('bottom') != -1 || isSmScreen))) { + if ((direction === 'down' && options.position.indexOf('top') !== -1 && !isSmScreen) || + (direction === 'up' && (options.position.indexOf('bottom') !== -1 || isSmScreen))) { return; } @@ -447,23 +493,31 @@ function MdToastProvider($$interimElementProvider) { options.parent.css('position', 'relative'); } + setupActionKeyListener(scope.toast ? scope.toast.actionKey : undefined); element.on(SWIPE_EVENTS, options.onSwipe); element.addClass(isSmScreen ? 'md-bottom' : options.position.split(' ').map(function(pos) { return 'md-' + pos; }).join(' ')); - if (options.parent) options.parent.addClass('md-toast-animating'); + if (options.parent) { + options.parent.addClass('md-toast-animating'); + } return $animate.enter(element, options.parent).then(function() { - if (options.parent) options.parent.removeClass('md-toast-animating'); + if (options.parent) { + options.parent.removeClass('md-toast-animating'); + } }); } function onRemove(scope, element, options) { + if (scope.toast && scope.toast.actionKey) { + removeActionKeyListener(); + } element.off(SWIPE_EVENTS, options.onSwipe); if (options.parent) options.parent.addClass('md-toast-animating'); if (options.openClass) options.parent.removeClass(options.openClass); - return ((options.$destroy == true) ? element.remove() : $animate.leave(element)) + return ((options.$destroy === true) ? element.remove() : $animate.leave(element)) .then(function () { if (options.parent) options.parent.removeClass('md-toast-animating'); if ($mdUtil.hasComputedStyle(options.parent, 'position', 'static')) { @@ -478,9 +532,26 @@ function MdToastProvider($$interimElementProvider) { return 'md-toast-open-bottom'; } - return 'md-toast-open-' + - (position.indexOf('top') > -1 ? 'top' : 'bottom'); + return 'md-toast-open-' + (position.indexOf('top') > -1 ? 'top' : 'bottom'); } - } + function setupActionKeyListener(actionKey) { + /** + * @param {KeyboardEvent} event + */ + var handleKeyDown = function(event) { + if (event.key === 'Escape') { + $mdToast.hide(false); + } + if (actionKey && event.key === actionKey && event.ctrlKey) { + $mdToast.hide(ACTION_RESOLVE); + } + }; + $document.on('keydown', handleKeyDown); + } + + function removeActionKeyListener() { + $document.off('keydown'); + } + } } diff --git a/src/components/toast/toast.spec.js b/src/components/toast/toast.spec.js index 46d8c5c0e58..bc8284f4b0e 100644 --- a/src/components/toast/toast.spec.js +++ b/src/components/toast/toast.spec.js @@ -59,7 +59,7 @@ describe('$mdToast service', function() { $material.flushOutstandingAnimations(); - expect(parent.find('span').text().trim()).toBe('Do something'); + expect(parent.find('span').text().trim()).toContain('Do something'); expect(parent.find('span')).toHaveClass('md-toast-text'); expect(parent.find('md-toast')).toHaveClass('md-capsule'); expect(parent.find('md-toast').attr('md-theme')).toBe('some-theme'); @@ -69,13 +69,13 @@ describe('$mdToast service', function() { expect(openAndclosed).toBe(true); })); - it('supports dynamicly updating the content', inject(function($mdToast, $rootScope, $rootElement) { + it('supports dynamically updating the content', inject(function($mdToast, $rootScope, $rootElement) { var parent = angular.element('
'); $mdToast.showSimple('Hello world'); $rootScope.$digest(); - $mdToast.updateContent('Goodbye world'); + $mdToast.updateTextContent('Goodbye world'); $rootScope.$digest(); - expect($rootElement.find('span').text().trim()).toBe('Goodbye world'); + expect($rootElement.find('span').text().trim()).toContain('Goodbye world'); })); it('supports an action toast', inject(function($mdToast, $rootScope, $material) {