MODULE-TYPO

- Sass set-up
- md-list
- md-theming (install)
This commit is contained in:
iRouge
2015-01-16 07:52:29 -05:00
parent ec1b4b9b0c
commit f1d2b8cb75
312 changed files with 26839 additions and 1309 deletions
@@ -0,0 +1,31 @@
<div ng-controller="AppCtrl" class="sample">
<md-tabs md-selected="selectedIndex">
<md-tab ng-repeat="tab in tabs"
ng-disabled="tab.disabled"
label="{{tab.title}}">
<div class="demo-tab tab{{$index%4}}" layout="column" layout-fill layout-align="space-around center">
<div ng-bind="tab.content"></div>
<md-button class="md-warn" ng-click="removeTab( tab )">
Remove Tab
</md-button>
</div>
</md-tab>
</md-tabs>
<form ng-submit="addTab(tTitle,tContent)" flex>
<div layout="row" layout-sm="column" layout-margin layout-align="left center">
<md-text-float label="Active Index" ng-model="selectedIndex" disabled></md-text-float>
<md-text-float label="Active Title" ng-model="tabs[selectedIndex].title"></md-text-float>
</div>
<div layout="row" layout-sm="column" layout-margin layout-align="left center">
<span class="title">Add a new Tab:</span>
<md-text-float label="Label" ng-model="tTitle"></md-text-float>
<md-text-float label="Content" ng-model="tContent" ></md-text-float>
<md-button class="add-tab md-primary" type="submit" style="max-height: 40px" >Add Tab</md-button>
</div>
</form>
</div>
@@ -0,0 +1,39 @@
angular.module('tabsDemo2', ['ngMaterial'])
.controller('AppCtrl', function ($scope, $log) {
var tabs = [
{ title: 'One', content: "Tabs will become paginated if there isn't enough room for them."},
{ title: 'Two', content: "You can swipe left and right on a mobile device to change tabs."},
{ title: 'Three', content: "You can bind the selected tab via the selected attribute on the md-tabs element."},
{ title: 'Four', content: "If you set the selected tab binding to -1, it will leave no tab selected."},
{ title: 'Five', content: "If you remove a tab, it will try to select a new one."},
{ title: 'Six', content: "There's an ink bar that follows the selected tab, you can turn it off if you want."},
{ title: 'Seven', content: "If you set ng-disabled on a tab, it becomes unselectable. If the currently selected tab becomes disabled, it will try to select the next tab."},
{ title: 'Eight', content: "If you look at the source, you're using tabs to look at a demo for tabs. Recursion!"},
{ title: 'Nine', content: "If you set md-theme=\"green\" on the md-tabs element, you'll get green tabs."},
{ title: 'Ten', content: "If you're still reading this, you should just go check out the API docs for tabs!"}
];
$scope.tabs = tabs;
$scope.selectedIndex = 2;
$scope.$watch('selectedIndex', function(current, old){
if ( old && (old != current)) $log.debug('Goodbye ' + tabs[old].title + '!');
if ( current ) $log.debug('Hello ' + tabs[current].title + '!');
});
$scope.addTab = function (title, view) {
view = view || title + " Content View";
tabs.push({ title: title, content: view, disabled: false});
};
$scope.removeTab = function (tab) {
for (var j = 0; j < tabs.length; j++) {
if (tab.title == tabs[j].title) {
$scope.tabs.splice(j, 1);
break;
}
}
};
});
@@ -0,0 +1,67 @@
.remove-tab {
margin-bottom: 40px;
}
.md-button {
display:block;
}
.demo-tab {
height: 300px;
text-align: center;
}
.tab0, .tab1, .tab2, .tab3 {
background-color: #bbdefb;
}
.md-header {
background-color: #1976D2 !important;
}
md-tab {
color: #90caf9 !important;
}
md-tab.active,
md-tab:focus {
color: white !important;
}
md-tab[disabled] {
opacity: 0.5;
}
.md-header .md-ripple {
border-color: #FFFF8D !important;
}
md-tabs-ink-bar {
background-color: #FFFF8D !important;
}
.title {
padding-top: 8px;
padding-right: 8px;
text-align: left;
text-transform: uppercase;
color: #888;
margin-top: 24px;
}
[layout-align] > * {
margin-left: 8px;
}
form > [layout] > * {
margin-left: 8px;
}
.long > input {
width: 264px;
}
.md-button {
max-height: 30px;
}
.md-button.add-tab {
margin-top:20px;
max-height:30px !important;
}
@@ -0,0 +1,58 @@
<div ng-controller="AppCtrl">
<md-tabs class="md-accent" md-selected="data.selectedIndex">
<md-tab id="tab1" aria-controls="tab1-content">
Item One
</md-tab>
<md-tab id="tab2" aria-controls="tab2-content"
ng-disabled="data.secondLocked">
{{data.secondLabel}}
</md-tab>
<md-tab id="tab3" aria-controls="tab3-content">
Item Three
</md-tab>
</md-tabs>
<ng-switch on="data.selectedIndex" class="tabpanel-container">
<div role="tabpanel"
id="tab1-content"
aria-labelledby="tab1"
ng-switch-when="0"
md-swipe-left="next()"
md-swipe-right="previous()" >
View for Item #1<br/>
data.selectedIndex = 0
</div>
<div role="tabpanel"
id="tab2-content"
aria-labelledby="tab2"
ng-switch-when="1"
md-swipe-left="next()"
md-swipe-right="previous()" >
View for {{data.secondLabel}}<br/>
data.selectedIndex = 1
</div>
<div role="tabpanel"
id="tab3-content"
aria-labelledby="tab3"
ng-switch-when="2"
md-swipe-left="next()"
md-swipe-right="previous()" >
View for Item #3<br/>
data.selectedIndex = 2
</div>
</ng-switch>
<div class="after-tabs-area" layout="row" layout-sm="column" layout-margin layout-align="left center">
<md-text-float type="number" label="Selected Index" ng-model="data.selectedIndex"></md-text-float>
<div flex></div>
<span>Item Two: </span>
<md-checkbox ng-model="data.secondLocked" aria-label="Disabled">
Disabled
</md-checkbox>
</div>
</div>
@@ -0,0 +1,19 @@
angular.module('tabsDemo1', ['ngMaterial'] )
.controller('AppCtrl', function( $scope ) {
$scope.data = {
selectedIndex : 0,
secondLocked : true,
secondLabel : "Item Two"
};
$scope.next = function() {
$scope.data.selectedIndex = Math.min($scope.data.selectedIndex + 1, 2) ;
};
$scope.previous = function() {
$scope.data.selectedIndex = Math.max($scope.data.selectedIndex - 1, 0);
};
});
@@ -0,0 +1,85 @@
#tab1-content {
background-color: #3F51B5;
}
#tab2-content {
background-color: #673AB7;
}
#tab3-content {
background-color: #00796B;
}
/*
* Animation styles
*/
.tabpanel-container {
display: block;
position: relative;
background: white;
border: 0px solid black;
height: 300px;
overflow: hidden;
}
[role="tabpanel"] {
color: white;
width: 100%;
height: 100%;
-webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
position:absolute;
}
[role="tabpanel"].ng-leave.ng-leave-active,
[role="tabpanel"].ng-enter {
top:-300px;
}
[role="tabpanel"].ng-leave,
[role="tabpanel"].ng-enter.ng-enter-active {
top:0;
}
[role="tabpanel"].ng-leave {
z-index: 100;
}
.tabpanel-container [role="tabpanel"] {
padding:20px;
}
.after-tabs-area {
padding: 25px;
}
.after-tabs-area > span {
margin-top:25px;
padding-right: 15px;
vertical-align: middle;
line-height: 30px;
height: 35px;
}
.after-tabs-area > md-checkbox {
margin-top:26px;
margin-left:0px;
}
.md-header {
background-color: #1976D2 !important;
}
md-tab {
color: #90caf9 !important;
}
md-tab.active,
md-tab:focus {
color: white !important;
}
md-tab[disabled] {
opacity: 0.5;
}
.md-header .md-ripple {
border-color: #FFFF8D !important;
}
md-tabs-ink-bar {
background-color: #FFFF8D !important;
}
@@ -0,0 +1,57 @@
(function() {
'use strict';
/**
* Conditionally configure ink bar animations when the
* tab selection changes. If `mdNoBar` then do not show the
* bar nor animate.
*/
angular.module('material.components.tabs')
.directive('mdTabsInkBar', MdTabInkDirective);
function MdTabInkDirective($$rAF) {
var lastIndex = 0;
return {
restrict: 'E',
require: ['^?mdNoBar', '^mdTabs'],
link: postLink
};
function postLink(scope, element, attr, ctrls) {
if (ctrls[0]) return;
var tabsCtrl = ctrls[1],
debouncedUpdateBar = $$rAF.debounce(updateBar);
tabsCtrl.inkBarElement = element;
scope.$on('$mdTabsPaginationChanged', debouncedUpdateBar);
function updateBar() {
var selected = tabsCtrl.getSelectedItem();
var hideInkBar = !selected || tabsCtrl.count() < 2;
element.css('display', hideInkBar ? 'none' : 'block');
if (hideInkBar) return;
if (scope.pagination && scope.pagination.tabData) {
var index = tabsCtrl.getSelectedIndex();
var data = scope.pagination.tabData.tabs[index] || { left: 0, right: 0, width: 0 };
var right = element.parent().prop('offsetWidth') - data.right;
var classNames = ['md-transition-left', 'md-transition-right', 'md-no-transition'];
var classIndex = lastIndex > index ? 0 : lastIndex < index ? 1 : 2;
element
.removeClass(classNames.join(' '))
.addClass(classNames[classIndex])
.css({ left: (data.left + 1) + 'px', right: right + 'px' });
lastIndex = index;
}
}
}
}
})();
@@ -0,0 +1,246 @@
(function() {
'use strict';
angular.module('material.components.tabs')
.directive('mdTabsPagination', TabPaginationDirective);
function TabPaginationDirective($mdConstant, $window, $$rAF, $$q, $timeout, $mdMedia) {
// Must match (2 * width of paginators) in scss
var PAGINATORS_WIDTH = (8 * 4) * 2;
return {
restrict: 'A',
require: '^mdTabs',
link: postLink
};
function postLink(scope, element, attr, tabsCtrl) {
var tabs = element[0].getElementsByTagName('md-tab');
var debouncedUpdatePagination = $$rAF.debounce(updatePagination);
var tabsParent = element.children();
var state = scope.pagination = {
page: -1,
active: false,
clickNext: function() { userChangePage(+1); },
clickPrevious: function() { userChangePage(-1); }
};
scope.$on('$mdTabsChanged', debouncedUpdatePagination);
angular.element($window).on('resize', debouncedUpdatePagination);
scope.$on('$destroy', function() {
angular.element($window).off('resize', debouncedUpdatePagination);
});
scope.$watch(function() { return tabsCtrl.tabToFocus; }, onTabFocus);
// Make sure we don't focus an element on the next page
// before it's in view
function onTabFocus(tab, oldTab) {
if (!tab) return;
var pageIndex = getPageForTab(tab);
if (!state.active || pageIndex === state.page) {
tab.element.focus();
} else {
// Go to the new page, wait for the page transition to end, then focus.
oldTab && oldTab.element.blur();
setPage(pageIndex).then(function() { tab.element.focus(); });
}
}
// Called when page is changed by a user action (click)
function userChangePage(increment) {
var sizeData = state.tabData;
var newPage = Math.max(0, Math.min(sizeData.pages.length - 1, state.page + increment));
var newTabIndex = sizeData.pages[newPage][ increment > 0 ? 'firstTabIndex' : 'lastTabIndex' ];
var newTab = tabsCtrl.itemAt(newTabIndex);
onTabFocus(newTab);
}
function updatePagination() {
if (!element.prop('offsetParent')) {
var watcher = waitForVisible();
return;
}
var tabs = element.find('md-tab');
disablePagination();
var sizeData = state.tabData = calculateTabData();
var needPagination = state.active = sizeData.pages.length > 1;
if (needPagination) { enablePagination(); }
scope.$evalAsync(function () { scope.$broadcast('$mdTabsPaginationChanged'); });
function enablePagination() {
tabsParent.css('width', '9999px');
//-- apply filler margins
angular.forEach(sizeData.tabs, function (tab) {
angular.element(tab.element).css('margin-left', tab.filler + 'px');
});
setPage(getPageForTab(tabsCtrl.getSelectedItem()));
}
function disablePagination() {
slideTabButtons(0);
tabsParent.css('width', '');
tabs.css('width', '');
tabs.css('margin-left', '');
state.page = null;
state.active = false;
}
function waitForVisible() {
return watcher || scope.$watch(
function () {
$timeout(function () {
if (element[0].offsetParent) {
if (angular.isFunction(watcher)) {
watcher();
}
debouncedUpdatePagination();
watcher = null;
}
}, 0, false);
}
);
}
}
function slideTabButtons(x) {
if (tabsCtrl.pagingOffset === x) {
// Resolve instantly if no change
return $$q.when();
}
var deferred = $$q.defer();
tabsCtrl.$$pagingOffset = x;
tabsParent.css($mdConstant.CSS.TRANSFORM, 'translate3d(' + x + 'px,0,0)');
tabsParent.on($mdConstant.CSS.TRANSITIONEND, onTabsParentTransitionEnd);
return deferred.promise;
function onTabsParentTransitionEnd(ev) {
// Make sure this event didn't bubble up from an animation in a child element.
if (ev.target === tabsParent[0]) {
tabsParent.off($mdConstant.CSS.TRANSITIONEND, onTabsParentTransitionEnd);
deferred.resolve();
}
}
}
function shouldStretchTabs() {
switch (scope.stretchTabs) {
case 'never': return false;
case 'always': return true;
default: return $mdMedia('sm');
}
}
function calculateTabData(noAdjust) {
var clientWidth = element.parent().prop('offsetWidth');
var tabsWidth = clientWidth - PAGINATORS_WIDTH - 1;
var $tabs = angular.element(tabs);
var totalWidth = 0;
var max = 0;
var tabData = [];
var pages = [];
var currentPage;
$tabs.css('max-width', '');
angular.forEach(tabs, function (tab, index) {
var tabWidth = Math.min(tabsWidth, tab.offsetWidth);
var data = {
element: tab,
left: totalWidth,
width: tabWidth,
right: totalWidth + tabWidth,
filler: 0
};
//-- This calculates the page for each tab. The first page will use the clientWidth, which
// does not factor in the pagination items. After the first page, tabsWidth is used
// because at this point, we know that the pagination buttons will be shown.
data.page = Math.ceil(data.right / ( pages.length === 1 && index === tabs.length - 1 ? clientWidth : tabsWidth )) - 1;
if (data.page >= pages.length) {
data.filler = (tabsWidth * data.page) - data.left;
data.right += data.filler;
data.left += data.filler;
currentPage = {
left: data.left,
firstTabIndex: index,
lastTabIndex: index,
tabs: [ data ]
};
pages.push(currentPage);
} else {
currentPage.lastTabIndex = index;
currentPage.tabs.push(data);
}
totalWidth = data.right;
max = Math.max(max, tabWidth);
tabData.push(data);
});
$tabs.css('max-width', tabsWidth + 'px');
if (!noAdjust && shouldStretchTabs()) {
return adjustForStretchedTabs();
} else {
return {
width: totalWidth,
max: max,
tabs: tabData,
pages: pages,
tabElements: tabs
};
}
function adjustForStretchedTabs() {
var canvasWidth = pages.length === 1 ? clientWidth : tabsWidth;
var tabsPerPage = Math.min(Math.floor(canvasWidth / max), tabs.length);
var tabWidth = Math.floor(canvasWidth / tabsPerPage);
$tabs.css('width', tabWidth + 'px');
return calculateTabData(true);
}
}
function getPageForTab(tab) {
var tabIndex = tabsCtrl.indexOf(tab);
if (tabIndex === -1) return 0;
var sizeData = state.tabData;
return sizeData ? sizeData.tabs[tabIndex].page : 0;
}
function setPage(page) {
if (page === state.page) return;
var lastPage = state.tabData.pages.length - 1;
if (page < 0) page = 0;
if (page > lastPage) page = lastPage;
state.hasPrev = page > 0;
state.hasNext = page < lastPage;
state.page = page;
scope.$broadcast('$mdTabsPaginationChanged');
return slideTabButtons(-state.tabData.pages[page].left);
}
}
}
})();
@@ -0,0 +1,90 @@
(function() {
'use strict';
angular.module('material.components.tabs')
.controller('$mdTab', TabItemController);
function TabItemController($scope, $element, $attrs, $compile, $animate, $mdUtil, $parse, $timeout) {
var self = this;
// Properties
self.contentContainer = angular.element('<div class="md-tab-content ng-hide">');
self.hammertime = new Hammer(self.contentContainer[0]);
self.element = $element;
// Methods
self.isDisabled = isDisabled;
self.onAdd = onAdd;
self.onRemove = onRemove;
self.onSelect = onSelect;
self.onDeselect = onDeselect;
var disabledParsed = $parse($attrs.ngDisabled);
function isDisabled() {
return disabledParsed($scope.$parent);
}
/**
* Add the tab's content to the DOM container area in the tabs,
* @param contentArea the contentArea to add the content of the tab to
*/
function onAdd(contentArea, shouldDisconnectScope) {
if (self.content.length) {
self.contentContainer.append(self.content);
self.contentScope = $scope.$parent.$new();
contentArea.append(self.contentContainer);
$compile(self.contentContainer)(self.contentScope);
if (shouldDisconnectScope === true) {
$timeout(function () {
$mdUtil.disconnectScope(self.contentScope);
}, 0, false);
}
}
}
function onRemove() {
self.hammertime.destroy();
$animate.leave(self.contentContainer).then(function() {
self.contentScope && self.contentScope.$destroy();
self.contentScope = null;
});
}
function toggleAnimationClass(rightToLeft) {
self.contentContainer[rightToLeft ? 'addClass' : 'removeClass']('md-transition-rtl');
}
function onSelect(rightToLeft) {
// Resume watchers and events firing when tab is selected
$mdUtil.reconnectScope(self.contentScope);
self.hammertime.on('swipeleft swiperight', $scope.onSwipe);
$element.addClass('active');
$element.attr('aria-selected', true);
$element.attr('tabIndex', 0);
toggleAnimationClass(rightToLeft);
$animate.removeClass(self.contentContainer, 'ng-hide');
$scope.onSelect();
}
function onDeselect(rightToLeft) {
// Stop watchers & events from firing while tab is deselected
$mdUtil.disconnectScope(self.contentScope);
self.hammertime.off('swipeleft swiperight', $scope.onSwipe);
$element.removeClass('active');
$element.attr('aria-selected', false);
// Only allow tabbing to the active tab
$element.attr('tabIndex', -1);
toggleAnimationClass(rightToLeft);
$animate.addClass(self.contentContainer, 'ng-hide');
$scope.onDeselect();
}
}
})();
@@ -0,0 +1,244 @@
(function() {
'use strict';
angular.module('material.components.tabs')
.directive('mdTab', MdTabDirective);
/**
* @ngdoc directive
* @name mdTab
* @module material.components.tabs
*
* @restrict E
*
* @description
* `<md-tab>` is the nested directive used [within `<md-tabs>`] to specify each tab with a **label** and optional *view content*.
*
* If the `label` attribute is not specified, then an optional `<md-tab-label>` tag can be used to specify more
* complex tab header markup. If neither the **label** nor the **md-tab-label** are specified, then the nested
* markup of the `<md-tab>` is used as the tab header markup.
*
* If a tab **label** has been identified, then any **non-**`<md-tab-label>` markup
* will be considered tab content and will be transcluded to the internal `<div class="md-tabs-content">` container.
*
* This container is used by the TabsController to show/hide the active tab's content view. This synchronization is
* automatically managed by the internal TabsController whenever the tab selection changes. Selection changes can
* be initiated via data binding changes, programmatic invocation, or user gestures.
*
* @param {string=} label Optional attribute to specify a simple string as the tab label
* @param {boolean=} md-active When evaluteing to true, selects the tab.
* @param {boolean=} disabled If present, disabled tab selection.
* @param {expression=} md-on-deselect Expression to be evaluated after the tab has been de-selected.
* @param {expression=} md-on-select Expression to be evaluated after the tab has been selected.
*
*
* @usage
*
* <hljs lang="html">
* <md-tab label="" disabled="" md-on-select="" md-on-deselect="" >
* <h3>My Tab content</h3>
* </md-tab>
*
* <md-tab >
* <md-tab-label>
* <h3>My Tab content</h3>
* </md-tab-label>
* <p>
* Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
* totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae
* dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit,
* sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
* </p>
* </md-tab>
* </hljs>
*
*/
function MdTabDirective($mdInkRipple, $compile, $mdUtil, $mdConstant, $timeout) {
return {
restrict: 'E',
require: ['mdTab', '^mdTabs'],
controller: '$mdTab',
scope: {
onSelect: '&mdOnSelect',
onDeselect: '&mdOnDeselect',
label: '@'
},
compile: compile
};
function compile(element, attr) {
var tabLabel = element.find('md-tab-label');
if (tabLabel.length) {
// If a tab label element is found, remove it for later re-use.
tabLabel.remove();
} else if (angular.isDefined(attr.label)) {
// Otherwise, try to use attr.label as the label
tabLabel = angular.element('<md-tab-label>').html(attr.label);
} else {
// If nothing is found, use the tab's content as the label
tabLabel = angular.element('<md-tab-label>')
.append(element.contents().remove());
}
// Everything that's left as a child is the tab's content.
var tabContent = element.contents().remove();
return function postLink(scope, element, attr, ctrls) {
var tabItemCtrl = ctrls[0]; // Controller for THIS tabItemCtrl
var tabsCtrl = ctrls[1]; // Controller for ALL tabs
scope.$watch(
function () { return attr.label; },
function () { $timeout(function () { tabsCtrl.scope.$broadcast('$mdTabsChanged'); }, 0, false); }
);
transcludeTabContent();
configureAria();
var detachRippleFn = $mdInkRipple.attachTabBehavior(scope, element, {
colorElement: tabsCtrl.inkBarElement
});
tabsCtrl.add(tabItemCtrl);
scope.$on('$destroy', function() {
detachRippleFn();
tabsCtrl.remove(tabItemCtrl);
});
element.on('$destroy', function () {
//-- wait for item to be removed from the dom
$timeout(function () {
tabsCtrl.scope.$broadcast('$mdTabsChanged');
}, 0, false);
});
if (!angular.isDefined(attr.ngClick)) {
element.on('click', defaultClickListener);
}
element.on('keydown', keydownListener);
scope.onSwipe = onSwipe;
if (angular.isNumber(scope.$parent.$index)) {
watchNgRepeatIndex();
}
if (angular.isDefined(attr.mdActive)) {
watchActiveAttribute();
}
watchDisabled();
function transcludeTabContent() {
// Clone the label we found earlier, and $compile and append it
var label = tabLabel.clone();
element.append(label);
$compile(label)(scope.$parent);
// Clone the content we found earlier, and mark it for later placement into
// the proper content area.
tabItemCtrl.content = tabContent.clone();
}
//defaultClickListener isn't applied if the user provides an ngClick expression.
function defaultClickListener() {
scope.$apply(function() {
tabsCtrl.select(tabItemCtrl);
tabsCtrl.focus(tabItemCtrl);
});
}
function keydownListener(ev) {
if (ev.keyCode == $mdConstant.KEY_CODE.SPACE || ev.keyCode == $mdConstant.KEY_CODE.ENTER ) {
// Fire the click handler to do normal selection if space is pressed
element.triggerHandler('click');
ev.preventDefault();
} else if (ev.keyCode === $mdConstant.KEY_CODE.LEFT_ARROW) {
scope.$evalAsync(function() {
tabsCtrl.focus(tabsCtrl.previous(tabItemCtrl));
});
} else if (ev.keyCode === $mdConstant.KEY_CODE.RIGHT_ARROW) {
scope.$evalAsync(function() {
tabsCtrl.focus(tabsCtrl.next(tabItemCtrl));
});
}
}
function onSwipe(ev) {
scope.$apply(function() {
if (ev.type === 'swipeleft') {
tabsCtrl.select(tabsCtrl.next());
} else {
tabsCtrl.select(tabsCtrl.previous());
}
});
}
// If tabItemCtrl is part of an ngRepeat, move the tabItemCtrl in our internal array
// when its $index changes
function watchNgRepeatIndex() {
// The tabItemCtrl has an isolate scope, so we watch the $index on the parent.
scope.$watch('$parent.$index', function $indexWatchAction(newIndex) {
tabsCtrl.move(tabItemCtrl, newIndex);
});
}
function watchActiveAttribute() {
var unwatch = scope.$parent.$watch('!!(' + attr.mdActive + ')', activeWatchAction);
scope.$on('$destroy', unwatch);
function activeWatchAction(isActive) {
var isSelected = tabsCtrl.getSelectedItem() === tabItemCtrl;
if (isActive && !isSelected) {
tabsCtrl.select(tabItemCtrl);
} else if (!isActive && isSelected) {
tabsCtrl.deselect(tabItemCtrl);
}
}
}
function watchDisabled() {
scope.$watch(tabItemCtrl.isDisabled, disabledWatchAction);
function disabledWatchAction(isDisabled) {
element.attr('aria-disabled', isDisabled);
// Auto select `next` tab when disabled
var isSelected = (tabsCtrl.getSelectedItem() === tabItemCtrl);
if (isSelected && isDisabled) {
tabsCtrl.select(tabsCtrl.next() || tabsCtrl.previous());
}
}
}
function configureAria() {
// Link together the content area and tabItemCtrl with an id
var tabId = attr.id || ('tab_' + $mdUtil.nextUid());
element.attr({
id: tabId,
role: 'tab',
tabIndex: -1 //this is also set on select/deselect in tabItemCtrl
});
// Only setup the contentContainer's aria attributes if tab content is provided
if (tabContent.length) {
var tabContentId = 'content_' + tabId;
if (!element.attr('aria-controls')) {
element.attr('aria-controls', tabContentId);
}
tabItemCtrl.contentContainer.attr({
id: tabContentId,
role: 'tabpanel',
'aria-labelledby': tabId
});
}
}
};
}
}
})();
@@ -0,0 +1,138 @@
(function() {
'use strict';
angular.module('material.components.tabs')
.controller('$mdTabs', MdTabsController);
function MdTabsController($scope, $element, $mdUtil, $timeout) {
var tabsList = $mdUtil.iterator([], false);
var self = this;
// Properties
self.$element = $element;
self.scope = $scope;
// The section containing the tab content $elements
var contentArea = self.contentArea = angular.element($element[0].querySelector('.md-tabs-content'));
// Methods from iterator
var inRange = self.inRange = tabsList.inRange;
var indexOf = self.indexOf = tabsList.indexOf;
var itemAt = self.itemAt = tabsList.itemAt;
self.count = tabsList.count;
self.getSelectedItem = getSelectedItem;
self.getSelectedIndex = getSelectedIndex;
self.add = add;
self.remove = remove;
self.move = move;
self.select = select;
self.focus = focus;
self.deselect = deselect;
self.next = next;
self.previous = previous;
$scope.$on('$destroy', function() {
deselect(getSelectedItem());
for (var i = tabsList.count() - 1; i >= 0; i--) {
remove(tabsList[i], true);
}
});
// Get the selected tab
function getSelectedItem() {
return itemAt($scope.selectedIndex);
}
function getSelectedIndex() {
return $scope.selectedIndex;
}
// Add a new tab.
// Returns a method to remove the tab from the list.
function add(tab, index) {
tabsList.add(tab, index);
// Select the new tab if we don't have a selectedIndex, or if the
// selectedIndex we've been waiting for is this tab
if ($scope.selectedIndex === -1 || !angular.isNumber($scope.selectedIndex) ||
$scope.selectedIndex === self.indexOf(tab)) {
tab.onAdd(self.contentArea, false);
self.select(tab);
} else {
tab.onAdd(self.contentArea, true);
}
$scope.$broadcast('$mdTabsChanged');
}
function remove(tab, noReselect) {
if (!tabsList.contains(tab)) return;
if (noReselect) return;
var isSelectedItem = getSelectedItem() === tab,
newTab = previous() || next();
deselect(tab);
tabsList.remove(tab);
tab.onRemove();
$scope.$broadcast('$mdTabsChanged');
if (isSelectedItem) { select(newTab); }
}
// Move a tab (used when ng-repeat order changes)
function move(tab, toIndex) {
var isSelected = getSelectedItem() === tab;
tabsList.remove(tab);
tabsList.add(tab, toIndex);
if (isSelected) select(tab);
$scope.$broadcast('$mdTabsChanged');
}
function select(tab, rightToLeft) {
if (!tab || tab.isSelected || tab.isDisabled()) return;
if (!tabsList.contains(tab)) return;
if (!angular.isDefined(rightToLeft)) {
rightToLeft = indexOf(tab) < $scope.selectedIndex;
}
deselect(getSelectedItem(), rightToLeft);
$scope.selectedIndex = indexOf(tab);
tab.isSelected = true;
tab.onSelect(rightToLeft);
$scope.$broadcast('$mdTabsChanged');
}
function focus(tab) {
// this variable is watched by pagination
self.tabToFocus = tab;
}
function deselect(tab, rightToLeft) {
if (!tab || !tab.isSelected) return;
if (!tabsList.contains(tab)) return;
$scope.selectedIndex = -1;
tab.isSelected = false;
tab.onDeselect(rightToLeft);
}
function next(tab, filterFn) {
return tabsList.next(tab || getSelectedItem(), filterFn || isTabEnabled);
}
function previous(tab, filterFn) {
return tabsList.previous(tab || getSelectedItem(), filterFn || isTabEnabled);
}
function isTabEnabled(tab) {
return tab && !tab.isDisabled();
}
}
})();
@@ -0,0 +1,169 @@
(function() {
'use strict';
angular.module('material.components.tabs')
.directive('mdTabs', TabsDirective);
/**
* @ngdoc directive
* @name mdTabs
* @module material.components.tabs
*
* @restrict E
*
* @description
* The `<md-tabs>` directive serves as the container for 1..n `<md-tab>` child directives to produces a Tabs components.
* In turn, the nested `<md-tab>` directive is used to specify a tab label for the **header button** and a [optional] tab view
* content that will be associated with each tab button.
*
* Below is the markup for its simplest usage:
*
* <hljs lang="html">
* <md-tabs>
* <md-tab label="Tab #1"></md-tab>
* <md-tab label="Tab #2"></md-tab>
* <md-tab label="Tab #3"></md-tab>
* <md-tabs>
* </hljs>
*
* Tabs supports three (3) usage scenarios:
*
* 1. Tabs (buttons only)
* 2. Tabs with internal view content
* 3. Tabs with external view content
*
* **Tab-only** support is useful when tab buttons are used for custom navigation regardless of any other components, content, or views.
* **Tabs with internal views** are the traditional usages where each tab has associated view content and the view switching is managed internally by the Tabs component.
* **Tabs with external view content** is often useful when content associated with each tab is independently managed and data-binding notifications announce tab selection changes.
*
* > As a performance bonus, if the tab content is managed internally then the non-active (non-visible) tab contents are temporarily disconnected from the `$scope.$digest()` processes; which restricts and optimizes DOM updates to only the currently active tab.
*
* Additional features also include:
*
* * Content can include any markup.
* * If a tab is disabled while active/selected, then the next tab will be auto-selected.
* * If the currently active tab is the last tab, then next() action will select the first tab.
* * Any markup (other than **`<md-tab>`** tags) will be transcluded into the tab header area BEFORE the tab buttons.
*
* ### Explanation of tab stretching
*
* Initially, tabs will have an inherent size. This size will either be defined by how much space is needed to accommodate their text or set by the user through CSS. Calculations will be based on this size.
*
* On mobile devices, tabs will be expanded to fill the available horizontal space. When this happens, all tabs will become the same size.
*
* On desktops, by default, stretching will never occur.
*
* This default behavior can be overridden through the `md-stretch-tabs` attribute. Here is a table showing when stretching will occur:
*
* `md-stretch-tabs` | mobile | desktop
* ------------------|-----------|--------
* `auto` | stretched | ---
* `always` | stretched | stretched
* `never` | --- | ---
*
* @param {integer=} md-selected Index of the active/selected tab
* @param {boolean=} md-no-ink If present, disables ink ripple effects.
* @param {boolean=} md-no-bar If present, disables the selection ink bar.
* @param {string=} md-align-tabs Attribute to indicate position of tab buttons: `bottom` or `top`; default is `top`
* @param {string=} md-stretch-tabs Attribute to indicate whether or not to stretch tabs: `auto`, `always`, or `never`; default is `auto`
*
* @usage
* <hljs lang="html">
* <md-tabs md-selected="selectedIndex" >
* <img ng-src="img/angular.png" class="centered">
*
* <md-tab
* ng-repeat="tab in tabs | orderBy:predicate:reversed"
* md-on-select="onTabSelected(tab)"
* md-on-deselect="announceDeselected(tab)"
* disabled="tab.disabled" >
*
* <md-tab-label>
* {{tab.title}}
* <img src="img/removeTab.png"
* ng-click="removeTab(tab)"
* class="delete" >
* </md-tab-label>
*
* {{tab.content}}
*
* </md-tab>
*
* </md-tabs>
* </hljs>
*
*/
function TabsDirective($mdTheming) {
return {
restrict: 'E',
controller: '$mdTabs',
require: 'mdTabs',
transclude: true,
scope: {
selectedIndex: '=?mdSelected'
},
template:
'<section class="md-header" ' +
'ng-class="{\'md-paginating\': pagination.active}">' +
'<button class="md-paginator md-prev" ' +
'ng-if="pagination.active && pagination.hasPrev" ' +
'ng-click="pagination.clickPrevious()" ' +
'aria-hidden="true">' +
'</button>' +
// overflow: hidden container when paginating
'<div class="md-header-items-container" md-tabs-pagination>' +
// flex container for <md-tab> elements
'<div class="md-header-items">' +
'<md-tabs-ink-bar></md-tabs-ink-bar>' +
'</div>' +
'</div>' +
'<button class="md-paginator md-next" ' +
'ng-if="pagination.active && pagination.hasNext" ' +
'ng-click="pagination.clickNext()" ' +
'aria-hidden="true">' +
'</button>' +
'</section>' +
'<section class="md-tabs-content"></section>',
link: postLink
};
function postLink(scope, element, attr, tabsCtrl, transclude) {
scope.stretchTabs = attr.hasOwnProperty('mdStretchTabs') ? attr.mdStretchTabs || 'always' : 'auto';
$mdTheming(element);
configureAria();
watchSelected();
transclude(scope.$parent, function(clone) {
angular.element(element[0].querySelector('.md-header-items')).append(clone);
});
function configureAria() {
element.attr('role', 'tablist');
}
function watchSelected() {
scope.$watch('selectedIndex', function watchSelectedIndex(newIndex, oldIndex) {
if (oldIndex == newIndex) return;
var rightToLeft = oldIndex > newIndex;
tabsCtrl.deselect(tabsCtrl.itemAt(oldIndex), rightToLeft);
if (tabsCtrl.inRange(newIndex)) {
var newTab = tabsCtrl.itemAt(newIndex);
while (newTab && newTab.isDisabled()) {
newTab = newIndex > oldIndex
? tabsCtrl.next(newTab)
: tabsCtrl.previous(newTab);
}
tabsCtrl.select(newTab, rightToLeft);
}
});
}
}
}
})();
@@ -0,0 +1,61 @@
$tabs-color-palette: $primary-color-palette !default;
$tabs-header-background-color: map-get($tabs-color-palette, '500') !default;
$tabs-inactive-color: map-get($tabs-color-palette, '100') !default;
$tabs-active-color: $primary-color-palette-contrast-color !default;
$tabs-disabled-color: $foreground-quarternary-color !default;
$tabs-highlight-color: $primary-color-palette-contrast-color !default;
md-tabs.md-THEME_NAME-theme {
.md-header {
background-color: '{{primary-color}}';
}
&.md-accent {
.md-header {
background-color: '{{accent-color}}';
}
md-tab:not([disabled]) {
color: '{{accent-100}}';
&.active {
color: '{{accent-contrast}}';
}
}
}
&.md-warn {
.md-header {
background-color: '{{warn-color}}';
}
md-tab:not([disabled]) {
color: '{{warn-100}}';
&.active {
color: '{{warn-contrast}}';
}
}
}
md-tabs-ink-bar {
color: $tabs-highlight-color;
background: $tabs-highlight-color;
}
md-tab {
color: '{{primary-100}}';
&.active {
color: '{{primary-contrast}}';
}
&[disabled] {
color: '{{foreground-4}}';
}
&:focus {
color: '{{primary-contrast}}';
background-color: '{{primary-contrast-0.1}}';
}
.md-ripple-container {
color: $tabs-highlight-color;
}
}
}
@@ -0,0 +1,30 @@
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.tabs
* @description
*
* Tabs, created with the `<md-tabs>` directive provide *tabbed* navigation with different styles.
* The Tabs component consists of clickable tabs that are aligned horizontally side-by-side.
*
* Features include support for:
*
* - static or dynamic tabs,
* - responsive designs,
* - accessibility support (ARIA),
* - tab pagination,
* - external or internal tab content,
* - focus indicators and arrow-key navigations,
* - programmatic lookup and access to tab controllers, and
* - dynamic transitions through different tab contents.
*
*/
/*
* @see js folder for tabs implementation
*/
angular.module('material.components.tabs', [
'material.core'
]);
})();
@@ -0,0 +1,175 @@
// Tabs
$tabs-paginator-width: $baseline-grid * 4 !default;
$tabs-tab-width: $baseline-grid * 12 !default;
$tabs-header-height: 48px !default;
md-tabs {
display: block;
width: 100%;
font-weight: 500;
}
.md-header {
width: 100%;
height: $tabs-header-height;
box-sizing: border-box;
position: relative;
}
.md-paginator {
z-index: 1;
margin-right: -2px;
display: flex;
justify-content: center;
align-items: center;
width: $tabs-paginator-width;
min-height: 100%;
cursor: pointer;
border: none;
background-color: transparent;
background-repeat: no-repeat;
background-position: center center;
position: absolute;
&.md-prev {
left: 0;
}
&.md-next {
right: 0;
}
/* TODO Once we have a better way to inline svg images, change this
to use svgs correctly */
&.md-prev {
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE3LjEuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPiA8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPiA8c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIyNHB4IiBoZWlnaHQ9IjI0cHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMjQgMjQiIHhtbDpzcGFjZT0icHJlc2VydmUiPiA8ZyBpZD0iSGVhZGVyIj4gPGc+IDxyZWN0IHg9Ii02MTgiIHk9Ii0xMjA4IiBmaWxsPSJub25lIiB3aWR0aD0iMTQwMCIgaGVpZ2h0PSIzNjAwIi8+IDwvZz4gPC9nPiA8ZyBpZD0iTGFiZWwiPiA8L2c+IDxnIGlkPSJJY29uIj4gPGc+IDxwb2x5Z29uIHBvaW50cz0iMTUuNCw3LjQgMTQsNiA4LDEyIDE0LDE4IDE1LjQsMTYuNiAxMC44LDEyIAkJIiBzdHlsZT0iZmlsbDp3aGl0ZTsiLz4gPHJlY3QgZmlsbD0ibm9uZSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ii8+IDwvZz4gPC9nPiA8ZyBpZD0iR3JpZCIgZGlzcGxheT0ibm9uZSI+IDxnIGRpc3BsYXk9ImlubGluZSI+IDwvZz4gPC9nPiA8L3N2Zz4NCg==');
}
&.md-next {
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE3LjEuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPiA8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPiA8c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIyNHB4IiBoZWlnaHQ9IjI0cHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMjQgMjQiIHhtbDpzcGFjZT0icHJlc2VydmUiPiA8ZyBpZD0iSGVhZGVyIj4gPGc+IDxyZWN0IHg9Ii02MTgiIHk9Ii0xMzM2IiBmaWxsPSJub25lIiB3aWR0aD0iMTQwMCIgaGVpZ2h0PSIzNjAwIi8+IDwvZz4gPC9nPiA8ZyBpZD0iTGFiZWwiPiA8L2c+IDxnIGlkPSJJY29uIj4gPGc+IDxwb2x5Z29uIHBvaW50cz0iMTAsNiA4LjYsNy40IDEzLjIsMTIgOC42LDE2LjYgMTAsMTggMTYsMTIgCQkiIHN0eWxlPSJmaWxsOndoaXRlOyIvPiA8cmVjdCBmaWxsPSJub25lIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiLz4gPC9nPiA8L2c+IDxnIGlkPSJHcmlkIiBkaXNwbGF5PSJub25lIj4gPGcgZGlzcGxheT0iaW5saW5lIj4gPC9nPiA8L2c+IDwvc3ZnPg0K');
}
}
/* If `center` justified, change to left-justify if paginating */
md-tabs[center] .md-header:not(.md-paginating) .md-header-items {
justify-content: center;
}
.md-paginating .md-header-items-container {
left: $tabs-paginator-width;
right: $tabs-paginator-width;
}
.md-header-items-container {
overflow: hidden;
position: absolute;
left: 0;
right: 0;
height: 100%;
white-space: nowrap;
font-size: 14px;
font-weight: 500;
text-transform: uppercase;
margin: auto;
.md-header-items {
display: flex;
box-sizing: border-box;
transition: transform $swift-ease-in-out-duration $swift-ease-in-out-timing-function;
transform: translate3d(0, 0, 0);
height: 100%;
width: 99999px;
}
}
.md-tabs-content {
overflow: hidden;
width: 100%;
position: relative;
.md-tab-content {
height: 100%;
&.ng-hide {
&.ng-animate {
display: block !important;
}
}
&.ng-animate {
transition: transform $swift-ease-in-out-duration $swift-ease-in-out-timing-function;
transform: translateX(0);
&.ng-hide-add {
transform: translateX(-100%);
&.md-transition-rtl {
transform: translateX(100%);
}
}
&.ng-hide-remove {
position: absolute;
transform: translateX(100%);
top: 0;
left: 0;
right: 0;
bottom: 0;
&.md-transition-rtl {
transform: translateX(-100%);
}
&.ng-hide-remove-active {
transform: translateX(0);
}
}
}
}
}
md-tabs-ink-bar {
$time: 0.25s;
$delay: $time * 0.3;
$shortTime: $time;
z-index: 1;
display: none;
position: absolute;
left: 0;
bottom: 0;
box-sizing: border-box;
height: 2px;
margin-top: -2px;
transform: scaleX(1);
transform-origin: 0 0;
&.md-transition-right {
transition: right $time $swift-ease-in-out-timing-function,
left $shortTime $swift-ease-in-out-timing-function $delay;
}
&.md-transition-left {
transition: right $shortTime $swift-ease-in-out-timing-function $delay,
left $time $swift-ease-in-out-timing-function;
}
}
md-tab {
display: flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 0;
overflow: hidden;
height: 100%;
text-align: center;
cursor: pointer;
padding: 20px 24px;
box-sizing: border-box;
transition: background 0.35s $swift-ease-in-out-timing-function,
color 0.35s $swift-ease-in-out-timing-function;
&[disabled] {
pointer-events: none;
cursor: default;
}
@include not-selectable();
&:focus {
outline: none;
}
md-tab-label {
flex: 1 1 auto;
z-index: 100;
opacity: 1;
overflow: hidden;
}
}
@@ -0,0 +1,294 @@
describe('<md-tabs>', function() {
beforeEach(module('material.components.tabs'));
beforeEach(function() {
TestUtil.mockElementFocus(this);
this.addMatchers({
toBeActiveTab: function(checkFocus) {
var fails = [];
var actual = this.actual;
this.message = function() {
return 'Expected ' + angular.mock.dump(actual) + (this.isNot ? ' not ' : ' ') +
'to be the active tab. Failures: ' + fails.join(', ');
};
if (!actual.hasClass('active')) {
fails.push('does not have active class');
}
if (actual.attr('aria-selected') != 'true') {
fails.push('aria-selected is not true');
}
if (actual.attr('tabindex') != '0') {
fails.push('tabindex is not 0');
}
return fails.length === 0;
}
});
});
function setup(template) {
var el;
inject(function($compile, $rootScope) {
el = $compile(template)($rootScope.$new());
$rootScope.$apply();
});
return el;
}
function triggerKeydown(el, keyCode) {
return el.triggerHandler({
type: 'keydown',
keyCode: keyCode
});
}
describe('activating tabs', function() {
it('should select first tab by default', function() {
var tabs = setup('<md-tabs>' +
'<md-tab></md-tab>' +
'<md-tab></md-tab>' +
'</md-tabs>');
expect(tabs.find('md-tab').eq(0)).toBeActiveTab();
});
it('should select & focus tab on click', inject(function($document) {
var tabs = setup('<md-tabs>' +
'<md-tab></md-tab>' +
'<md-tab></md-tab>' +
'<md-tab ng-disabled="true"></md-tab>' +
'</md-tabs>');
var tabItems = tabs.find('md-tab');
tabs.find('md-tab').eq(1).triggerHandler('click');
expect(tabItems.eq(1)).toBeActiveTab();
expect($document.activeElement).toBe(tabItems[1]);
tabs.find('md-tab').eq(0).triggerHandler('click');
expect(tabItems.eq(0)).toBeActiveTab();
expect($document.activeElement).toBe(tabItems[0]);
}));
it('should focus tab on arrow if tab is enabled', inject(function($document, $mdConstant, $timeout) {
var tabs = setup('<md-tabs>' +
'<md-tab></md-tab>' +
'<md-tab ng-disabled="true"></md-tab>' +
'<md-tab></md-tab>' +
'</md-tabs>');
var tabItems = tabs.find('md-tab');
expect(tabItems.eq(0)).toBeActiveTab();
// Boundary case, do nothing
triggerKeydown(tabItems.eq(0), $mdConstant.KEY_CODE.LEFT_ARROW);
expect(tabItems.eq(0)).toBeActiveTab();
// Tab 0 should still be active, but tab 2 focused (skip tab 1 it's disabled)
triggerKeydown(tabItems.eq(0), $mdConstant.KEY_CODE.RIGHT_ARROW);
expect(tabItems.eq(0)).toBeActiveTab();
$timeout.flush();
expect($document.activeElement).toBe(tabItems[2]);
// Boundary case, do nothing
triggerKeydown(tabItems.eq(0), $mdConstant.KEY_CODE.RIGHT_ARROW);
expect(tabItems.eq(0)).toBeActiveTab();
$timeout.flush();
expect($document.activeElement).toBe(tabItems[2]);
// Skip tab 1 again, it's disabled
triggerKeydown(tabItems.eq(2), $mdConstant.KEY_CODE.LEFT_ARROW);
expect(tabItems.eq(0)).toBeActiveTab();
$timeout.flush();
expect($document.activeElement).toBe(tabItems[0]);
}));
it('should select tab on space or enter', inject(function($mdConstant) {
var tabs = setup('<md-tabs>' +
'<md-tab></md-tab>' +
'<md-tab></md-tab>' +
'</md-tabs>');
var tabItems = tabs.find('md-tab');
triggerKeydown(tabItems.eq(1), $mdConstant.KEY_CODE.ENTER);
expect(tabItems.eq(1)).toBeActiveTab();
triggerKeydown(tabItems.eq(0), $mdConstant.KEY_CODE.SPACE);
expect(tabItems.eq(0)).toBeActiveTab();
}));
it('the active tab\'s content should always be connected', inject(function($timeout) {
var tabs = setup('<md-tabs>' +
'<md-tab label="label1!">content1!</md-tab>' +
'<md-tab label="label2!">content2!</md-tab>' +
'</md-tabs>');
var tabItems = tabs.find('md-tab');
var contents = angular.element(tabs[0].querySelectorAll('.md-tab-content'));
$timeout.flush();
expect(contents.eq(0).scope().$$disconnected).toBeFalsy();
expect(contents.eq(1).scope().$$disconnected).toBeTruthy();
tabItems.eq(1).triggerHandler('click');
expect(contents.eq(0).scope().$$disconnected).toBeTruthy();
expect(contents.eq(1).scope().$$disconnected).toBeFalsy();
}));
it('should bind to selected', function() {
var tabs = setup('<md-tabs md-selected="current">' +
'<md-tab></md-tab>' +
'<md-tab></md-tab>' +
'<md-tab></md-tab>' +
'</md-tabs>');
var tabItems = tabs.find('md-tab');
expect(tabItems.eq(0)).toBeActiveTab();
expect(tabs.scope().current).toBe(0);
tabs.scope().$apply('current = 1');
expect(tabItems.eq(1)).toBeActiveTab();
tabItems.eq(2).triggerHandler('click');
expect(tabs.scope().current).toBe(2);
});
it('should use active binding', function() {
var tabs = setup('<md-tabs>' +
'<md-tab md-active="active0"></md-tab>' +
'<md-tab md-active="active1"></md-tab>' +
'<md-tab md-active="active2"></md-tab>' +
'</md-tabs>');
var tabItems = tabs.find('md-tab');
tabs.scope().$apply('active2 = true');
expect(tabItems.eq(2)).toBeActiveTab();
tabs.scope().$apply('active1 = true');
expect(tabItems.eq(1)).toBeActiveTab();
tabs.scope().$apply('active1 = false');
expect(tabItems.eq(1)).not.toBeActiveTab();
});
it('disabling active tab', function() {
var tabs = setup('<md-tabs>' +
'<md-tab ng-disabled="disabled0"></md-tab>' +
'<md-tab ng-disabled="disabled1"></md-tab>' +
'</md-tabs>');
var tabItems = tabs.find('md-tab');
expect(tabItems.eq(0)).toBeActiveTab();
tabs.scope().$apply('disabled0 = true');
expect(tabItems.eq(1)).toBeActiveTab();
expect(tabItems.eq(0).attr('aria-disabled')).toBe('true');
expect(tabItems.eq(1).attr('aria-disabled')).not.toBe('true');
tabs.scope().$apply('disabled0 = false; disabled1 = true');
expect(tabItems.eq(0)).toBeActiveTab();
expect(tabItems.eq(0).attr('aria-disabled')).not.toBe('true');
expect(tabItems.eq(1).attr('aria-disabled')).toBe('true');
});
it('swiping tabs', function() {
var tabs = setup('<md-tabs>' +
'<md-tab></md-tab>' +
'<md-tab></md-tab>' +
'</md-tabs>');
var tabItems = tabs.find('md-tab');
tabItems.eq(0).isolateScope().onSwipe({
type: 'swipeleft'
});
expect(tabItems.eq(1)).toBeActiveTab();
tabItems.eq(1).isolateScope().onSwipe({
type: 'swipeleft'
});
expect(tabItems.eq(1)).toBeActiveTab();
tabItems.eq(1).isolateScope().onSwipe({
type: 'swipeleft'
});
expect(tabItems.eq(1)).toBeActiveTab();
tabItems.eq(1).isolateScope().onSwipe({
type: 'swiperight'
});
expect(tabItems.eq(0)).toBeActiveTab();
tabItems.eq(0).isolateScope().onSwipe({
type: 'swiperight'
});
expect(tabItems.eq(0)).toBeActiveTab();
});
});
describe('tab label & content DOM', function() {
it('should support all 3 label types', function() {
var tabs1 = setup('<md-tabs>' +
'<md-tab label="<b>super</b> label"></md-tab>' +
'</md-tabs>');
expect(tabs1.find('md-tab-label').html()).toBe('<b>super</b> label');
var tabs2 = setup('<md-tabs>' +
'<md-tab><b>super</b> label</md-tab>' +
'</md-tabs>');
expect(tabs2.find('md-tab-label').html()).toBe('<b>super</b> label');
var tabs3 = setup('<md-tabs>' +
'<md-tab><md-tab-label><b>super</b> label</md-tab-label></md-tab>' +
'</md-tabs>');
expect(tabs3.find('md-tab-label').html()).toBe('<b>super</b> label');
});
it('should support content inside with each kind of label', function() {
var tabs1 = setup('<md-tabs>' +
'<md-tab label="label that!"><b>content</b> that!</md-tab>' +
'</md-tabs>');
expect(tabs1.find('md-tab-label').html()).toBe('label that!');
expect(tabs1[0].querySelector('.md-tabs-content .md-tab-content').innerHTML)
.toBe('<b>content</b> that!');
var tabs2 = setup('<md-tabs>' +
'<md-tab><md-tab-label>label that!</md-tab-label><b>content</b> that!</md-tab>' +
'</md-tabs>');
expect(tabs1.find('md-tab-label').html()).toBe('label that!');
expect(tabs1[0].querySelector('.md-tabs-content .md-tab-content').innerHTML)
.toBe('<b>content</b> that!');
});
it('should connect content with child of the outside scope', function() {
var tabs = setup('<md-tabs>' +
'<md-tab label="label!">content!</md-tab>' +
'</md-tabs>');
var content = angular.element(tabs[0].querySelector('.md-tab-content'));
expect(content.scope().$parent.$id).toBe(tabs.find('md-tab').scope().$id);
});
});
describe('aria', function() {
it('should link tab content to tabItem with auto-generated ids', function() {
var tabs = setup('<md-tabs>' +
'<md-tab label="label!">content!</md-tab>' +
'</md-tabs>');
var tabItem = tabs.find('md-tab');
var tabContent = angular.element(tabs[0].querySelector('.md-tab-content'));
expect(tabs.attr('role')).toBe('tablist');
expect(tabItem.attr('id')).toBeTruthy();
expect(tabItem.attr('role')).toBe('tab');
expect(tabItem.attr('aria-controls')).toBe(tabContent.attr('id'));
expect(tabContent.attr('id')).toBeTruthy();
expect(tabContent.attr('role')).toBe('tabpanel');
expect(tabContent.attr('aria-labelledby')).toBe(tabItem.attr('id'));
//Unique ids check
expect(tabContent.attr('id')).not.toEqual(tabItem.attr('id'));
});
});
});