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..10d5ba7d9c3 --- /dev/null +++ b/src/components/tabs/js/tabDirective.js @@ -0,0 +1,100 @@ +(function () { + 'use strict'; + + angular + .module('material.components.tabs') + .directive('mdTab', MdTab); + + /** + * @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 MdTab () { + return { + require: '^mdTabs', + terminal: true, + scope: { + label: '@', + active: '=?mdActive', + disabled: '=?ngDisabled' + }, + link: link + }; + + function link (scope, element, attr, tabsCtrl) { + var tabs = element.parent()[0].getElementsByTagName('md-tab'), + index = Array.prototype.indexOf.call(tabs, element[0]), + data = tabsCtrl.insertTab({ + scope: scope, + parent: scope.$parent, + index: index, + template: getTemplate(), + label: getLabel() + }, index); + + scope.$watch('active', function (active) { if (active) tabsCtrl.select(data.getIndex()); }); + scope.$watch('disabled', function () { tabsCtrl.refreshIndex(); }); + scope.$on('$destroy', function () { tabsCtrl.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..9d87403eccd 100644 --- a/src/components/tabs/js/tabsController.js +++ b/src/components/tabs/js/tabsController.js @@ -1,138 +1,252 @@ -(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'; + + angular + .module('material.components.tabs') + .controller('MdTabsController', MdTabsController); + + function MdTabsController ($scope, $element, $window, $timeout, $mdConstant, $mdInkRipple, $mdUtil) { + var ctrl = this, + elements = { + tabs: $element[0].getElementsByTagName('md-tab-item'), + dummies: $element[0].getElementsByTagName('md-dummy-tab'), + canvas: $element[0].getElementsByTagName('md-tab-canvas')[0], + inkBar: $element[0].getElementsByTagName('md-ink-bar')[0], + wrapper: $element[0].getElementsByTagName('md-pagination-wrapper')[0] + }; + + 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; + + 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; + + 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); + } - function getSelectedIndex() { - return $scope.selectedIndex; - } + function keydown (event) { + var newIndex; + switch (event.keyCode) { + case $mdConstant.KEY_CODE.LEFT_ARROW: + handleArrowKey(-1); + break; + case $mdConstant.KEY_CODE.RIGHT_ARROW: + handleArrowKey(1); + break; + case $mdConstant.KEY_CODE.SPACE: + case $mdConstant.KEY_CODE.ENTER: + event.preventDefault(); + $scope.selectedIndex = ctrl.focusIndex; + break; + } + ctrl.lastClick = false; + function handleArrowKey (inc) { + event.preventDefault(); + for (newIndex = ctrl.focusIndex + inc; + ctrl.tabs[newIndex] && ctrl.tabs[newIndex].scope.disabled; + newIndex += inc) {} + if (ctrl.tabs[newIndex]) ctrl.focusIndex = newIndex; + } + } - // Add a new tab. - // Returns a method to remove the tab from the list. - function add(tab, index) { - tabsList.add(tab, index); + function handleOffsetChange (left) { + angular.element(elements.wrapper).css('left', '-' + left + 'px'); + } - // 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); + function handleFocusIndexChange (newIndex, oldIndex) { + if (newIndex === oldIndex) return; + if (!elements.tabs[newIndex]) return; + adjustOffset(); + redirectFocus(); } - $scope.$broadcast('$mdTabsChanged'); - } + function redirectFocus () { + elements.dummies[ctrl.focusIndex].focus(); + } - function remove(tab, noReselect) { - if (!tabsList.contains(tab)) return; - if (noReselect) return; - var isSelectedItem = getSelectedItem() === tab, - newTab = previous() || next(); + 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)); + } - deselect(tab); - tabsList.remove(tab); - tab.onRemove(); + function handleWindowResize () { + ctrl.lastSelectedIndex = $scope.selectedIndex; + updateInkBarStyles(); + } - $scope.$broadcast('$mdTabsChanged'); + 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 (isSelectedItem) { select(newTab); } - } + function removeTab (tabData) { + ctrl.tabs.splice(tabData.getIndex(), 1); + refreshIndex(); + $timeout(updateInkBarStyles); + } - // Move a tab (used when ng-repeat order changes) - function move(tab, toIndex) { - var isSelected = getSelectedItem() === tab; + function refreshIndex () { + $scope.selectedIndex = getNearestSafeIndex($scope.selectedIndex); + ctrl.focusIndex = getNearestSafeIndex(ctrl.focusIndex); + } - tabsList.remove(tab); - tabsList.add(tab, toIndex); - if (isSelected) select(tab); + function handleSelectedIndexChange (newValue, oldValue) { + if (newValue === oldValue) return; + $scope.selectedIndex = getNearestSafeIndex(newValue); + ctrl.lastSelectedIndex = oldValue; + updateInkBarStyles(); + elements.canvas.focus(); + } - $scope.$broadcast('$mdTabsChanged'); - } + 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 select(tab, rightToLeft) { - if (!tab || tab.isSelected || tab.isDisabled()) return; - if (!tabsList.contains(tab)) return; + } - if (!angular.isDefined(rightToLeft)) { - rightToLeft = indexOf(tab) < $scope.selectedIndex; + 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'); + } } - deselect(getSelectedItem(), rightToLeft); - $scope.selectedIndex = indexOf(tab); - tab.isSelected = true; - tab.onSelect(rightToLeft); + 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; + } - $scope.$broadcast('$mdTabsChanged'); - } + function shouldStretchTabs () { + switch ($scope.stretchTabs) { + case 'always': return true; + case 'never': return false; + default: return !shouldPaginate() && $window.matchMedia('(max-width: 600px)').matches; + } + } - function focus(tab) { - // this variable is watched by pagination - self.tabToFocus = tab; - } + function shouldPaginate () { + var canvasWidth = $element.prop('clientWidth'); + angular.forEach(elements.tabs, function (tab) { + canvasWidth -= tab.offsetWidth; + }); + return canvasWidth < 0; + } - function deselect(tab, rightToLeft) { - if (!tab || !tab.isSelected) return; - if (!tabsList.contains(tab)) return; + function select (index) { + ctrl.focusIndex = $scope.selectedIndex = index; + ctrl.lastClick = true; + } - $scope.selectedIndex = -1; - tab.isSelected = false; - tab.onDeselect(rightToLeft); - } + function scroll (event) { + if (!shouldPaginate()) return; + event.preventDefault(); + ctrl.offsetLeft = fixOffset(ctrl.offsetLeft - event.wheelDelta); + } - function next(tab, filterFn) { - return tabsList.next(tab || getSelectedItem(), filterFn || isTabEnabled); - } - function previous(tab, filterFn) { - return tabsList.previous(tab || getSelectedItem(), filterFn || isTabEnabled); - } + 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 isTabEnabled(tab) { - return tab && !tab.isDisabled(); - } + 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..95daeeaf3e1 100644 --- a/src/components/tabs/js/tabsDirective.js +++ b/src/components/tabs/js/tabsDirective.js @@ -1,171 +1,98 @@ -(function() { -'use strict'; +(function () { + 'use strict'; -angular.module('material.components.tabs') - .directive('mdTabs', TabsDirective); + angular + .module('material.components.tabs') + .directive('mdTabs', MdTabs); -/** - * @ngdoc directive - * @name mdTabs - * @module material.components.tabs - * - * @restrict E - * - * @description - * The `` directive serves as the container for 1..n `` child directives to produces a Tabs components. - * In turn, the nested `` 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: - * - * - * - * - * - * - * - * - * - * 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 **``** 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 - * - * - * - * - * - * - * - * {{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 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); - } - }); - } + 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; + } +}