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

Commit

Permalink
Add the FAB Speed Dial and FAB Toolbar components.
Browse files Browse the repository at this point in the history
This PR adds support for two new components:
 1. fabSpeedDial - Creates a list of actions that expand from a single floating action button.
 2. fabToolbar - Creates a list of actions that transform from a floating action button into
    a toolbar.

Todo - The following are tasks we would like to implement in a future release.

 * Update keyboard support in-line with suggestions (i.e. tab to focus, then use arrow keys).
 * Add aria support.
 * Update animations to match new specs.
 * Use $animateCss to clean up code.

Fix opacity bug causing issues in the demo.

Wrap sd demo in a single md-content container.

Closes #3108
  • Loading branch information
topherfangio authored and Robert Messerle committed Jun 8, 2015
1 parent 9521a1e commit 545582d
Show file tree
Hide file tree
Showing 14 changed files with 1,073 additions and 0 deletions.
68 changes: 68 additions & 0 deletions src/components/fabSpeedDial/demoBasicUsage/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<div ng-controller="AppCtrl" layout="column">
<md-content class="md-padding" layout="column">
<p>
You may supply a direction of <code>left</code>, <code>up</code>, <code>down</code>, or
<code>right</code> through the <code>md-direction</code> attribute.
</p>

<div class="lock-size" layout="row" layout-align="center center">
<md-fab-speed-dial md-open="demo.isOpen" md-direction="{{demo.selectedDirection}}"
ng-class="demo.selectedMode">
<md-fab-trigger>
<md-button aria-label="menu" class="md-fab md-warn">
<md-icon md-svg-src="img/icons/menu.svg"></md-icon>
</md-button>
</md-fab-trigger>

<md-fab-actions>
<md-button aria-label="twitter" class="md-fab md-raised md-mini">
<md-icon md-svg-src="img/icons/twitter.svg"></md-icon>
</md-button>
<md-button aria-label="facebook" class="md-fab md-raised md-mini">
<md-icon md-svg-src="img/icons/facebook.svg"></md-icon>
</md-button>
<md-button aria-label="Google hangout" class="md-fab md-raised md-mini">
<md-icon md-svg-src="img/icons/hangout.svg"></md-icon>
</md-button>
</md-fab-actions>
</md-fab-speed-dial>
</div>

<div layout="row" layout-align="space-around">
<div layout="column">
<b>Direction</b>

<md-radio-group ng-model="demo.selectedDirection">
<md-radio-button ng-repeat="direction in demo.availableDirections"
ng-value="direction" class="text-capitalize">
{{direction}}
</md-radio-button>
</md-radio-group>
</div>

<div layout="column">
<b>Open/Closed</b>

<md-radio-group ng-model="demo.isOpen">
<md-radio-button ng-value="true">Open</md-radio-button>
<md-radio-button ng-value="false">Closed</md-radio-button>
</md-radio-group>
</div>

<div layout="column">
<b>Animation Modes</b>

<md-radio-group ng-model="demo.selectedMode">
<md-radio-button ng-repeat="mode in demo.availableModes" ng-value="mode">
{{mode}}
</md-radio-button>
</md-radio-group>
</div>
</div>

<p class="note">
Note that you can also hover over the directive's area or tab through each button to open and
activate the speed dial menu.
</p>
</md-content>
</div>
19 changes: 19 additions & 0 deletions src/components/fabSpeedDial/demoBasicUsage/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
(function() {
'use strict';

angular.module('fabSpeedDialBasicUsageDemo', ['ngMaterial'])
.controller('AppCtrl', function($scope) {
$scope.demo = {
topDirections: ['left', 'up'],
bottomDirections: ['down', 'right'],

isOpen: false,

availableModes: ['md-fling', 'md-scale'],
selectedMode: 'md-fling',

availableDirections: ['up', 'down', 'left', 'right'],
selectedDirection: 'up'
};
});
})();
20 changes: 20 additions & 0 deletions src/components/fabSpeedDial/demoBasicUsage/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.text-capitalize {
text-transform: capitalize;
}

