From 1e28e1381453e5fce84118f8d5e3f58850bab13f Mon Sep 17 00:00:00 2001 From: Robert Messerle Date: Mon, 22 Dec 2014 22:31:56 -0800 Subject: [PATCH] feat(tabs): refactors tabs to function closer to spec Closes #1087 Closes #1107 Closes #1140 Closes #1247 Closes #1261 Closes #1380 Closes #1387 Closes #1403 Closes #1443 Closes #1505 Closes #1506 Closes #1516 Closes #1518 Closes #1564 Closes #1570 Closes #1620 Closes #1626 Closes #1698 Closes #1777 Closes #1788 Closes #1850 Closes #1959 --- .../tabs/demoDynamicTabs/index.html | 15 +- src/components/tabs/demoDynamicTabs/script.js | 65 ++-- .../demoDynamicTabs/{style.css => style.scss} | 36 +- src/components/tabs/demoStaticTabs/index.html | 55 +-- src/components/tabs/demoStaticTabs/style.css | 92 ----- src/components/tabs/demoStaticTabs/style.scss | 29 ++ src/components/tabs/js/inkBarDirective.js | 57 --- .../tabs/js/labelTemplateDirective.js | 22 ++ src/components/tabs/js/paginationDirective.js | 251 ------------- src/components/tabs/js/tabContentDirective.js | 21 ++ src/components/tabs/js/tabDirective.js | 110 ++++++ src/components/tabs/js/tabItemController.js | 109 ------ src/components/tabs/js/tabItemDirective.js | 240 +----------- src/components/tabs/js/tabScroll.js | 22 ++ src/components/tabs/js/tabsController.js | 354 ++++++++++++------ src/components/tabs/js/tabsDirective.js | 214 ++++++----- src/components/tabs/tabs-theme.scss | 86 +++-- src/components/tabs/tabs.scss | 336 +++++++++-------- src/components/tabs/tabs.spec.js | 173 ++++----- src/core/services/ripple/ripple.js | 2 +- src/core/style/mixins.scss | 8 + 21 files changed, 943 insertions(+), 1354 deletions(-) rename src/components/tabs/demoDynamicTabs/{style.css => style.scss} (61%) delete mode 100644 src/components/tabs/demoStaticTabs/style.css create mode 100644 src/components/tabs/demoStaticTabs/style.scss delete mode 100644 src/components/tabs/js/inkBarDirective.js create mode 100644 src/components/tabs/js/labelTemplateDirective.js delete mode 100644 src/components/tabs/js/paginationDirective.js create mode 100644 src/components/tabs/js/tabContentDirective.js create mode 100644 src/components/tabs/js/tabDirective.js delete mode 100644 src/components/tabs/js/tabItemController.js create mode 100644 src/components/tabs/js/tabScroll.js diff --git a/src/components/tabs/demoDynamicTabs/index.html b/src/components/tabs/demoDynamicTabs/index.html index b21806aa3a0..bb3b3e7e0b2 100644 --- a/src/components/tabs/demoDynamicTabs/index.html +++ b/src/components/tabs/demoDynamicTabs/index.html @@ -1,21 +1,19 @@
- + - -
+
- - Remove Tab - +
+ Remove Tab
-
+
@@ -27,7 +25,7 @@
-
+
Add a new Tab: @@ -41,5 +39,4 @@
-
diff --git a/src/components/tabs/demoDynamicTabs/script.js b/src/components/tabs/demoDynamicTabs/script.js index e3e404b14dd..acc409313a5 100644 --- a/src/components/tabs/demoDynamicTabs/script.js +++ b/src/components/tabs/demoDynamicTabs/script.js @@ -1,39 +1,40 @@ 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!"} - ]; + .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!"} + ], + selected = null, + previous = null; - $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.tabs = tabs; + $scope.selectedIndex = 2; + + $scope.$watch('selectedIndex', function(current, old){ + previous = selected; + selected = tabs[current]; + if ( old && (old != current)) $log.debug('Goodbye ' + previous.title + '!'); + if ( current ) $log.debug('Hello ' + selected.title + '!'); + }); - $scope.addTab = function (title, view) { - view = view || title + " Content View"; - tabs.push({ title: title, content: view, disabled: false}); - }; + $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; - } - } - }; + $scope.removeTab = function (tab) { + var index = tabs.indexOf(tab); + tabs.splice(index, 1); + }; - }); + }); diff --git a/src/components/tabs/demoDynamicTabs/style.css b/src/components/tabs/demoDynamicTabs/style.scss similarity index 61% rename from src/components/tabs/demoDynamicTabs/style.css rename to src/components/tabs/demoDynamicTabs/style.scss index ba9af889cfd..bd7057edbbc 100644 --- a/src/components/tabs/demoDynamicTabs/style.css +++ b/src/components/tabs/demoDynamicTabs/style.scss @@ -1,33 +1,21 @@ -.sample { - height:500px; -} - .remove-tab { margin-bottom: 40px; } - -.demo-tab { - height: 300px; - text-align: center; -} - -.demo-tab button { - margin-bottom: 40px; +.demo-tab > div > div { + padding: 25px; + box-sizing: border-box; } - -.tab0, .tab1, .tab2, .tab3 { +.edit-form input { + width: 100%; } - -md-tabs, md-tabs .md-header { - border-bottom: 1px solid #D8D8D8; +md-tabs { + border-bottom: 1px solid rgba(0,0,0,0.12); } md-tab[disabled] { opacity: 0.5; } - - -.md-tab-content { - background: white; +label { + text-align: left; } .title { @@ -35,26 +23,20 @@ md-tab[disabled] { padding-right: 8px; text-align: left; text-transform: uppercase; - color: #888; margin-top: 24px; } - - [layout-align] > * { margin-left: 8px; } - form > [layout] > * { margin-left: 8px; } form > [layout] > span { padding-top:2px } - .long > input { width: 264px; } - .md-button.add-tab { margin-top:20px; max-height:30px !important; diff --git a/src/components/tabs/demoStaticTabs/index.html b/src/components/tabs/demoStaticTabs/index.html index c8db2fbf93f..f2c30d32c9c 100644 --- a/src/components/tabs/demoStaticTabs/index.html +++ b/src/components/tabs/demoStaticTabs/index.html @@ -1,48 +1,29 @@
- - Item One + + Item One + + View for Item #1
+ data.selectedIndex = 0; +
- - {{data.secondLabel}} + + {{data.secondLabel}} + + View for Item #2
+ data.selectedIndex = 1; +
- - Item Three + + Item Three + + View for Item #3
+ data.selectedIndex = 2; +
- -
- View for Item #1
- data.selectedIndex = 0 -
-
- View for {{data.secondLabel}}
- data.selectedIndex = 1 -
-
- View for Item #3
- data.selectedIndex = 2 -
-
-
diff --git a/src/components/tabs/demoStaticTabs/style.css b/src/components/tabs/demoStaticTabs/style.css deleted file mode 100644 index 7f32487eca8..00000000000 --- a/src/components/tabs/demoStaticTabs/style.css +++ /dev/null @@ -1,92 +0,0 @@ -.sample { - height:450px; -} - -#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; -} -md-tabs .md-paginator { - color: white; -} diff --git a/src/components/tabs/demoStaticTabs/style.scss b/src/components/tabs/demoStaticTabs/style.scss new file mode 100644 index 00000000000..cd4ce504d00 --- /dev/null +++ b/src/components/tabs/demoStaticTabs/style.scss @@ -0,0 +1,29 @@ +.sample { + height:450px; + md-tab-content { + padding: 25px; + &:nth-child(1) { + background-color: #42A5F5; + } + &:nth-child(2) { + background-color: #689F38; + } + &:nth-child(3) { + background-color: #26C6DA; + } + } + .after-tabs-area { + padding: 25px; + > span { + margin-top:25px; + padding-right: 15px; + vertical-align: middle; + line-height: 30px; + height: 35px; + } + > md-checkbox { + margin-top:26px; + margin-left:0px; + } + } +} diff --git a/src/components/tabs/js/inkBarDirective.js b/src/components/tabs/js/inkBarDirective.js deleted file mode 100644 index 74920ec0dea..00000000000 --- a/src/components/tabs/js/inkBarDirective.js +++ /dev/null @@ -1,57 +0,0 @@ -(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) { - var mdNoBar = !!ctrls[0]; - - var tabsCtrl = ctrls[1], - debouncedUpdateBar = $$rAF.throttle(updateBar); - - tabsCtrl.inkBarElement = element; - - scope.$on('$mdTabsPaginationChanged', debouncedUpdateBar); - - function updateBar() { - var selected = tabsCtrl.getSelectedItem(); - var hideInkBar = !selected || tabsCtrl.count() < 2 || mdNoBar; - - 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 + 'px', right: right + 'px' }); - - lastIndex = index; - } - } - } -} -})(); diff --git a/src/components/tabs/js/labelTemplateDirective.js b/src/components/tabs/js/labelTemplateDirective.js new file mode 100644 index 00000000000..b28a03918a0 --- /dev/null +++ b/src/components/tabs/js/labelTemplateDirective.js @@ -0,0 +1,22 @@ +(function () { + 'use strict'; + angular + .module('material.components.tabs') + .directive('mdLabelTemplate', MdLabelTemplate); + + function MdLabelTemplate ($compile) { + return { + restrict: 'A', + link: link, + scope: { template: '=mdLabelTemplate' }, + require: '^mdTabs' + }; + function link (scope, element, attr, ctrl) { + var index = scope.$parent.$index; + scope.$watch('template', function (html) { + element.html(html); + $compile(element.contents())(ctrl.tabs[index].parent); + }); + } + } +})(); \ No newline at end of file diff --git a/src/components/tabs/js/paginationDirective.js b/src/components/tabs/js/paginationDirective.js deleted file mode 100644 index 476d532cb2a..00000000000 --- a/src/components/tabs/js/paginationDirective.js +++ /dev/null @@ -1,251 +0,0 @@ -(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.throttle(updatePagination); - var tabsParent = element.children(); - var locked = false; - var state = scope.pagination = { - page: -1, - active: false, - clickNext: function() { locked || userChangePage(+1); }, - clickPrevious: function() { locked || 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() { - locked = false; - 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); - locked = true; - 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); - } - } - -} -})(); diff --git a/src/components/tabs/js/tabContentDirective.js b/src/components/tabs/js/tabContentDirective.js new file mode 100644 index 00000000000..4de91d9c234 --- /dev/null +++ b/src/components/tabs/js/tabContentDirective.js @@ -0,0 +1,21 @@ +(function () { + 'use strict'; + angular + .module('material.components.tabs') + .directive('mdTabContent', MdTabContent); + + function MdTabContent ($compile, $mdUtil) { + return { + terminal: true, + scope: { + tab: '=mdTabData', + active: '=mdActive' + }, + link: link + }; + function link (scope, element) { + element.html(scope.tab.template); + $compile(element.contents())(scope.tab.parent); + } + } +})(); diff --git a/src/components/tabs/js/tabDirective.js b/src/components/tabs/js/tabDirective.js new file mode 100644 index 00000000000..c18ca40b533 --- /dev/null +++ b/src/components/tabs/js/tabDirective.js @@ -0,0 +1,110 @@ +/** + * @ngdoc directive + * @name mdTab + * @module material.components.tabs + * + * @restrict E + * + * @description + * Use the `` a nested directive used within `` to specify a tab with a **label** and optional *view content*. + * + * If the `label` attribute is not specified, then an optional `` 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 `` is used as the tab header markup. + * + * If a tab **label** has been identified, then any **non-**`` markup + * will be considered tab content and will be transcluded to the internal `
` 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=} 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 + * + * + * + *

My Tab content

+ *
+ * + * + * + *

My Tab content

+ *
+ *

+ * 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. + *

+ *
+ *
+ * + */ + +(function () { + 'use strict'; + + angular + .module('material.components.tabs') + .directive('mdTab', MdTab); + + function MdTab () { + return { + require: '^mdTabs', + terminal: true, + scope: { + label: '@', + active: '=?mdActive', + disabled: '=?ngDisabled' + }, + link: link + }; + + function link (scope, element, attr, ctrl) { + var tabs = element.parent()[0].getElementsByTagName('md-tab'), + index = Array.prototype.indexOf.call(tabs, element[0]), + data = ctrl.insertTab({ + scope: scope, + parent: scope.$parent, + index: index, + template: getTemplate(), + label: getLabel() + }, index); + + scope.$watch('active', function (active) { if (active) ctrl.select(data.getIndex()); }); + scope.$watch('disabled', function () { ctrl.refreshIndex(); }); + scope.$watch(getTemplate, function (template, oldTemplate) { + if (template === oldTemplate) return; + data.template = template; + ctrl.updateInkBarStyles(); + }); + scope.$watch(getLabel, function (label, oldLabel) { + if (label === oldLabel) return; + data.label = label; + ctrl.updateInkBarStyles(); + }); + scope.$on('$destroy', function () { ctrl.removeTab(data); }); + + function getLabel () { + //-- if label provided, then send label + if (attr.label) return attr.label; + //-- otherwise, we have to search for the `md-tab-label` element + var label = element.find('md-tab-label'); + if (label) return label.html(); + //-- otherwise, we have no label. + return 'Missing Label'; + } + + function getTemplate () { + var content = element.find('md-tab-template'); + return content.length ? content.html() : attr.label ? element.html() : null; + } + } + } +})(); diff --git a/src/components/tabs/js/tabItemController.js b/src/components/tabs/js/tabItemController.js deleted file mode 100644 index 1c984b2e4a9..00000000000 --- a/src/components/tabs/js/tabItemController.js +++ /dev/null @@ -1,109 +0,0 @@ -(function() { -'use strict'; - - -angular.module('material.components.tabs') - .controller('$mdTab', TabItemController); - -function TabItemController($scope, $element, $attrs, $compile, $animate, $mdUtil, $parse, $timeout) { - var self = this; - var tabsCtrl = $element.controller('mdTabs'); - - // Properties - self.contentContainer = angular.element('
'); - 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() { - $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); - - $element - .addClass('active') - .attr({ - 'aria-selected': true, - 'tabIndex': 0 - }) - .on('$md.swipeleft $md.swiperight', onSwipe); - - 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); - - $element - .removeClass('active') - .attr({ - 'aria-selected': false, - 'tabIndex': -1 - }) - .off('$md.swipeleft $md.swiperight', onSwipe); - - toggleAnimationClass(rightToLeft); - $animate.addClass(self.contentContainer, 'ng-hide'); - - $scope.onDeselect(); - } - - ///// Private functions - - function onSwipe(ev) { - $scope.$apply(function() { - if (/left/.test(ev.type)) { - tabsCtrl.select(tabsCtrl.next()); - } else { - tabsCtrl.select(tabsCtrl.previous()); - } - }); - } - - -} - -})(); diff --git a/src/components/tabs/js/tabItemDirective.js b/src/components/tabs/js/tabItemDirective.js index 6fe68404693..ada4e059522 100644 --- a/src/components/tabs/js/tabItemDirective.js +++ b/src/components/tabs/js/tabItemDirective.js @@ -1,234 +1,14 @@ -(function() { -'use strict'; +(function () { + 'use strict'; -angular.module('material.components.tabs') - .directive('mdTab', MdTabDirective); + angular + .module('material.components.tabs') + .directive('mdTabItem', MdTabItem); -/** - * @ngdoc directive - * @name mdTab - * @module material.components.tabs - * - * @restrict E - * - * @description - * Use the `` a nested directive used within `` to specify a tab with a **label** and optional *view content*. - * - * If the `label` attribute is not specified, then an optional `` 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 `` is used as the tab header markup. - * - * If a tab **label** has been identified, then any **non-**`` markup - * will be considered tab content and will be transcluded to the internal `
` 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 - * - * - * - *

My Tab content

- *
- * - * - * - *

My Tab content

- *
- *

- * 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. - *

- *
- *
- * - */ -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('').html(attr.label); - - } else { - // If nothing is found, use the tab's content as the label - tabLabel = angular.element('') - .append(element.contents().remove()); + function MdTabItem () { + return { require: '^mdTabs', link: link }; + function link (scope, element, attr, ctrl) { + ctrl.attachRipple(scope, element); } - - // 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 - - $timeout(element.addClass.bind(element, 'md-tab-themed'), 0, false); - - scope.$watch( - function () { return attr.label; }, - function () { $timeout(function () { tabsCtrl.scope.$broadcast('$mdTabsChanged'); }, 0, false); } - ); - - transcludeTabContent(); - configureAria(); - - $mdInkRipple.attachTabBehavior(scope, element, { - colorElement: tabsCtrl.inkBarElement - }); - tabsCtrl.add(tabItemCtrl); - scope.$on('$destroy', function() { - 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); - - 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)); - }); - } - } - - // 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 - }); - } - } - - }; - } - -} - -})(); +})(); \ No newline at end of file diff --git a/src/components/tabs/js/tabScroll.js b/src/components/tabs/js/tabScroll.js new file mode 100644 index 00000000000..0a55c7cac57 --- /dev/null +++ b/src/components/tabs/js/tabScroll.js @@ -0,0 +1,22 @@ +(function () { + 'use strict'; + angular.module('material.components.tabs') + .directive('mdTabScroll', MdTabScroll); + + function MdTabScroll () { + return { + restrict: 'A', + link: function (scope, element, attr) { + element.on('mousewheel', function (event) { + var newScope = scope.$new(); + newScope.$event = event; + newScope.$element = element; + newScope.$apply(function () { + newScope.$eval(attr.mdTabScroll); + }); + }); + } + + } + } +})(); \ No newline at end of file diff --git a/src/components/tabs/js/tabsController.js b/src/components/tabs/js/tabsController.js index a20861c6d5c..c8e0bb16ea0 100644 --- a/src/components/tabs/js/tabsController.js +++ b/src/components/tabs/js/tabsController.js @@ -1,138 +1,262 @@ -(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 () { + 'use strict'; - function getSelectedIndex() { - return $scope.selectedIndex; - } + angular + .module('material.components.tabs') + .controller('MdTabsController', MdTabsController); + + function MdTabsController ($scope, $element, $window, $timeout, $mdConstant, $mdInkRipple, $mdUtil) { + var ctrl = this, + elements = getElements(); - // Add a new tab. - // Returns a method to remove the tab from the list. - function add(tab, index) { - tabsList.add(tab, index); + ctrl.scope = $scope; + ctrl.parent = $scope.$parent; + ctrl.tabs = []; + ctrl.lastSelectedIndex = null; + ctrl.focusIndex = 0; + ctrl.offsetLeft = 0; + ctrl.hasContent = true; + ctrl.hasFocus = false; + ctrl.lastClick = false; - // Select the new tab if we don't have a selectedIndex, or if the - // selectedIndex we've been waiting for is this tab - if (!angular.isDefined(tab.element.attr('md-active')) && ($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); + ctrl.redirectFocus = redirectFocus; + ctrl.attachRipple = attachRipple; + ctrl.shouldStretchTabs = shouldStretchTabs; + ctrl.shouldPaginate = shouldPaginate; + ctrl.insertTab = insertTab; + ctrl.removeTab = removeTab; + ctrl.select = select; + ctrl.scroll = scroll; + ctrl.nextPage = nextPage; + ctrl.previousPage = previousPage; + ctrl.keydown = keydown; + ctrl.canPageForward = canPageForward; + ctrl.canPageBack = canPageBack; + ctrl.refreshIndex = refreshIndex; + ctrl.incrementSelectedIndex = incrementSelectedIndex; + ctrl.updateInkBarStyles = updateInkBarStyles; + + init(); + + function init () { + $scope.$watch('selectedIndex', handleSelectedIndexChange); + $scope.$watch('$mdTabsCtrl.focusIndex', handleFocusIndexChange); + $scope.$watch('$mdTabsCtrl.offsetLeft', handleOffsetChange); + angular.element($window).on('resize', function () { $scope.$apply(handleWindowResize); }); + $timeout(updateInkBarStyles, 0, false); } - $scope.$broadcast('$mdTabsChanged'); - } + function getElements () { + var elements = {}; + elements.canvas = $element[0].getElementsByTagName('md-tab-canvas')[0]; + elements.wrapper = elements.canvas.getElementsByTagName('md-pagination-wrapper')[0]; + elements.tabs = elements.wrapper.getElementsByTagName('md-tab-item'); + elements.dummies = elements.canvas.getElementsByTagName('md-dummy-tab'); + elements.inkBar = elements.wrapper.getElementsByTagName('md-ink-bar')[0]; + return elements; + } - function remove(tab, noReselect) { - if (!tabsList.contains(tab)) return; - if (noReselect) return; - var isSelectedItem = getSelectedItem() === tab, - newTab = previous() || next(); + function keydown (event) { + switch (event.keyCode) { + case $mdConstant.KEY_CODE.LEFT_ARROW: + incrementSelectedIndex(-1, true); + break; + case $mdConstant.KEY_CODE.RIGHT_ARROW: + incrementSelectedIndex(1, true); + break; + case $mdConstant.KEY_CODE.SPACE: + case $mdConstant.KEY_CODE.ENTER: + event.preventDefault(); + $scope.selectedIndex = ctrl.focusIndex; + break; + } + ctrl.lastClick = false; + } - deselect(tab); - tabsList.remove(tab); - tab.onRemove(); + function incrementSelectedIndex (inc, focus) { + var newIndex, + index = focus ? ctrl.focusIndex : $scope.selectedIndex; + event.preventDefault(); + for (newIndex = index + inc; + ctrl.tabs[newIndex] && ctrl.tabs[newIndex].scope.disabled; + newIndex += inc) {} + if (ctrl.tabs[newIndex]) { + if (focus) ctrl.focusIndex = newIndex; + else $scope.selectedIndex = newIndex; + } + } - $scope.$broadcast('$mdTabsChanged'); + function handleOffsetChange (left) { + angular.element(elements.wrapper).css('left', '-' + left + 'px'); + } - if (isSelectedItem) { select(newTab); } - } + function handleFocusIndexChange (newIndex, oldIndex) { + if (newIndex === oldIndex) return; + if (!elements.tabs[newIndex]) return; + adjustOffset(); + redirectFocus(); + } - // Move a tab (used when ng-repeat order changes) - function move(tab, toIndex) { - var isSelected = getSelectedItem() === tab; + function redirectFocus () { + elements.dummies[ctrl.focusIndex].focus(); + } - tabsList.remove(tab); - tabsList.add(tab, toIndex); - if (isSelected) select(tab); + function adjustOffset () { + var tab = elements.tabs[ctrl.focusIndex], + left = tab.offsetLeft, + right = tab.offsetWidth + left; + ctrl.offsetLeft = Math.max(ctrl.offsetLeft, fixOffset(right - elements.canvas.clientWidth)); + ctrl.offsetLeft = Math.min(ctrl.offsetLeft, fixOffset(left)); + } - $scope.$broadcast('$mdTabsChanged'); - } + function handleWindowResize () { + ctrl.lastSelectedIndex = $scope.selectedIndex; + updateInkBarStyles(); + } - function select(tab, rightToLeft) { - if (!tab || tab.isSelected || tab.isDisabled()) return; - if (!tabsList.contains(tab)) return; + function insertTab (tabData, index) { + var proto = { + getIndex: function () { return ctrl.tabs.indexOf(tab); }, + isActive: function () { return this.getIndex() === $scope.selectedIndex; }, + isLeft: function () { return this.getIndex() < $scope.selectedIndex; }, + isRight: function () { return this.getIndex() > $scope.selectedIndex; }, + hasFocus: function () { return !ctrl.lastClick && ctrl.hasFocus && this.getIndex() === ctrl.focusIndex; }, + id: $mdUtil.nextUid() + }, + tab = angular.extend(proto, tabData); + if (!angular.isString(tabData.template)) { + ctrl.hasContent = false; + } + if (angular.isDefined(index)) { + ctrl.tabs.splice(index, 0, tab); + } else { + ctrl.tabs.push(tab); + } + return tab; + } - if (!angular.isDefined(rightToLeft)) { - rightToLeft = indexOf(tab) < $scope.selectedIndex; + function removeTab (tabData) { + ctrl.tabs.splice(tabData.getIndex(), 1); + refreshIndex(); + $timeout(updateInkBarStyles); } - deselect(getSelectedItem(), rightToLeft); - $scope.selectedIndex = indexOf(tab); - tab.isSelected = true; - tab.onSelect(rightToLeft); + function refreshIndex () { + $scope.selectedIndex = getNearestSafeIndex($scope.selectedIndex); + ctrl.focusIndex = getNearestSafeIndex(ctrl.focusIndex); + } - $scope.$broadcast('$mdTabsChanged'); - } + function handleSelectedIndexChange (newValue, oldValue) { + if (newValue === oldValue) return; + $scope.selectedIndex = getNearestSafeIndex(newValue); + ctrl.lastSelectedIndex = oldValue; + updateInkBarStyles(); + } - function focus(tab) { - // this variable is watched by pagination - self.tabToFocus = tab; - } + function updateInkBarStyles () { + if (!ctrl.tabs.length) return; + var index = $scope.selectedIndex, + totalWidth = elements.wrapper.offsetWidth, + tab = elements.tabs[index], + left = tab.offsetLeft, + right = totalWidth - left - tab.offsetWidth; + updateInkBarClassName(); + angular.element(elements.inkBar).css({ left: left + 'px', right: right + 'px' }); - function deselect(tab, rightToLeft) { - if (!tab || !tab.isSelected) return; - if (!tabsList.contains(tab)) return; + } - $scope.selectedIndex = -1; - tab.isSelected = false; - tab.onDeselect(rightToLeft); - } + function updateInkBarClassName () { + var newIndex = $scope.selectedIndex, + oldIndex = ctrl.lastSelectedIndex, + ink = angular.element(elements.inkBar); + ink.removeClass('md-left md-right'); + if (!angular.isNumber(oldIndex)) return; + if (newIndex < oldIndex) { + ink.addClass('md-left'); + } else if (newIndex > oldIndex) { + ink.addClass('md-right'); + } + } - function next(tab, filterFn) { - return tabsList.next(tab || getSelectedItem(), filterFn || isTabEnabled); - } - function previous(tab, filterFn) { - return tabsList.previous(tab || getSelectedItem(), filterFn || isTabEnabled); - } + function getNearestSafeIndex(newIndex) { + var maxOffset = Math.max(ctrl.tabs.length - newIndex, newIndex), + i, tab; + for (i = 0; i <= maxOffset; i++) { + tab = ctrl.tabs[newIndex + i]; + if (tab && (tab.scope.disabled !== true)) return tab.getIndex(); + tab = ctrl.tabs[newIndex - i]; + if (tab && (tab.scope.disabled !== true)) return tab.getIndex(); + } + return newIndex; + } - function isTabEnabled(tab) { - return tab && !tab.isDisabled(); - } + function shouldStretchTabs () { + switch ($scope.stretchTabs) { + case 'always': return true; + case 'never': return false; + default: return !shouldPaginate() && $window.matchMedia('(max-width: 600px)').matches; + } + } + + function shouldPaginate () { + var canvasWidth = $element.prop('clientWidth'); + angular.forEach(elements.tabs, function (tab) { + canvasWidth -= tab.offsetWidth; + }); + return canvasWidth <= 0; + } + + function select (index) { + ctrl.focusIndex = $scope.selectedIndex = index; + ctrl.lastClick = true; + } -} -})(); + function scroll (event) { + if (!shouldPaginate()) return; + event.preventDefault(); + ctrl.offsetLeft = fixOffset(ctrl.offsetLeft - event.wheelDelta); + } + + function fixOffset (value) { + var lastTab = elements.tabs[elements.tabs.length - 1], + totalWidth = lastTab.offsetLeft + lastTab.offsetWidth; + value = Math.max(0, value); + value = Math.min(totalWidth - elements.canvas.clientWidth, value); + return value; + } + + function nextPage () { + var viewportWidth = elements.canvas.clientWidth, + totalWidth = viewportWidth + ctrl.offsetLeft, + i, tab; + for (i = 0; i < elements.tabs.length; i++) { + tab = elements.tabs[i]; + if (tab.offsetLeft + tab.offsetWidth > totalWidth) break; + } + ctrl.offsetLeft = fixOffset(tab.offsetLeft); + } + + function previousPage () { + var i, tab; + for (i = 0; i < elements.tabs.length; i++) { + tab = elements.tabs[i]; + if (tab.offsetLeft + tab.offsetWidth >= ctrl.offsetLeft) break; + } + ctrl.offsetLeft = fixOffset(tab.offsetLeft + tab.offsetWidth - elements.canvas.clientWidth); + } + + function canPageBack () { + return ctrl.offsetLeft > 0; + } + + function canPageForward () { + var lastTab = elements.tabs[elements.tabs.length - 1]; + return lastTab.offsetLeft + lastTab.offsetWidth > elements.canvas.clientWidth + ctrl.offsetLeft; + } + + function attachRipple (scope, element) { + var options = { colorElement: angular.element(elements.inkBar) }; + $mdInkRipple.attachTabBehavior(scope, element, options); + } + } +})(); \ No newline at end of file diff --git a/src/components/tabs/js/tabsDirective.js b/src/components/tabs/js/tabsDirective.js index 2799b7d1371..f10f827b864 100644 --- a/src/components/tabs/js/tabsDirective.js +++ b/src/components/tabs/js/tabsDirective.js @@ -1,9 +1,3 @@ -(function() { -'use strict'; - -angular.module('material.components.tabs') - .directive('mdTabs', TabsDirective); - /** * @ngdoc directive * @name mdTabs @@ -36,14 +30,10 @@ angular.module('material.components.tabs') * **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 **``** tags) will be transcluded into the tab header area BEFORE the tab buttons. * * ### Explanation of tab stretching * @@ -71,101 +61,131 @@ angular.module('material.components.tabs') * * * - * * - * - * - * {{tab.title}} - * - * - * + * ng-repeat="tab in tabs | orderBy:predicate:reversed" + * md-on-select="onTabSelected(tab)" + * md-on-deselect="announceDeselected(tab)" + * ng-disabled="tab.disabled"> + * + * {{tab.title}} + * + * + * * {{tab.content}} - * + * * - * * * * */ -function TabsDirective($mdTheming) { - return { - restrict: 'E', - controller: '$mdTabs', - require: 'mdTabs', - transclude: true, - scope: { - selectedIndex: '=?mdSelected' - }, - template: - '
' + - - '' + - - // overflow: hidden container when paginating - '
' + - // flex container for elements - '
' + - '' + - '
' + - '
' + - - '' + - - '
' + - '
', - 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 () { + 'use strict'; - function watchSelected() { - scope.$watch('selectedIndex', function watchSelectedIndex(newIndex, oldIndex) { - if (oldIndex == newIndex) return; - var rightToLeft = oldIndex > newIndex; - tabsCtrl.deselect(tabsCtrl.itemAt(oldIndex), rightToLeft); + angular + .module('material.components.tabs') + .directive('mdTabs', MdTabs); - 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); - } - }); - } + function MdTabs ($mdTheming) { + return { + scope: { + selectedIndex: '=?mdSelected', + stretchTabs: '@?mdStretchTabs' + }, + transclude: true, + template: '\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
\ + \ +
\ + \ +
\ + \ + \ + \ + ', + controller: 'MdTabsController', + controllerAs: '$mdTabsCtrl', + link: function (scope, element, attr) { + //-- watch attributes + attr.$observe('mdNoBar', function (value) { scope.noInkBar = angular.isDefined(value); }); + //-- set default value for selectedIndex + scope.selectedIndex = angular.isNumber(scope.selectedIndex) ? scope.selectedIndex : 0; + //-- apply themes + $mdTheming(element); + } + }; } -} -})(); +})(); \ No newline at end of file diff --git a/src/components/tabs/tabs-theme.scss b/src/components/tabs/tabs-theme.scss index 329450417c5..69e3b8158c8 100644 --- a/src/components/tabs/tabs-theme.scss +++ b/src/components/tabs/tabs-theme.scss @@ -1,84 +1,82 @@ md-tabs.md-THEME_NAME-theme { - .md-header { + md-tab-wrapper { background-color: transparent; + border-color: '{{foreground-4}}'; } .md-paginator md-icon { color: '{{primary-color}}'; } + md-ink-bar { + color: '{{accent-color}}'; + background: '{{accent-color}}'; + } + + .md-tab { + color: '{{foreground-2}}'; + &[disabled] { + color: '{{foreground-3}}'; + } + &.md-active, &.md-focus { + color: '{{primary-color}}'; + } + &.md-focus { + background: '{{primary-color-0.1}}'; + } + .md-ripple-container { + color: '{{accent-100}}'; + } + } + &.md-accent { - .md-header { + md-tab-wrapper { background-color: '{{accent-color}}'; } md-tab:not([disabled]) { color: '{{accent-100}}'; - &.active { + &.md-active, &.md-focus { color: '{{accent-contrast}}'; } + &.md-focus { + background: '{{accent-contrast-0.1}}'; + } + } + + md-ink-bar { + color: '{{primary-600-1}}'; + background: '{{primary-600-1}}'; } } &.md-primary { - .md-header { + md-tab-wrapper { background-color: '{{primary-color}}'; } md-tab:not([disabled]) { color: '{{primary-100}}'; - &.active { + &.md-active, &.md-focus { color: '{{primary-contrast}}'; } - } - md-tab { - color: '{{primary-100}}'; - &[disabled] { - color: '{{foreground-3}}'; - } - &:focus { - color: '{{primary-contrast}}'; - background-color: '{{primary-contrast-0.1}}'; - } - &.active { - color: '{{primary-contrast}}'; - } - .md-ripple-container { - color: '{{primary-contrast}}'; + &.md-focus { + background: '{{primary-contrast-0.1}}'; } } } &.md-warn { - .md-header { + md-tab-wrapper { background-color: '{{warn-color}}'; } md-tab:not([disabled]) { color: '{{warn-100}}'; - &.active { + &.md-active, &.md-focus { color: '{{warn-contrast}}'; } - } - } - - - md-tabs-ink-bar { - color: '{{accent-color}}'; - background: '{{accent-color}}'; - } - - md-tab { - color: '{{foreground-2}}'; - &[disabled] { - color: '{{foreground-3}}'; - } - &:focus { - color: '{{foreground-1}}'; - } - &.active { - color: '{{primary-color}}'; - } - .md-ripple-container { - color: '{{accent-100}}'; + &.md-focus { + background: '{{warn-contrast-0.1}}'; + } } } } diff --git a/src/components/tabs/tabs.scss b/src/components/tabs/tabs.scss index 26a72b25e3d..d71da69e2fb 100644 --- a/src/components/tabs/tabs.scss +++ b/src/components/tabs/tabs.scss @@ -1,175 +1,207 @@ -// 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; - overflow: auto; -} - -.md-header { - width: 100%; - height: $tabs-header-height; - box-sizing: border-box; +md-tab-data { 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; - - md-icon { - transform: rotate(180deg); - } - - } -} - -/* 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; + top: 0; 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; - } + bottom: 0; + z-index: -1; + opacity: 0; } - -.md-tabs-content { +md-tabs { + display: block; + margin: 0; + border-radius: 2px; overflow: hidden; - width: 100%; - position: relative; - .md-tab-content { - height: 100%; - &.ng-hide { - &.ng-animate { - display: block !important; + &[md-align-tabs="bottom"] { + padding-bottom: $tabs-header-height; + position: relative; + md-tab-wrapper { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: $tabs-header-height; + } + } + &[md-border-bottom] { + md-tab-wrapper { + border-width: 1px; + border-style: solid; + } + } + md-tab-wrapper { + display: block; + position: relative; + md-ink-bar { + position: absolute; + left: auto; + right: auto; + bottom: 0; + height: 2px; + &.md-left { + transition: left ($swift-ease-in-out-duration * 0.45) $swift-ease-in-out-timing-function, + right $swift-ease-in-out-duration $swift-ease-in-out-timing-function; + } + &.md-right { + transition: left $swift-ease-in-out-duration $swift-ease-in-out-timing-function, + right ($swift-ease-in-out-duration * 0.45) $swift-ease-in-out-timing-function; } } - &.ng-animate { - transition: transform $swift-ease-in-out-duration $swift-ease-in-out-timing-function; + md-tab { + padding: 12px 24px 14px; + position: absolute; + z-index: -1; + white-space: nowrap; + box-shadow: none; + border: none; + left: -9999px; + text-transform: uppercase; + font-size: 14px; + &:after { + content: attr(label); + } + } + .md-tab { + font-size: 14px; + text-align: center; + line-height: $tabs-header-height - 24; + padding: 12px 24px; + transition: background-color 0.35s $swift-ease-in-out-timing-function; + cursor: pointer; + white-space: nowrap; + position: relative; + text-transform: uppercase; + float: left; + font-weight: 500; + &.md-focus { + box-shadow: none; + outline: none; + } + &.md-active { + cursor: default; + } + &.md-disabled { + pointer-events: none; + touch-action: pan-y; + user-select: none; + -webkit-user-drag: none; + opacity: 0.5; + cursor: default; + } + &.ng-leave { + transition: none; + } + } + md-prev-button, + md-next-button { + height: 100%; + width: $tabs-paginator-width; + position: absolute; + top: 50%; + transform: translateY(-50%); + line-height: 1em; + z-index: 2; + cursor: pointer; + font-size: 16px; + background: transparent no-repeat center center; + transition: $swift-ease-in-out; + md-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate3d(-50%, -50%, 0); + } + &.md-disabled { + opacity: 0.25; + cursor: default; + } + &.ng-leave { + transition: none; + } + } + md-prev-button { + left: 0; + background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE3LjEuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPiA8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPiA8c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIyNHB4IiBoZWlnaHQ9IjI0cHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMjQgMjQiIHhtbDpzcGFjZT0icHJlc2VydmUiPiA8ZyBpZD0iSGVhZGVyIj4gPGc+IDxyZWN0IHg9Ii02MTgiIHk9Ii0xMjA4IiBmaWxsPSJub25lIiB3aWR0aD0iMTQwMCIgaGVpZ2h0PSIzNjAwIi8+IDwvZz4gPC9nPiA8ZyBpZD0iTGFiZWwiPiA8L2c+IDxnIGlkPSJJY29uIj4gPGc+IDxwb2x5Z29uIHBvaW50cz0iMTUuNCw3LjQgMTQsNiA4LDEyIDE0LDE4IDE1LjQsMTYuNiAxMC44LDEyIAkJIiBzdHlsZT0iZmlsbDp3aGl0ZTsiLz4gPHJlY3QgZmlsbD0ibm9uZSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ii8+IDwvZz4gPC9nPiA8ZyBpZD0iR3JpZCIgZGlzcGxheT0ibm9uZSI+IDxnIGRpc3BsYXk9ImlubGluZSI+IDwvZz4gPC9nPiA8L3N2Zz4NCg=='); + } + md-next-button { + right: 0; + background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE3LjEuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPiA8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPiA8c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIyNHB4IiBoZWlnaHQ9IjI0cHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMjQgMjQiIHhtbDpzcGFjZT0icHJlc2VydmUiPiA8ZyBpZD0iSGVhZGVyIj4gPGc+IDxyZWN0IHg9Ii02MTgiIHk9Ii0xMzM2IiBmaWxsPSJub25lIiB3aWR0aD0iMTQwMCIgaGVpZ2h0PSIzNjAwIi8+IDwvZz4gPC9nPiA8ZyBpZD0iTGFiZWwiPiA8L2c+IDxnIGlkPSJJY29uIj4gPGc+IDxwb2x5Z29uIHBvaW50cz0iMTAsNiA4LjYsNy40IDEzLjIsMTIgOC42LDE2LjYgMTAsMTggMTYsMTIgCQkiIHN0eWxlPSJmaWxsOndoaXRlOyIvPiA8cmVjdCBmaWxsPSJub25lIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiLz4gPC9nPiA8L2c+IDxnIGlkPSJHcmlkIiBkaXNwbGF5PSJub25lIj4gPGcgZGlzcGxheT0iaW5saW5lIj4gPC9nPiA8L2c+IDwvc3ZnPg0K'); + md-icon { + transform: translate3d(-50%, -50%, 0) rotate(180deg); + } + } + md-tab-canvas { + @include pie-clearfix; + position: relative; + overflow: hidden; + display: block; + &.md-paginated { + margin: 0 $tabs-paginator-width; + } + } + md-pagination-wrapper { + @include pie-clearfix; + display: block; + transition: left $swift-ease-in-out-duration $swift-ease-in-out-timing-function; + position: relative; + width: 999999px; + } + &.md-stretch-tabs { + md-pagination-wrapper { + width: 100%; + display: flex; + flex-direction: row; + md-tab-item { + flex: 1; + } + } + } + } + md-tab-content-wrapper { + display: block; + min-height: 200px; + position: relative; + overflow: hidden; + md-tab-content { + display: block; + position: absolute; + top: 0; left: 0; right: 0; bottom: 0; transform: translateX(0); - &.ng-hide-add { + text-align: center; + transition: transform $swift-ease-in-out-duration $swift-ease-in-out-timing-function; + &.ng-leave, &.md-no-transition { + transition: none; + } + &.md-left { transform: translateX(-100%); - &.md-transition-rtl { - transform: translateX(100%); + animation: 2 * $swift-ease-in-out-duration md-tab-content-hide; + opacity: 0; + * { + transition: visibility 0s linear; + transition-delay: $swift-ease-in-out-duration; + visibility: hidden; } } - &.ng-hide-remove { - position: absolute; + &.md-right { transform: translateX(100%); - top: 0; - left: 0; - right: 0; - bottom: 0; - &.md-transition-rtl { - transform: translateX(-100%); - } - &.ng-hide-remove-active { - transform: translateX(0); + animation: 2 * $swift-ease-in-out-duration md-tab-content-hide; + opacity: 0; + * { + transition: visibility 0s linear; + transition-delay: $swift-ease-in-out-duration; + visibility: hidden; } } } } } -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: none; - &.md-tab-themed { - 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; - } +@keyframes md-tab-content-hide { + 0% { opacity: 1; } + 50% { opacity: 1; } + 100% { opacity: 0; } } diff --git a/src/components/tabs/tabs.spec.js b/src/components/tabs/tabs.spec.js index 232fd39047f..88b16ffaaae 100644 --- a/src/components/tabs/tabs.spec.js +++ b/src/components/tabs/tabs.spec.js @@ -11,15 +11,15 @@ describe('', function() { return 'Expected ' + angular.mock.dump(actual) + (this.isNot ? ' not ' : ' ') + 'to be the active tab. Failures: ' + fails.join(', '); }; - - if (!actual.hasClass('active')) { + if (!actual.length) { + fails.push('element not found'); + return; + } + if (!actual.hasClass('md-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; } @@ -45,28 +45,27 @@ describe('', function() { describe('activating tabs', function() { it('should select first tab by default', function() { - var tabs = setup('' + - '' + - '' + - ''); - expect(tabs.find('md-tab').eq(0)).toBeActiveTab(); + var tabs = setup('\ + a\ + b\ + '); + expect(tabs.find('md-tab-item').eq(0)).toBeActiveTab(); }); - it('should select & focus tab on click', inject(function($document) { + it('should select & focus tab on click', inject(function($document, $rootScope) { var tabs = setup('' + '' + '' + '' + ''); - var tabItems = tabs.find('md-tab'); + var tabItems = tabs.find('md-tab-item'); - tabs.find('md-tab').eq(1).triggerHandler('click'); + tabs.find('md-tab-item').eq(1).triggerHandler('click'); + $rootScope.$apply(); expect(tabItems.eq(1)).toBeActiveTab(); - expect($document.activeElement).toBe(tabItems[1]); - - tabs.find('md-tab').eq(0).triggerHandler('click'); + + tabs.find('md-tab-item').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) { @@ -75,30 +74,32 @@ describe('', function() { '' + '' + ''); - var tabItems = tabs.find('md-tab'); + var tabItems = tabs.find('md-tab-item'); + expect(tabItems.eq(0)).toBeActiveTab(); // Boundary case, do nothing - triggerKeydown(tabItems.eq(0), $mdConstant.KEY_CODE.LEFT_ARROW); + triggerKeydown(tabs.find('md-dummy-tab').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); + triggerKeydown(tabs.find('md-dummy-tab').eq(0), $mdConstant.KEY_CODE.RIGHT_ARROW); expect(tabItems.eq(0)).toBeActiveTab(); - $timeout.flush(); - expect($document.activeElement).toBe(tabItems[2]); + + triggerKeydown(tabs.find('md-dummy-tab').eq(2), $mdConstant.KEY_CODE.ENTER); + expect(tabItems.eq(2)).toBeActiveTab(); // 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]); + triggerKeydown(tabs.find('md-dummy-tab').eq(0), $mdConstant.KEY_CODE.RIGHT_ARROW); + expect(tabItems.eq(2)).toBeActiveTab(); + + triggerKeydown(tabs.find('md-dummy-tab').eq(2), $mdConstant.KEY_CODE.ENTER); + expect(tabItems.eq(2)).toBeActiveTab(); // Skip tab 1 again, it's disabled - triggerKeydown(tabItems.eq(2), $mdConstant.KEY_CODE.LEFT_ARROW); + triggerKeydown(tabs.find('md-dummy-tab').eq(2), $mdConstant.KEY_CODE.LEFT_ARROW); + triggerKeydown(tabs.find('md-dummy-tab').eq(0), $mdConstant.KEY_CODE.ENTER); expect(tabItems.eq(0)).toBeActiveTab(); - $timeout.flush(); - expect($document.activeElement).toBe(tabItems[0]); })); @@ -107,39 +108,23 @@ describe('', function() { '' + '' + ''); - var tabItems = tabs.find('md-tab'); + var tabItems = tabs.find('md-tab-item'); + tabs.find('md-tab-item').eq(0).triggerHandler('click'); - triggerKeydown(tabItems.eq(1), $mdConstant.KEY_CODE.ENTER); + triggerKeydown(tabs.find('md-dummy-tab').eq(1), $mdConstant.KEY_CODE.ENTER); expect(tabItems.eq(1)).toBeActiveTab(); - triggerKeydown(tabItems.eq(0), $mdConstant.KEY_CODE.SPACE); + triggerKeydown(tabs.find('md-dummy-tab').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('' + - 'content1!' + - 'content2!' + - ''); - 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('' + '' + '' + '' + ''); - var tabItems = tabs.find('md-tab'); + var tabItems = tabs.find('md-tab-item'); expect(tabItems.eq(0)).toBeActiveTab(); expect(tabs.scope().current).toBe(0); @@ -151,28 +136,12 @@ describe('', function() { expect(tabs.scope().current).toBe(2); }); - it('should use active binding', function() { - var tabs = setup('' + - '' + - '' + - '' + - ''); - 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('' + '' + '' + ''); - var tabItems = tabs.find('md-tab'); + var tabItems = tabs.find('md-tab-item'); expect(tabItems.eq(0)).toBeActiveTab(); @@ -194,19 +163,31 @@ describe('', function() { ''); var tabItems = tabs.find('md-tab'); - tabItems.eq(0).triggerHandler('$md.swipeleft'); + return; //-- TODO: Wire up swipe logic + + tabItems.eq(0).isolateScope().onSwipe({ + type: 'swipeleft' + }); expect(tabItems.eq(1)).toBeActiveTab(); - tabItems.eq(1).triggerHandler('$md.swipeleft'); + tabItems.eq(1).isolateScope().onSwipe({ + type: 'swipeleft' + }); expect(tabItems.eq(1)).toBeActiveTab(); - tabItems.eq(1).triggerHandler('$md.swipeleft'); + tabItems.eq(1).isolateScope().onSwipe({ + type: 'swipeleft' + }); expect(tabItems.eq(1)).toBeActiveTab(); - tabItems.eq(1).triggerHandler('$md.swiperight'); + tabItems.eq(1).isolateScope().onSwipe({ + type: 'swiperight' + }); expect(tabItems.eq(0)).toBeActiveTab(); - tabItems.eq(0).triggerHandler('$md.swiperight'); + tabItems.eq(0).isolateScope().onSwipe({ + type: 'swiperight' + }); expect(tabItems.eq(0)).toBeActiveTab(); }); @@ -214,45 +195,35 @@ describe('', function() { describe('tab label & content DOM', function() { - it('should support all 3 label types', function() { + it('should support both label types', function() { var tabs1 = setup('' + - '' + + '' + ''); - expect(tabs1.find('md-tab-label').html()).toBe('super label'); + expect(tabs1.find('md-tab-item').text()).toBe('super label'); var tabs2 = setup('' + - 'super label' + - ''); - expect(tabs2.find('md-tab-label').html()).toBe('super label'); - - var tabs3 = setup('' + 'super label' + ''); - expect(tabs3.find('md-tab-label').html()).toBe('super label'); + expect(tabs2.find('md-tab-item').text()).toBe('super label'); + }); it('should support content inside with each kind of label', function() { var tabs1 = setup('' + 'content that!' + ''); - expect(tabs1.find('md-tab-label').html()).toBe('label that!'); - expect(tabs1[0].querySelector('.md-tabs-content .md-tab-content').innerHTML) - .toBe('content that!'); - - var tabs2 = setup('' + - 'label that!content that!' + - ''); - expect(tabs1.find('md-tab-label').html()).toBe('label that!'); - expect(tabs1[0].querySelector('.md-tabs-content .md-tab-content').innerHTML) - .toBe('content that!'); - }); - - it('should connect content with child of the outside scope', function() { - var tabs = setup('' + - 'content!' + - ''); - var content = angular.element(tabs[0].querySelector('.md-tab-content')); - expect(content.scope().$parent.$id).toBe(tabs.find('md-tab').scope().$id); + expect(tabs1.find('md-tab-item').text()).toBe('label that!'); + expect(tabs1[0].querySelector('md-tab-content').textContent).toBe('content that!'); + + var tabs2 = setup('\ + \ + label that!\ + content that!\ + \ + '); + expect(tabs1.find('md-tab-item').text()).toBe('label that!'); + expect(tabs1[0].querySelector('md-tab-content').textContent) + .toBe('content that!'); }); }); @@ -263,8 +234,8 @@ describe('', function() { var tabs = setup('' + 'content!' + ''); - var tabItem = tabs.find('md-tab'); - var tabContent = angular.element(tabs[0].querySelector('.md-tab-content')); + var tabItem = tabs.find('md-tab-item'); + var tabContent = angular.element(tabs[0].querySelector('md-tab-content')); expect(tabs.attr('role')).toBe('tablist'); diff --git a/src/core/services/ripple/ripple.js b/src/core/services/ripple/ripple.js index 7ea6dc4400a..2fe11e750e5 100644 --- a/src/core/services/ripple/ripple.js +++ b/src/core/services/ripple/ripple.js @@ -83,7 +83,7 @@ function InkRippleService($window, $timeout) { isHeld = false, node = element[0], rippleSizeSetting = element.attr('md-ripple-size'), - color = parseColor(element.attr('md-ink-ripple')) || parseColor($window.getComputedStyle(options.colorElement[0]).color || 'rgb(0, 0, 0)'); + color = parseColor(element.attr('md-ink-ripple')) || parseColor(options.colorElement.length && $window.getComputedStyle(options.colorElement[0]).color || 'rgb(0, 0, 0)'); switch (rippleSizeSetting) { case 'full': diff --git a/src/core/style/mixins.scss b/src/core/style/mixins.scss index e01ec99cb3d..f240fdea109 100644 --- a/src/core/style/mixins.scss +++ b/src/core/style/mixins.scss @@ -17,3 +17,11 @@ color: $color; } } + +@mixin pie-clearfix { + &:after { + content: ''; + display: table; + clear: both; + } +}