Refactor snackbar.

master
Jonathan Garbee 2015-12-21 16:40:18 -05:00
parent ef4135d7b5
commit 2089a682f7
7 changed files with 189 additions and 114 deletions

View File

@ -7,38 +7,43 @@ Actions should undo the committed action or retry it if it failed for example.
Actions should not be to close the snackbar.
By not providing an action, the snackbar becomes a **toast** component.
## To include an MDL **snackbar** component:
## Basic Usage:
&nbsp;1. Create a `<div>` element to contain the snackbar. This element should have no content of its own and be a direct child of the `<body>`.
Start a snackbar with a container div element.
On that container define the `mdl-js-snackbar` and `mdl-snackbar` classes.
It is also beneficial to add the aria live and atomic values to this container.
Within the container create a container element for the message.
This element should have the class `mdl-snackbar__text`.
Leave this element empty!
Text is added when the snackbar is called to be shown.
Second in the container, add a button element.
This element should have the class `mdl-snackbar__action`.
It is recommended to set the type to button to make sure no forms get submitted by accident.
Leave the text content empty here as well!
Do not directly apply any event handlers.
You now have complete markup for the snackbar to function.
All that is left is within your JavaScript to call the `showSnackbar` method on the snackbar container.
This takes a [plain object](#data-object) to configure the snackbar content appropriately.
You may call it multiple consecutive times and messages will stack.
## Examples
All snackbars should be shown through the same element.
#### Markup:
```html
<body>
<div>
</div>
</body>
```
&nbsp;2. Add the `mdl-js-snackbar` class to the container.
```html
<div aria-live="assertive" aria-atomic="true" aria-relevant="text" class="mdl-js-snackbar">
<div aria-live="assertive" aria-atomic="true" aria-relevant="text" class="mdl-snackbar mdl-js-snackbar">
<div class="mdl-snackbar__text"></div>
<button type="button" class="mdl-snackbar__action"></button>
</div>
```
> Note: In this example there are a few aria attributes for accessibility. Please modify these as-needed for your site.
&nbsp;3. Use JavaScript to trigger a snackbar.
```JavaScript
var notification = document.querySelector('.mdl-js-snackbar');
var data = {
message: 'Message Sent'
};
notification.MaterialSnackbar.showSnackbar(data);
```
## Examples
### Snackbar
```js
@ -56,9 +61,34 @@ notification.MaterialSnackbar.showSnackbar(data);
```js
var notification = document.querySelector('.mdl-js-snackbar');
notification.MaterialSnackbar.showSnackbar({message: 'Image Uploaded'});
notification.MaterialSnackbar.showSnackbar(
{
message: 'Image Uploaded'
}
);
```
## CSS Classes
### Blocks
| MDL Class | Effect | Remarks |
|-----------|--------|---------|
| `mdl-snackbar` | Defines the container of the snackbar component. | Required on snackbar container |
### Elements
| MDL Class | Effect | Remarks |
|-----------|--------|---------|
| `mdl-snackbar__text` | Defines the element containing the text of the snackbar. | Required |
| `mdl-snackbar__action` | Defines the element that triggers the action of a snackbar. | Required |
### Modifiers
| MDL Class | Effect | Remarks |
|-----------|--------|---------|
| `mdl-snackbar--active` | Marks the snackbar as active which causes it to display. | Required when active. Controlled in JavaScript |
## Data Object
The Snackbar components `showSnackbar` method takes an object for snackbar data.

View File

@ -15,6 +15,7 @@
*/
@import "../variables";
@import "../mixins";
.mdl-snackbar {
position: fixed;
@ -29,10 +30,12 @@
display: flex;
font-family: $preferred_font;
will-change: transform;
transform: translate3d(0,-50px,0) rotateZ(0deg);
transform: translate3d(0, -50px, 0) rotateZ(0deg);
opacity: 0;
pointer-events: none;
@media(max-width: $snackbar-tablet-breakpoint - 1) {
width: 100%;
left:0;
left: 0;
min-height: 48px;
max-height: 80px;
}
@ -40,33 +43,41 @@
min-width: 288px;
max-width: 568px;
}
&.is-active {
&--active {
max-height: 48px;
transform: translate3d(0, 0, 0);
}
}
.mdl-snackbar__text {
padding: 14px 24px;
vertical-align: middle;
color: white;
}
.mdl-snackbar__action {
background: transparent;
border: none;
color: $snackbar-action-color;
text-transform: uppercase;
padding: 14px 24px;
@include typo-button();
overflow: hidden;
outline: none;
cursor: pointer;
text-decoration: none;
text-align: center;
vertical-align: middle;
&::-moz-focus-inner {
border: 0;
opacity: 1;
pointer-events: auto;
}
&__text {
padding: 14px 24px;
vertical-align: middle;
color: white;
}
&__action {
background: transparent;
border: none;
color: $snackbar-action-color;
text-transform: uppercase;
padding: 14px 24px;
@include typo-button();
overflow: hidden;
outline: none;
opacity: 0;
pointer-events: none;
cursor: pointer;
text-decoration: none;
text-align: center;
vertical-align: middle;
&::-moz-focus-inner {
border: 0;
}
&:not([aria-hidden]) {
opacity: 1;
pointer-events: auto;
}
}
}

View File

@ -26,8 +26,20 @@
*/
var MaterialSnackbar = function MaterialSnackbar(element) {
this.element_ = element;
this.textElement_ = this.element_.querySelector('.' + this.cssClasses_.MESSAGE);
this.actionElement_ = this.element_.querySelector('.' + this.cssClasses_.ACTION);
if (!this.textElement_) {
throw new Error('There must be a message element for a snackbar.');
}
if (!this.actionElement_) {
throw new Error('There must be an action element for a snackbar.');
}
this.active = false;
this.init();
this.actionHandler_ = undefined;
this.message_ = undefined;
this.actionText_ = undefined;
this.queuedNotifications_ = [];
this.setActionHidden_(true);
};
window['MaterialSnackbar'] = MaterialSnackbar;
@ -43,54 +55,32 @@
SNACKBAR: 'mdl-snackbar',
MESSAGE: 'mdl-snackbar__text',
ACTION: 'mdl-snackbar__action',
ACTIVE: 'is-active'
ACTIVE: 'mdl-snackbar--active'
};
/**
* Create the internal snackbar markup.
* Display the snackbar.
*
* @private
*/
MaterialSnackbar.prototype.createSnackbar_ = function() {
this.snackbarElement_ = document.createElement('div');
this.textElement_ = document.createElement('div');
this.snackbarElement_.classList.add(this.cssClasses_.SNACKBAR);
this.textElement_.classList.add(this.cssClasses_.MESSAGE);
this.snackbarElement_.appendChild(this.textElement_);
this.snackbarElement_.setAttribute('aria-hidden', true);
MaterialSnackbar.prototype.displaySnackbar_ = function() {
this.element_.setAttribute('aria-hidden', 'true');
if (this.actionHandler_) {
this.actionElement_ = document.createElement('button');
this.actionElement_.type = 'button';
this.actionElement_.classList.add(this.cssClasses_.ACTION);
this.actionElement_.textContent = this.actionText_;
this.snackbarElement_.appendChild(this.actionElement_);
this.actionElement_.addEventListener('click', this.actionHandler_);
this.setActionHidden_(false);
}
this.element_.appendChild(this.snackbarElement_);
this.textElement_.textContent = this.message_;
this.snackbarElement_.classList.add(this.cssClasses_.ACTIVE);
this.snackbarElement_.setAttribute('aria-hidden', false);
this.element_.classList.add(this.cssClasses_.ACTIVE);
this.element_.setAttribute('aria-hidden', 'false');
setTimeout(this.cleanup_.bind(this), this.timeout_);
};
/**
* Remove the internal snackbar markup.
*
* @private
*/
MaterialSnackbar.prototype.removeSnackbar_ = function() {
if (this.actionElement_ && this.actionElement_.parentNode) {
this.actionElement_.parentNode.removeChild(this.actionElement_);
}
this.textElement_.parentNode.removeChild(this.textElement_);
this.snackbarElement_.parentNode.removeChild(this.snackbarElement_);
};
/**
* Create the internal snackbar markup.
* Show the snackbar.
*
* @param {Object} data The data for the notification.
* @public
@ -122,7 +112,7 @@
if (data['actionText']) {
this.actionText_ = data['actionText'];
}
this.createSnackbar_();
this.displaySnackbar_();
}
};
MaterialSnackbar.prototype['showSnackbar'] = MaterialSnackbar.prototype.showSnackbar;
@ -145,39 +135,35 @@
* @private
*/
MaterialSnackbar.prototype.cleanup_ = function() {
this.snackbarElement_.classList.remove(this.cssClasses_.ACTIVE);
this.snackbarElement_.setAttribute('aria-hidden', true);
if (this.actionElement_) {
this.element_.classList.remove(this.cssClasses_.ACTIVE);
this.element_.setAttribute('aria-hidden', 'true');
this.textElement_.textContent = '';
if (Boolean(this.actionElement_.getAttribute('aria-hidden'))) {
this.setActionHidden_(true);
this.actionElement_.textContent = '';
this.actionElement_.removeEventListener('click', this.actionHandler_);
}
this.setDefaults_();
this.actionHandler_ = undefined;
this.message_ = undefined;
this.actionText_ = undefined;
this.active = false;
this.removeSnackbar_();
this.checkQueue_();
};
/**
* Clean properties to avoid one entry affecting another.
* Set the action handler hidden state.
*
* @param {boolean} value
* @private
*/
MaterialSnackbar.prototype.setDefaults_ = function() {
this.actionHandler_ = undefined;
this.message_ = undefined;
this.actionText_ = undefined;
MaterialSnackbar.prototype.setActionHidden_ = function(value) {
if (value) {
this.actionElement_.setAttribute('aria-hidden', 'true');
} else {
this.actionElement_.removeAttribute('aria-hidden');
}
};
/**
* Initialize the object.
*
* @public
*/
MaterialSnackbar.prototype.init = function() {
this.setDefaults_();
this.queuedNotifications_ = [];
};
MaterialSnackbar.prototype['init'] = MaterialSnackbar.prototype.init;
// The component registers itself. It can assume componentHandler is available
// in the global scope.
componentHandler.register({

View File

@ -1,5 +1,8 @@
<button id="demo-show-snackbar" class="mdl-button mdl-js-button mdl-button--raised" type="button">Show Snackbar</button>
<div id="demo-snackbar-example" class="mdl-js-snackbar"></div>
<div id="demo-snackbar-example" class="mdl-js-snackbar mdl-snackbar">
<div class="mdl-snackbar__text"></div>
<button class="mdl-snackbar__action" type="button"></button>
</div>
<script>
(function() {
'use strict';

View File

@ -1,5 +1,8 @@
<button id="demo-show-toast" class="mdl-button mdl-js-button mdl-button--raised" type="button">Show Toast</button>
<div id="demo-toast-example" class="mdl-js-snackbar"></div>
<div id="demo-toast-example" class="mdl-js-snackbar mdl-snackbar">
<div class="mdl-snackbar__text"></div>
<button class="mdl-snackbar__action" type="button"></button>
</div>
<script>
(function() {
'use strict';

View File

@ -25,7 +25,7 @@ var menuStamps = [];
// 'MaterialRadio',
'MaterialRipple',
// 'MaterialSlider',
'MaterialSnackbar',
// 'MaterialSnackbar',
// 'MaterialSwitch',
'MaterialTabs',
// 'MaterialTextfield',

View File

@ -16,25 +16,49 @@
describe('MaterialSnackbar', function () {
function createSnackbarMarkup() {
var el = document.createElement('div');
el.className = 'mdl-js-snackbar mdl-snackbar';
var text = document.createElement('div');
var action = document.createElement('button');
action.type = 'button';
action.classList.add('mdl-snackbar__action');
text.classList.add('mdl-snackbar__text');
el.appendChild(text);
el.appendChild(action);
return el;
}
it('should be globally available', function () {
expect(MaterialSnackbar).to.be.a('function');
});
it('should expose public methods', function() {
var el = createSnackbarMarkup();
componentHandler.upgradeElement(el);
var methods = [
'showSnackbar'
];
methods.forEach(function(item) {
expect(el.MaterialSnackbar[item]).to.be.a('function');
});
});
it('should be upgradable', function() {
var el = document.createElement('div');
var el = createSnackbarMarkup();
componentHandler.upgradeElement(el, 'MaterialSnackbar');
expect($(el)).to.have.data('upgraded', ',MaterialSnackbar');
});
it('should reveal showSnackbar to widget', function() {
var el = document.createElement('div');
var el = createSnackbarMarkup();
componentHandler.upgradeElement(el, 'MaterialSnackbar');
expect(el.MaterialSnackbar.showSnackbar).to.be.a('function');
});
it('should throw an error if not provided data', function() {
expect(function() {
var el = document.createElement('div');
var el = createSnackbarMarkup();
componentHandler.upgradeElement(el, 'MaterialSnackbar');
el.MaterialSnackbar.showSnackbar();
}).to.throw('Please provide a data object with at least a message to display.');
@ -42,7 +66,7 @@ describe('MaterialSnackbar', function () {
it('should throw an error if not provided a message', function() {
expect(function() {
var el = document.createElement('div');
var el = createSnackbarMarkup();
componentHandler.upgradeElement(el, 'MaterialSnackbar');
el.MaterialSnackbar.showSnackbar({});
}).to.throw('Please provide a message to be displayed.');
@ -50,7 +74,7 @@ describe('MaterialSnackbar', function () {
it('should throw an error if not provided actionText with an actionHandler', function() {
expect(function() {
var el = document.createElement('div');
var el = createSnackbarMarkup();
componentHandler.upgradeElement(el, 'MaterialSnackbar');
el.MaterialSnackbar.showSnackbar({
message: 'Test message',
@ -59,4 +83,22 @@ describe('MaterialSnackbar', function () {
}).to.throw('Please provide action text with the handler.');
});
it('should throw an error if not constructed with a text area in the markup', function() {
expect(function() {
var el = document.createElement('div');
el.className = 'mdl-js-snackbar mdl-snackbar';
componentHandler.upgradeElement(el);
}).to.throw('There must be a message element for a snackbar.');
});
it('should throw an error if not constructed with a text area in the markup', function() {
expect(function() {
var el = document.createElement('div');
el.className = 'mdl-js-snackbar mdl-snackbar';
var textArea = document.createElement('div');
textArea.className = 'mdl-snackbar__text';
el.appendChild(textArea);
componentHandler.upgradeElement(el);
}).to.throw('There must be an action element for a snackbar.');
});
});