.md-fab:hover, .md-fab.md-focused {
background-color: #000 !important;
}

p.note {
font-size: 1.2rem;
}

.lock-size {
min-width: 300px;
min-height: 300px;
width: 300px;
height: 300px;
margin-left: auto;
margin-right: auto;
}
48 changes: 48 additions & 0 deletions src/components/fabSpeedDial/fabActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
(function() {
'use strict';

angular
.module('material.components.fabActions', ['material.core'])
.directive('mdFabActions', MdFabActionsDirective);

/**
* @ngdoc directive
* @name mdFabActions
* @module material.components.fabSpeedDial
*
* @restrict E
*
* @description
* The `<md-fab-actions>` directive is used inside of a `<md-fab-speed-dial>` or
* `<md-fab-toolbar>` directive to mark the an element (or elements) as the actions and setup the
* proper event listeners.
*
* @usage
* See the `<md-fab-speed-dial>` or `<md-fab-toolbar>` directives for example usage.
*/
function MdFabActionsDirective() {
return {
restrict: 'E',

require: ['^?mdFabSpeedDial', '^?mdFabToolbar'],

link: function(scope, element, attributes, controllers) {
// Grab whichever parent controller is used
var controller = controllers[0] || controllers[1];

// Make the children open/close their parent directive
if (controller) {
angular.forEach(element.children(), function(child) {
angular.element(child).on('focus', controller.open);
angular.element(child).on('blur', controller.close);
});
}

// After setting up the listeners, wrap every child in a new div and add a class that we can
// scale/fling independently
element.children().wrap('<div class="md-fab-action-item">');
}
}
}

})();
223 changes: 223 additions & 0 deletions src/components/fabSpeedDial/fabSpeedDial.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
(function() {
'use strict';

angular
.module('material.components.fabSpeedDial', [
'material.core',
'material.components.fabTrigger',
'material.components.fabActions'
])
.directive('mdFabSpeedDial', MdFabSpeedDialDirective)
.animation('.md-fling', MdFabSpeedDialFlingAnimation)
.animation('.md-scale', MdFabSpeedDialScaleAnimation);

/**
* @ngdoc directive
* @name mdFabSpeedDial
* @module material.components.fabSpeedDial
*
* @restrict E
*
* @description
* The `<md-fab-speed-dial>` directive is used to present a series of popup elements (usually
* `<md-button>`s) for quick access to common actions.
*
* There are currently two animations available by applying one of the following classes to
* the component:
*
* - `md-fling` - The speed dial items appear from underneath the trigger and move into their
* appropriate positions.
* - `md-scale` - The speed dial items appear in their proper places by scaling from 0% to 100%.
*
* @usage
* <hljs lang="html">
* <md-fab-speed-dial direction="up" class="md-fling">
* <md-fab-trigger>
* <md-button aria-label="Add..."><md-icon icon="/img/icons/plus.svg"></md-icon></md-button>
* </md-fab-trigger>
*
* <md-fab-actions>
* <md-button aria-label="Add User">
* <md-icon icon="/img/icons/user.svg"></md-icon>
* </md-button>
*
* <md-button aria-label="Add Group">
* <md-icon icon="/img/icons/group.svg"></md-icon>
* </md-button>
* </md-fab-actions>
* </md-fab-speed-dial>
* </hljs>
*
* @param {string=} md-direction From which direction you would like the speed dial to appear
* relative to the trigger element.
* @param {expression=} md-open Programmatically control whether or not the speed-dial is visible.
*/
function MdFabSpeedDialDirective() {
return {
restrict: 'E',

scope: {
direction: '@?mdDirection',
isOpen: '=?mdOpen'
},

bindToController: true,
controller: FabSpeedDialController,
controllerAs: 'vm',

link: FabSpeedDialLink
};

function FabSpeedDialLink(scope, element) {
// Prepend an element to hold our CSS variables so we can use them in the animations below
element.prepend('<div class="md-css-variables"></div>');
}

function FabSpeedDialController($scope, $element, $animate) {
var vm = this;

// Define our open/close functions
// Note: Used by fabTrigger and fabActions directives
vm.open = function() {
$scope.$apply('vm.isOpen = true');
};

vm.close = function() {
$scope.$apply('vm.isOpen = false');
};

setupDefaults();
setupListeners();
setupWatchers();

// Set our default variables
function setupDefaults() {
// Set the default direction to 'down' if none is specified
vm.direction = vm.direction || 'down';

// Set the default to be closed
vm.isOpen = vm.isOpen || false;
}

// Setup our event listeners
function setupListeners() {
$element.on('mouseenter', vm.open);
$element.on('mouseleave', vm.close);
}

// Setup our watchers
function setupWatchers() {
// Watch for changes to the direction and update classes/attributes
$scope.$watch('vm.direction', function(newDir, oldDir) {
// Add the appropriate classes so we can target the direction in the CSS
$animate.removeClass($element, 'md-' + oldDir);
$animate.addClass($element, 'md-' + newDir);
});


// Watch for changes to md-open
$scope.$watch('vm.isOpen', function(isOpen) {
var toAdd = isOpen ? 'md-is-open' : '';
var toRemove = isOpen ? '' : 'md-is-open';

$animate.setClass($element, toAdd, toRemove);
});
}
}
}

function MdFabSpeedDialFlingAnimation() {
function runAnimation(element) {
var el = element[0];
var ctrl = element.controller('mdFabSpeedDial');
var items = el.querySelectorAll('.md-fab-action-item');

// Grab our element which stores CSS variables
var variablesElement = el.querySelector('.md-css-variables');

// Setup JS variables based on our CSS variables
var startZIndex = variablesElement.style.zIndex;

// Always reset the items to their natural position/state
angular.forEach(items, function(item, index) {
var styles = item.style;

styles.transform = '';
styles.transitionDelay = '';
styles.opacity = 1;

// Make the items closest to the trigger have the highest z-index
item.style.zIndex = (items.length - index) + startZIndex;
});

// If the control is closed, hide the items behind the trigger
if (!ctrl.isOpen) {
angular.forEach(items, function(item, index) {
var newPosition, axis;

switch (ctrl.direction) {
case 'up':
newPosition = item.scrollHeight * (index + 1);
axis = 'Y';
break;
case 'down':
newPosition = -item.scrollHeight * (index + 1);
axis = 'Y';
break;
case 'left':
newPosition = item.scrollWidth * (index + 1);
axis = 'X';
break;
case 'right':
newPosition = -item.scrollWidth * (index + 1);
axis = 'X';
break;
}

item.style.transform = 'translate' + axis + '(' + newPosition + 'px)';
});
}
}

return {
addClass: function(element, className, done) {
if (element.hasClass('md-fling')) {
runAnimation(element);
}
},
removeClass: function(element, className, done) {
runAnimation(element);
}
}
}

function MdFabSpeedDialScaleAnimation() {
var delay = 65;

function runAnimation(element) {
var el = element[0];
var ctrl = element.controller('mdFabSpeedDial');
var items = el.querySelectorAll('.md-fab-action-item');

// Always reset the items to their natural position/state
angular.forEach(items, function(item, index) {
var styles = item.style,
offsetDelay = index * delay;

styles.opacity = ctrl.isOpen ? 1 : 0;
styles.transform = ctrl.isOpen ? 'scale(1)' : 'scale(0)';
styles.transitionDelay = (ctrl.isOpen ? offsetDelay : (items.length - offsetDelay)) + 'ms';
});
}

return {
addClass: function(element, className, done) {
runAnimation(element);
},

removeClass: function(element, className, done) {
runAnimation(element);
}
}
}
})();
Loading

0 comments on commit 545582d

Please sign in to comment.