+
+
+```
+
+### Styles
+
+```scss
+@import "@material/tab-bar/mdc-tab-bar";
+@import "@material/tab-scroller/mdc-tab-scroller";
+@import "@material/tab-indicator/mdc-tab-indicator";
+@import "@material/tab/mdc-tab";
+```
+
+### JavaScript Instantiation
+
+```js
+import {MDCTabBar} from '@material/tab-bar';
+
+const tabBar = new MDCTabBar(document.querySelector('.mdc-tab-bar'));
+```
+
+> See [Importing the JS component](../../docs/importing-js.md) for more information on how to import JavaScript.
+
+## Style Customization
+
+### CSS Classes
+
+CSS Class | Description
+--- | ---
+`mdc-tab-bar` | Mandatory.
+
+### Sass Mixins
+
+To customize the width of the tab bar, use the following mixin.
+
+Mixin | Description
+--- | ---
+`mdc-tab-bar-width($width)` | Customizes the width of the tab bar.
+
+## `MDCTabBar` Properties and Methods
+
+Method Signature | Description
+--- | ---
+`activateTab(index: number) => void` | Activates the tab at the given index.
+`scrollIntoView(index: number) => void` | Scrolls the tab at the given index into view.
+
+Event Name | Event Data Structure | Description
+--- | --- | ---
+`MDCTabBar:activated` | `{"detail": {"index": number}}` | Emitted when a Tab is activated with the index of the activated Tab. Listen for this to update content when a Tab becomes active.
+
+## Usage within Web Frameworks
+
+If you are using a JavaScript framework, such as React or Angular, you can create a Tab Bar for your framework. Depending on your needs, you can use the _Simple Approach: Wrapping MDC Web Vanilla Components_, or the _Advanced Approach: Using Foundations and Adapters_. Please follow the instructions [here](../../docs/integrating-into-frameworks.md).
+
+### `MDCTabBarAdapter`
+
+Method Signature | Description
+--- | ---
+`scrollTo(scrollX: number) => void` | Scrolls the Tab Scroller to the given position.
+`incrementScroll(scrollXIncrement: number) => void` | Increments the Tab Scroller by the given value.
+`getScrollPosition() => number` | Returns the scroll position of the Tab Scroller.
+`getScrollContentWidth() => number` | Returns the width of the Tab Scroller's scroll content element.
+`getOffsetWidth() => number` | Returns the offsetWidth of the root element.
+`isRTL() => boolean` | Returns if the text direction is RTL.
+`activateTabAtIndex(index: number, clientRect: ClientRect) => void` | Activates the Tab at the given index with the given clientRect.
+`deactivateTabAtIndex(index) => void` | Deactivates the Tab at the given index.
+`getTabIndicatorClientRectAtIndex(index: number) => ClientRect` | Returns the client rect of the Tab at the given index.
+`getTabDimensionsAtIndex(index) => MDCTabDimensions` | Returns the dimensions of the Tab at the given index.
+`getTabListLength() => number` | Returns the number of child Tab components.
+`getActiveTabIndex() => number` | Returns the index of the active Tab.
+`getIndexOfTab(tab: MDCTab) => number` | Returns the index of the given Tab instance.
+`notifyTabActivated(index: number) => void` | Emits the `MDCTabBar:activated` event.
+
+### `MDCTabBarFoundation`
+
+Method Signature | Description
+--- | ---
+`activateTab(index: number) => void` | Activates the Tab at the given index.
+`handleKeyDown(evt: Event) => void` | Handles the logic for the `"keydown"` event.
+`handleTabInteraction(evt: Event) => void` | Handles the logic for the `"MDCTab:interacted"` event.
+`scrollIntoView(index: number) => void` | Scrolls the Tab at the given index into view.
diff --git a/packages/mdc-tab-bar/_mixins.scss b/packages/mdc-tab-bar/_mixins.scss
new file mode 100644
index 00000000000..0ea61ac3562
--- /dev/null
+++ b/packages/mdc-tab-bar/_mixins.scss
@@ -0,0 +1,20 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+@mixin mdc-tab-bar-width($width) {
+ width: $width;
+}
diff --git a/packages/mdc-tab-bar/adapter.js b/packages/mdc-tab-bar/adapter.js
new file mode 100644
index 00000000000..ed9afcd0594
--- /dev/null
+++ b/packages/mdc-tab-bar/adapter.js
@@ -0,0 +1,125 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+/* eslint no-unused-vars: [2, {"args": "none"}] */
+
+/* eslint-disable no-unused-vars */
+import {MDCTabDimensions} from '@material/tab/adapter';
+import {MDCTab} from '@material/tab/index';
+/* eslint-enable no-unused-vars */
+
+/**
+ * Adapter for MDC Tab Bar.
+ *
+ * Defines the shape of the adapter expected by the foundation. Implement this
+ * adapter to integrate the Tab Bar into your framework. See
+ * https://github.com/material-components/material-components-web/blob/master/docs/authoring-components.md
+ * for more information.
+ *
+ * @record
+ */
+class MDCTabBarAdapter {
+ /**
+ * Scrolls to the given position
+ * @param {number} scrollX The position to scroll to
+ */
+ scrollTo(scrollX) {}
+
+ /**
+ * Increments the current scroll position by the given amount
+ * @param {number} scrollXIncrement The amount to increment scroll
+ */
+ incrementScroll(scrollXIncrement) {}
+
+ /**
+ * Returns the current scroll position
+ * @return {number}
+ */
+ getScrollPosition() {}
+
+ /**
+ * Returns the width of the scroll content
+ * @return {number}
+ */
+ getScrollContentWidth() {}
+
+ /**
+ * Returns the root element's offsetWidth
+ * @return {number}
+ */
+ getOffsetWidth() {}
+
+ /**
+ * Returns if the Tab Bar language direction is RTL
+ * @return {boolean}
+ */
+ isRTL() {}
+
+ /**
+ * Activates the tab at the given index with the given client rect
+ * @param {number} index The index of the tab to activate
+ * @param {!ClientRect} clientRect The client rect of the previously active Tab Indicator
+ */
+ activateTabAtIndex(index, clientRect) {}
+
+ /**
+ * Deactivates the tab at the given index
+ * @param {number} index The index of the tab to activate
+ */
+ deactivateTabAtIndex(index) {}
+
+ /**
+ * Returns the client rect of the tab's indicator
+ * @param {number} index The index of the tab
+ * @return {!ClientRect}
+ */
+ getTabIndicatorClientRectAtIndex(index) {}
+
+ /**
+ * Returns the tab dimensions of the tab at the given index
+ * @param {number} index The index of the tab
+ * @return {!MDCTabDimensions}
+ */
+ getTabDimensionsAtIndex(index) {}
+
+ /**
+ * Returns the length of the tab list
+ * @return {number}
+ */
+ getTabListLength() {}
+
+ /**
+ * Returns the index of the active tab
+ * @return {number}
+ */
+ getActiveTabIndex() {}
+
+ /**
+ * Returns the index of the given tab
+ * @param {!MDCTab} tab The tab whose index to determin
+ * @return {number}
+ */
+ getIndexOfTab(tab) {}
+
+ /**
+ * Emits the MDCTabBar:activated event
+ * @param {number} index The index of the activated tab
+ */
+ notifyTabActivated(index) {}
+}
+
+export default MDCTabBarAdapter;
diff --git a/packages/mdc-tab-bar/constants.js b/packages/mdc-tab-bar/constants.js
new file mode 100644
index 00000000000..26f7d4899a6
--- /dev/null
+++ b/packages/mdc-tab-bar/constants.js
@@ -0,0 +1,41 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** @enum {string} */
+const strings = {
+ TAB_ACTIVATED_EVENT: 'MDCTabBar:activated',
+ TAB_SCROLLER_SELECTOR: '.mdc-tab-scroller',
+ TAB_SELECTOR: '.mdc-tab',
+ END_KEY: 'End',
+ HOME_KEY: 'Home',
+ ARROW_LEFT_KEY: 'ArrowLeft',
+ ARROW_RIGHT_KEY: 'ArrowRight',
+};
+
+/** @enum {number} */
+const numbers = {
+ EXTRA_SCROLL_AMOUNT: 20,
+ END_KEYCODE: 35,
+ HOME_KEYCODE: 36,
+ ARROW_LEFT_KEYCODE: 37,
+ ARROW_RIGHT_KEYCODE: 39,
+};
+
+export {
+ numbers,
+ strings,
+};
diff --git a/packages/mdc-tab-bar/foundation.js b/packages/mdc-tab-bar/foundation.js
new file mode 100644
index 00000000000..ccc30691db5
--- /dev/null
+++ b/packages/mdc-tab-bar/foundation.js
@@ -0,0 +1,402 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+import MDCFoundation from '@material/base/foundation';
+
+import {strings, numbers} from './constants';
+import MDCTabBarAdapter from './adapter';
+
+/* eslint-disable no-unused-vars */
+import MDCTabFoundation from '@material/tab/foundation';
+import {MDCTabDimensions} from '@material/tab/adapter';
+/* eslint-enable no-unused-vars */
+
+/**
+ * @type {Set}
+ */
+const ACCEPTABLE_KEYS = new Set();
+// IE11 has no support for new Set with iterable so we need to initialize this by hand
+ACCEPTABLE_KEYS.add(strings.ARROW_LEFT_KEY);
+ACCEPTABLE_KEYS.add(strings.ARROW_RIGHT_KEY);
+ACCEPTABLE_KEYS.add(strings.END_KEY);
+ACCEPTABLE_KEYS.add(strings.HOME_KEY);
+
+/**
+ * @type {Map}
+ */
+const KEYCODE_MAP = new Map();
+// IE11 has no support for new Map with iterable so we need to initialize this by hand
+KEYCODE_MAP.set(numbers.HOME_KEYCODE, strings.HOME_KEY);
+KEYCODE_MAP.set(numbers.END_KEYCODE, strings.END_KEY);
+KEYCODE_MAP.set(numbers.ARROW_LEFT_KEYCODE, strings.ARROW_LEFT_KEY);
+KEYCODE_MAP.set(numbers.ARROW_RIGHT_KEYCODE, strings.ARROW_RIGHT_KEY);
+
+/**
+ * @extends {MDCFoundation}
+ * @final
+ */
+class MDCTabBarFoundation extends MDCFoundation {
+ /** @return enum {string} */
+ static get strings() {
+ return strings;
+ }
+
+ /** @return enum {number} */
+ static get numbers() {
+ return numbers;
+ }
+
+ /**
+ * @see MDCTabBarAdapter for typing information
+ * @return {!MDCTabBarAdapter}
+ */
+ static get defaultAdapter() {
+ return /** @type {!MDCTabBarAdapter} */ ({
+ scrollTo: () => {},
+ incrementScroll: () => {},
+ getScrollPosition: () => {},
+ getScrollContentWidth: () => {},
+ getOffsetWidth: () => {},
+ isRTL: () => {},
+ activateTabAtIndex: () => {},
+ deactivateTabAtIndex: () => {},
+ getTabIndicatorClientRectAtIndex: () => {},
+ getTabDimensionsAtIndex: () => {},
+ getActiveTabIndex: () => {},
+ getIndexOfTab: () => {},
+ getTabListLength: () => {},
+ notifyTabActivated: () => {},
+ });
+ }
+
+ /**
+ * @param {!MDCTabBarAdapter} adapter
+ * */
+ constructor(adapter) {
+ super(Object.assign(MDCTabBarFoundation.defaultAdapter, adapter));
+ }
+
+ init() {
+ const activeIndex = this.adapter_.getActiveTabIndex();
+ this.scrollIntoView(activeIndex);
+ }
+
+ /**
+ * Activates the tab at the given index
+ * @param {number} index
+ */
+ activateTab(index) {
+ const previousActiveIndex = this.adapter_.getActiveTabIndex();
+ if (!this.indexIsInRange_(index)) {
+ return;
+ }
+
+ this.adapter_.deactivateTabAtIndex(previousActiveIndex);
+ this.adapter_.activateTabAtIndex(index, this.adapter_.getTabIndicatorClientRectAtIndex(previousActiveIndex));
+ this.scrollIntoView(index);
+
+ // Only notify the tab activation if the index is different than the previously active index
+ if (index !== previousActiveIndex) {
+ this.adapter_.notifyTabActivated(index);
+ }
+ }
+
+ /**
+ * Handles the keydown event
+ * @param {!Event} evt
+ */
+ handleKeyDown(evt) {
+ // Get the key from the event
+ const key = this.getKeyFromEvent_(evt);
+
+ // Early exit if the event key isn't one of the keyboard navigation keys
+ if (key === undefined) {
+ return;
+ }
+
+ evt.preventDefault();
+ this.activateTabFromKey_(key);
+ }
+
+ /**
+ * Handles the MDCTab:interacted event
+ * @param {!Event} evt
+ */
+ handleTabInteraction(evt) {
+ this.activateTab(this.adapter_.getIndexOfTab(evt.detail.tab));
+ }
+
+ /**
+ * Scrolls the tab at the given index into view
+ * @param {number} index The tab index to make visible
+ */
+ scrollIntoView(index) {
+ // Early exit if the index is out of range
+ if (!this.indexIsInRange_(index)) {
+ return;
+ }
+
+ // Always scroll to 0 if scrolling to the 0th index
+ if (index === 0) {
+ return this.adapter_.scrollTo(0);
+ }
+
+ // Always scroll to the max value if scrolling to the Nth index
+ // MDCTabScroller.scrollTo() will never scroll past the max possible value
+ if (index === this.adapter_.getTabListLength() - 1) {
+ return this.adapter_.scrollTo(this.adapter_.getScrollContentWidth());
+ }
+
+ if (this.isRTL_()) {
+ return this.scrollIntoViewRTL_(index);
+ }
+
+ this.scrollIntoView_(index);
+ }
+
+ /**
+ * Private method for activating a tab from a key
+ * @param {string} key The name of the key
+ * @private
+ */
+ activateTabFromKey_(key) {
+ const isRTL = this.isRTL_();
+ const maxTabIndex = this.adapter_.getTabListLength() - 1;
+ const shouldGoToEnd = key === strings.END_KEY;
+ const shouldDecrement = key === strings.ARROW_LEFT_KEY && !isRTL || key === strings.ARROW_RIGHT_KEY && isRTL;
+ const shouldIncrement = key === strings.ARROW_RIGHT_KEY && !isRTL || key === strings.ARROW_LEFT_KEY && isRTL;
+ let tabIndex = this.adapter_.getActiveTabIndex();
+
+ if (shouldGoToEnd) {
+ tabIndex = maxTabIndex;
+ } else if (shouldDecrement) {
+ tabIndex -= 1;
+ } else if (shouldIncrement) {
+ tabIndex += 1;
+ } else {
+ tabIndex = 0;
+ }
+
+ if (tabIndex < 0) {
+ tabIndex = maxTabIndex;
+ } else if (tabIndex > maxTabIndex) {
+ tabIndex = 0;
+ }
+
+ this.activateTab(tabIndex);
+ }
+
+ /**
+ * Calculates the scroll increment that will make the tab at the given index visible
+ * @param {number} index The index of the tab
+ * @param {number} nextIndex The index of the next tab
+ * @param {number} scrollPosition The current scroll position
+ * @param {number} barWidth The width of the Tab Bar
+ * @return {number}
+ * @private
+ */
+ calculateScrollIncrement_(index, nextIndex, scrollPosition, barWidth) {
+ const nextTabDimensions = this.adapter_.getTabDimensionsAtIndex(nextIndex);
+ const relativeContentLeft = nextTabDimensions.contentLeft - scrollPosition - barWidth;
+ const relativeContentRight = nextTabDimensions.contentRight - scrollPosition;
+ const leftIncrement = relativeContentRight - numbers.EXTRA_SCROLL_AMOUNT;
+ const rightIncrement = relativeContentLeft + numbers.EXTRA_SCROLL_AMOUNT;
+
+ if (nextIndex < index) {
+ return Math.min(leftIncrement, 0);
+ }
+
+ return Math.max(rightIncrement, 0);
+ }
+
+ /**
+ * Calculates the scroll increment that will make the tab at the given index visible in RTL
+ * @param {number} index The index of the tab
+ * @param {number} nextIndex The index of the next tab
+ * @param {number} scrollPosition The current scroll position
+ * @param {number} barWidth The width of the Tab Bar
+ * @param {number} scrollContentWidth The width of the scroll content
+ * @return {number}
+ * @private
+ */
+ calculateScrollIncrementRTL_(index, nextIndex, scrollPosition, barWidth, scrollContentWidth, ) {
+ const nextTabDimensions = this.adapter_.getTabDimensionsAtIndex(nextIndex);
+ const relativeContentLeft = scrollContentWidth - nextTabDimensions.contentLeft - scrollPosition;
+ const relativeContentRight = scrollContentWidth - nextTabDimensions.contentRight - scrollPosition - barWidth;
+ const leftIncrement = relativeContentRight + numbers.EXTRA_SCROLL_AMOUNT;
+ const rightIncrement = relativeContentLeft - numbers.EXTRA_SCROLL_AMOUNT;
+
+ if (nextIndex > index) {
+ return Math.max(leftIncrement, 0);
+ }
+
+ return Math.min(rightIncrement, 0);
+ }
+
+ /**
+ * Determines the index of the adjacent tab closest to either edge of the Tab Bar
+ * @param {number} index The index of the tab
+ * @param {!MDCTabDimensions} tabDimensions The dimensions of the tab
+ * @param {number} scrollPosition The current scroll position
+ * @param {number} barWidth The width of the tab bar
+ * @return {number}
+ * @private
+ */
+ findAdjacentTabIndexClosestToEdge_(index, tabDimensions, scrollPosition, barWidth) {
+ /**
+ * Tabs are laid out in the Tab Scroller like this:
+ *
+ * Scroll Position
+ * +---+
+ * | | Bar Width
+ * | +-----------------------------------+
+ * | | |
+ * | V V
+ * | +-----------------------------------+
+ * V | Tab Scroller |
+ * +------------+--------------+-------------------+
+ * | Tab | Tab | Tab |
+ * +------------+--------------+-------------------+
+ * | |
+ * +-----------------------------------+
+ *
+ * To determine the next adjacent index, we look at the Tab root left and
+ * Tab root right, both relative to the scroll position. If the Tab root
+ * left is less than 0, then we know it's out of view to the left. If the
+ * Tab root right minus the bar width is greater than 0, we know the Tab is
+ * out of view to the right. From there, we either increment or decrement
+ * the index.
+ */
+ const relativeRootLeft = tabDimensions.rootLeft - scrollPosition;
+ const relativeRootRight = tabDimensions.rootRight - scrollPosition - barWidth;
+ const relativeRootDelta = relativeRootLeft + relativeRootRight;
+ const leftEdgeIsCloser = relativeRootLeft < 0 || relativeRootDelta < 0;
+ const rightEdgeIsCloser = relativeRootRight > 0 || relativeRootDelta > 0;
+
+ if (leftEdgeIsCloser) {
+ return index - 1;
+ }
+
+ if (rightEdgeIsCloser) {
+ return index + 1;
+ }
+
+ return -1;
+ }
+
+ /**
+ * Determines the index of the adjacent tab closest to either edge of the Tab Bar in RTL
+ * @param {number} index The index of the tab
+ * @param {!MDCTabDimensions} tabDimensions The dimensions of the tab
+ * @param {number} scrollPosition The current scroll position
+ * @param {number} barWidth The width of the tab bar
+ * @param {number} scrollContentWidth The width of the scroller content
+ * @return {number}
+ * @private
+ */
+ findAdjacentTabIndexClosestToEdgeRTL_(index, tabDimensions, scrollPosition, barWidth, scrollContentWidth) {
+ const rootLeft = scrollContentWidth - tabDimensions.rootLeft - barWidth - scrollPosition;
+ const rootRight = scrollContentWidth - tabDimensions.rootRight - scrollPosition;
+ const rootDelta = rootLeft + rootRight;
+ const leftEdgeIsCloser = rootLeft > 0 || rootDelta > 0;
+ const rightEdgeIsCloser = rootRight < 0 || rootDelta < 0;
+
+ if (leftEdgeIsCloser) {
+ return index + 1;
+ }
+
+ if (rightEdgeIsCloser) {
+ return index - 1;
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns the key associated with a keydown event
+ * @param {!Event} evt The keydown event
+ * @return {string}
+ * @private
+ */
+ getKeyFromEvent_(evt) {
+ if (ACCEPTABLE_KEYS.has(evt.key)) {
+ return evt.key;
+ }
+
+ return KEYCODE_MAP.get(evt.keyCode);
+ }
+
+ /**
+ * Returns whether a given index is inclusively between the ends
+ * @param {number} index The index to test
+ * @private
+ */
+ indexIsInRange_(index) {
+ return index >= 0 && index < this.adapter_.getTabListLength();
+ }
+
+ /**
+ * Returns the view's RTL property
+ * @return {boolean}
+ * @private
+ */
+ isRTL_() {
+ return this.adapter_.isRTL();
+ }
+
+ /**
+ * Scrolls the tab at the given index into view for left-to-right useragents
+ * @param {number} index The index of the tab to scroll into view
+ * @private
+ */
+ scrollIntoView_(index) {
+ const scrollPosition = this.adapter_.getScrollPosition();
+ const barWidth = this.adapter_.getOffsetWidth();
+ const tabDimensions = this.adapter_.getTabDimensionsAtIndex(index);
+ const nextIndex = this.findAdjacentTabIndexClosestToEdge_(index, tabDimensions, scrollPosition, barWidth);
+
+ if (!this.indexIsInRange_(nextIndex)) {
+ return;
+ }
+
+ const scrollIncrement = this.calculateScrollIncrement_(index, nextIndex, scrollPosition, barWidth);
+ this.adapter_.incrementScroll(scrollIncrement);
+ }
+
+ /**
+ * Scrolls the tab at the given index into view in RTL
+ * @param {number} index The tab index to make visible
+ * @private
+ */
+ scrollIntoViewRTL_(index) {
+ const scrollPosition = this.adapter_.getScrollPosition();
+ const barWidth = this.adapter_.getOffsetWidth();
+ const tabDimensions = this.adapter_.getTabDimensionsAtIndex(index);
+ const scrollWidth = this.adapter_.getScrollContentWidth();
+ const nextIndex = this.findAdjacentTabIndexClosestToEdgeRTL_(
+ index, tabDimensions, scrollPosition, barWidth, scrollWidth);
+
+ if (!this.indexIsInRange_(nextIndex)) {
+ return;
+ }
+
+ const scrollIncrement = this.calculateScrollIncrementRTL_(index, nextIndex, scrollPosition, barWidth, scrollWidth);
+ this.adapter_.incrementScroll(scrollIncrement);
+ }
+}
+
+export default MDCTabBarFoundation;
diff --git a/packages/mdc-tab-bar/index.js b/packages/mdc-tab-bar/index.js
new file mode 100644
index 00000000000..1ec6ea937d6
--- /dev/null
+++ b/packages/mdc-tab-bar/index.js
@@ -0,0 +1,148 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import MDCComponent from '@material/base/component';
+
+import {MDCTab, MDCTabFoundation} from '@material/tab/index';
+import {MDCTabScroller} from '@material/tab-scroller/index';
+
+import MDCTabBarAdapter from './adapter';
+import MDCTabBarFoundation from './foundation';
+
+/**
+ * @extends {MDCComponent}
+ * @final
+ */
+class MDCTabBar extends MDCComponent {
+ /**
+ * @param {...?} args
+ */
+ constructor(...args) {
+ super(...args);
+
+ /** @private {!Array} */
+ this.tabList_;
+
+ /** @type {(function(!Element): !MDCTab)} */
+ this.tabFactory_;
+
+ /** @private {?MDCTabScroller} */
+ this.tabScroller_;
+
+ /** @type {(function(!Element): !MDCTabScroller)} */
+ this.tabScrollerFactory_;
+
+ /** @private {?function(?Event): undefined} */
+ this.handleTabInteraction_;
+
+ /** @private {?function(?Event): undefined} */
+ this.handleKeyDown_;
+ }
+
+ /**
+ * @param {!Element} root
+ * @return {!MDCTabBar}
+ */
+ static attachTo(root) {
+ return new MDCTabBar(root);
+ }
+
+ /**
+ * @param {(function(!Element): !MDCTab)=} tabFactory A function which creates a new MDCTab
+ * @param {(function(!Element): !MDCTabScroller)=} tabScrollerFactory A function which creates a new MDCTabScroller
+ */
+ initialize(
+ tabFactory = (el) => new MDCTab(el),
+ tabScrollerFactory = (el) => new MDCTabScroller(el),
+ ) {
+ this.tabFactory_ = tabFactory;
+ this.tabScrollerFactory_ = tabScrollerFactory;
+
+ const tabElements = [].slice.call(this.root_.querySelectorAll(MDCTabBarFoundation.strings.TAB_SELECTOR));
+ this.tabList_ = tabElements.map((el) => this.tabFactory_(el));
+
+ const tabScrollerElement = this.root_.querySelector(MDCTabBarFoundation.strings.TAB_SCROLLER_SELECTOR);
+ if (tabScrollerElement) {
+ this.tabScroller_ = this.tabScrollerFactory_(tabScrollerElement);
+ }
+ }
+
+ initialSyncWithDOM() {
+ this.handleTabInteraction_ = (evt) => this.foundation_.handleTabInteraction(evt);
+ this.handleKeyDown_ = (evt) => this.foundation_.handleKeyDown(evt);
+
+ this.root_.addEventListener(MDCTabFoundation.strings.INTERACTED_EVENT, this.handleTabInteraction_);
+ this.root_.addEventListener('keydown', this.handleKeyDown_);
+ }
+
+ destroy() {
+ super.destroy();
+ this.root_.removeEventListener(MDCTabFoundation.strings.INTERACTED_EVENT, this.handleTabInteraction_);
+ this.root_.removeEventListener('keydown', this.handleKeyDown_);
+ this.tabList_.forEach((tab) => tab.destroy());
+ this.tabScroller_.destroy();
+ }
+
+ /**
+ * @return {!MDCTabBarFoundation}
+ */
+ getDefaultFoundation() {
+ return new MDCTabBarFoundation(
+ /** @type {!MDCTabBarAdapter} */ ({
+ scrollTo: (scrollX) => this.tabScroller_.scrollTo(scrollX),
+ incrementScroll: (scrollXIncrement) => this.tabScroller_.incrementScroll(scrollXIncrement),
+ getScrollPosition: () => this.tabScroller_.getScrollPosition(),
+ getScrollContentWidth: () => this.tabScroller_.getScrollContentWidth(),
+ getOffsetWidth: () => this.root_.offsetWidth,
+ isRTL: () => window.getComputedStyle(this.root_).getPropertyValue('direction') === 'rtl',
+ activateTabAtIndex: (index, clientRect) => this.tabList_[index].activate(clientRect),
+ deactivateTabAtIndex: (index) => this.tabList_[index].deactivate(),
+ getTabIndicatorClientRectAtIndex: (index) => this.tabList_[index].computeIndicatorClientRect(),
+ getTabDimensionsAtIndex: (index) => this.tabList_[index].computeDimensions(),
+ getActiveTabIndex: () => {
+ for (let i = 0; i < this.tabList_.length; i++) {
+ if (this.tabList_[i].active) {
+ return i;
+ }
+ }
+ return -1;
+ },
+ getIndexOfTab: (tabToFind) => this.tabList_.indexOf(tabToFind),
+ getTabListLength: () => this.tabList_.length,
+ notifyTabActivated: (index) => this.emit(MDCTabBarFoundation.strings.TAB_ACTIVATED_EVENT, {index}, true),
+ })
+ );
+ }
+
+ /**
+ * Activates the tab at the given index
+ * @param {number} index The index of the tab
+ */
+ activateTab(index) {
+ this.foundation_.activateTab(index);
+ }
+
+ /**
+ * Scrolls the tab at the given index into view
+ * @param {number} index THe index of the tab
+ */
+ scrollIntoView(index) {
+ this.foundation_.scrollIntoView(index);
+ }
+}
+
+export {MDCTabBar, MDCTabBarFoundation};
diff --git a/packages/mdc-tab-bar/mdc-tab-bar.scss b/packages/mdc-tab-bar/mdc-tab-bar.scss
new file mode 100644
index 00000000000..df351f43383
--- /dev/null
+++ b/packages/mdc-tab-bar/mdc-tab-bar.scss
@@ -0,0 +1,22 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import "./mixins";
+
+.mdc-tab-bar {
+ @include mdc-tab-bar-width(100%);
+}
diff --git a/packages/mdc-tab-bar/package.json b/packages/mdc-tab-bar/package.json
new file mode 100644
index 00000000000..0ac5e8a37ec
--- /dev/null
+++ b/packages/mdc-tab-bar/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "@material/tab-bar",
+ "description": "The Material Components for the web tab bar component",
+ "version": "0.0.0",
+ "license": "Apache-2.0",
+ "keywords": [
+ "material components",
+ "material design",
+ "tab",
+ "bar"
+ ],
+ "main": "index.js",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/material-components/material-components-web.git"
+ },
+ "dependencies": {
+ "@material/base": "^0.35.0",
+ "@material/tab": "^0.37.1",
+ "@material/tab-scroller": "^0.0.0",
+ "@material/elevation": "^0.36.1"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/mdc-tab-indicator/package.json b/packages/mdc-tab-indicator/package.json
index 2a676259381..e954f317593 100644
--- a/packages/mdc-tab-indicator/package.json
+++ b/packages/mdc-tab-indicator/package.json
@@ -3,7 +3,6 @@
"description": "The Material Components for the web tab indicator component",
"version": "0.0.0",
"license": "Apache-2.0",
- "private": true,
"keywords": [
"material components",
"material design",
diff --git a/packages/mdc-tab-scroller/package.json b/packages/mdc-tab-scroller/package.json
index de0a260b8f2..ed6084bd89f 100644
--- a/packages/mdc-tab-scroller/package.json
+++ b/packages/mdc-tab-scroller/package.json
@@ -3,7 +3,6 @@
"description": "The Material Components for the web tab scroller component",
"version": "0.0.0",
"license": "Apache-2.0",
- "private": true,
"keywords": [
"material components",
"material design",
diff --git a/packages/mdc-tab/README.md b/packages/mdc-tab/README.md
index 845f923808c..b24e77ce29b 100644
--- a/packages/mdc-tab/README.md
+++ b/packages/mdc-tab/README.md
@@ -9,13 +9,6 @@ path: /catalog/tabs/tab/
# Tab
-
-
Tabs organize and allow navigation between groups of content that are related and at the same level of hierarchy.
Each Tab governs the visibility of one group of content.
diff --git a/packages/mdc-tab/package.json b/packages/mdc-tab/package.json
index 9d2d3321a60..99e8c219bc9 100644
--- a/packages/mdc-tab/package.json
+++ b/packages/mdc-tab/package.json
@@ -3,7 +3,6 @@
"description": "The Material Components for the web tab component",
"version": "0.37.1",
"license": "Apache-2.0",
- "private": true,
"keywords": [
"material components",
"material design",
diff --git a/packages/mdc-tabs/README.md b/packages/mdc-tabs/README.md
index 4eb7fbf76d5..fc238de10dd 100644
--- a/packages/mdc-tabs/README.md
+++ b/packages/mdc-tabs/README.md
@@ -9,10 +9,11 @@ path: /catalog/tabs/legacy/
## Important - Deprecation Notice
-The `mdc-tabs` package is deprecated and no longer maintained. Improved functionality is available across the
-`mdc-tab-bar`, `mdc-tab-scroller`, `mdc-tab-indicator`, and `mdc-tab` packages. Bugs and feature requests
-will no longer be accepted for this package. It is recommended that you migrate to the new packages to continue to
-receive new features and updates.
+The `mdc-tabs` package is deprecated and no longer maintained, and is no longer included in the all-in-one
+`material-components-web` package. Improved functionality is available across the `mdc-tab-bar`, `mdc-tab-scroller`,
+`mdc-tab-indicator`, and `mdc-tab` packages, which are now included in the `material-components-web` package.
+Bugs and feature requests will no longer be accepted for this package. It is recommended that you migrate to the new
+packages to continue to receive new features and updates.
# MDC Tabs
diff --git a/scripts/check-pkg-for-release.js b/scripts/check-pkg-for-release.js
index 139a9c6be60..6c00ac4ed08 100644
--- a/scripts/check-pkg-for-release.js
+++ b/scripts/check-pkg-for-release.js
@@ -45,6 +45,21 @@ const MASTER_PKG = require(path.join(process.env.PWD, MASTER_PKG_PATH));
// are necessary since other MDC packages depend on them.
const CSS_WHITELIST = ['base', 'animation', 'auto-init', 'rtl', 'selection-control'];
+// List of packages that are intentionally not included in the MCW package's dependencies
+const NOT_MCW_DEP = [
+ 'tabs', // Deprecated; CSS classes conflict with tab and tab-bar
+];
+
+const NOT_AUTOINIT = [
+ 'auto-init',
+ 'base',
+ 'selection-control',
+ 'tab', // Only makes sense in context of tab-bar
+ 'tab-indicator', // Only makes sense in context of tab-bar
+ 'tab-scroller', // Only makes sense in context of tab-bar
+ 'tabs', // Deprecated
+];
+
main();
function main() {
@@ -127,15 +142,17 @@ function checkDependencyAddedInMDCPackage() {
}
function checkPkgDependencyAddedInMDCPackage() {
- assert.notEqual(typeof MASTER_PKG.dependencies[pkg.name], 'undefined',
- 'FAILURE: Component ' + pkg.name + ' is not a denpendency for MDC Web. ' +
- 'Please add ' + pkg.name +' to ' + MASTER_PKG_PATH + '\' dependencies before commit.');
+ if (NOT_MCW_DEP.indexOf(getPkgName()) === -1) {
+ assert.notEqual(typeof MASTER_PKG.dependencies[pkg.name], 'undefined',
+ 'FAILURE: Component ' + pkg.name + ' is not a denpendency for MDC Web. ' +
+ 'Please add ' + pkg.name +' to ' + MASTER_PKG_PATH + '\' dependencies before commit.');
+ }
}
function checkCSSDependencyAddedInMDCPackage() {
const name = getPkgName();
const nameMDC = `mdc-${name}`;
- if (CSS_WHITELIST.indexOf(name) === -1) {
+ if (CSS_WHITELIST.indexOf(name) === -1 && NOT_MCW_DEP.indexOf(name) === -1) {
const src = fs.readFileSync(path.join(process.env.PWD, MASTER_CSS_PATH), 'utf8');
const cssRules = cssom.parse(src).cssRules;
const cssRule = path.join(pkg.name, nameMDC);
@@ -150,9 +167,8 @@ function checkCSSDependencyAddedInMDCPackage() {
function checkJSDependencyAddedInMDCPackage() {
const NOT_IMPORTED = ['animation'];
- const NOT_AUTOINIT = ['auto-init', 'base', 'selection-control'];
const name = getPkgName();
- if (typeof(pkg.main) !== 'undefined' && NOT_IMPORTED.indexOf(name) === -1) {
+ if (typeof(pkg.main) !== 'undefined' && NOT_IMPORTED.indexOf(name) === -1 && NOT_MCW_DEP.indexOf(name) === -1) {
const nameCamel = camelCase(pkg.name.replace('@material/', ''));
const src = fs.readFileSync(path.join(process.env.PWD, MASTER_JS_PATH), 'utf8');
const ast = recast.parse(src, {
diff --git a/scripts/webpack/css-bundle-factory.js b/scripts/webpack/css-bundle-factory.js
index 3c1429665d3..a84bac23e4c 100644
--- a/scripts/webpack/css-bundle-factory.js
+++ b/scripts/webpack/css-bundle-factory.js
@@ -163,7 +163,9 @@ class CssBundleFactory {
'mdc.snackbar': getAbsolutePath('/packages/mdc-snackbar/mdc-snackbar.scss'),
'mdc.switch': getAbsolutePath('/packages/mdc-switch/mdc-switch.scss'),
'mdc.tab': getAbsolutePath('/packages/mdc-tab/mdc-tab.scss'),
+ 'mdc.tab-bar': getAbsolutePath('/packages/mdc-tab-bar/mdc-tab-bar.scss'),
'mdc.tab-indicator': getAbsolutePath('/packages/mdc-tab-indicator/mdc-tab-indicator.scss'),
+ 'mdc.tab-scroller': getAbsolutePath('/packages/mdc-tab-scroller/mdc-tab-scroller.scss'),
'mdc.tabs': getAbsolutePath('/packages/mdc-tabs/mdc-tabs.scss'),
'mdc.textfield': getAbsolutePath('/packages/mdc-textfield/mdc-text-field.scss'),
'mdc.theme': getAbsolutePath('/packages/mdc-theme/mdc-theme.scss'),
diff --git a/scripts/webpack/js-bundle-factory.js b/scripts/webpack/js-bundle-factory.js
index 1d2aa864827..791539823c3 100644
--- a/scripts/webpack/js-bundle-factory.js
+++ b/scripts/webpack/js-bundle-factory.js
@@ -147,6 +147,7 @@ class JsBundleFactory {
slider: getAbsolutePath('/packages/mdc-slider/index.js'),
snackbar: getAbsolutePath('/packages/mdc-snackbar/index.js'),
tab: getAbsolutePath('/packages/mdc-tab/index.js'),
+ tabBar: getAbsolutePath('/packages/mdc-tab-bar/index.js'),
tabIndicator: getAbsolutePath('/packages/mdc-tab-indicator/index.js'),
tabScroller: getAbsolutePath('/packages/mdc-tab-scroller/index.js'),
tabs: getAbsolutePath('/packages/mdc-tabs/index.js'),
diff --git a/test/unit/mdc-tab-bar/foundation.test.js b/test/unit/mdc-tab-bar/foundation.test.js
new file mode 100644
index 00000000000..ae9194a32e4
--- /dev/null
+++ b/test/unit/mdc-tab-bar/foundation.test.js
@@ -0,0 +1,582 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+import {assert} from 'chai';
+import td from 'testdouble';
+
+import {verifyDefaultAdapter} from '../helpers/foundation';
+import {setupFoundationTest} from '../helpers/setup';
+import MDCTabBarFoundation from '../../../packages/mdc-tab-bar/foundation';
+
+suite('MDCTabBarFoundation');
+
+test('exports cssClasses', () => {
+ assert.isOk('cssClasses' in MDCTabBarFoundation);
+});
+
+test('exports strings', () => {
+ assert.isOk('strings' in MDCTabBarFoundation);
+});
+
+test('defaultAdapter returns a complete adapter implementation', () => {
+ verifyDefaultAdapter(MDCTabBarFoundation, [
+ 'scrollTo', 'incrementScroll', 'getScrollPosition', 'getScrollContentWidth',
+ 'getOffsetWidth', 'isRTL',
+ 'activateTabAtIndex', 'deactivateTabAtIndex',
+ 'getTabIndicatorClientRectAtIndex', 'getTabDimensionsAtIndex',
+ 'getActiveTabIndex', 'getIndexOfTab', 'getTabListLength',
+ 'notifyTabActivated',
+ ]);
+});
+
+const setupTest = () => setupFoundationTest(MDCTabBarFoundation);
+
+test('#init() scrolls the active tab into view', () => {
+ const {foundation, mockAdapter} = setupTest();
+ foundation.scrollIntoView = td.function();
+ td.when(mockAdapter.getActiveTabIndex()).thenReturn(99);
+ foundation.init();
+ td.verify(foundation.scrollIntoView(99), {times: 1});
+});
+
+const stubActivateTab = () => {
+ const {foundation, mockAdapter} = setupTest();
+ const activateTab = td.function();
+ foundation.activateTab = activateTab;
+ return {activateTab, foundation, mockAdapter};
+};
+
+const mockKeyDownEvent = ({key, keyCode}) => {
+ const preventDefault = td.function();
+ const fakeEvent = {
+ key,
+ keyCode,
+ preventDefault,
+ };
+
+ return {preventDefault, fakeEvent};
+};
+
+test('#handleTabInteraction() activates the tab', () => {
+ const {foundation, activateTab} = stubActivateTab();
+ foundation.handleTabInteraction({detail: {}});
+ td.verify(activateTab(td.matchers.anything()), {times: 1});
+});
+
+test('#handleKeyDown() activates the tab at the 0th index when the home key is pressed', () => {
+ const {foundation, activateTab} = stubActivateTab();
+ const {fakeEvent: fakeKeyEvent} = mockKeyDownEvent({key: 'Home'});
+ const {fakeEvent: fakeKeyCodeEvent} = mockKeyDownEvent({keyCode: 36});
+ foundation.handleKeyDown(fakeKeyEvent);
+ foundation.handleKeyDown(fakeKeyCodeEvent);
+ td.verify(activateTab(0), {times: 2});
+});
+
+test('#handleKeyDown() activates the tab at the N - 1 index when the end key is pressed', () => {
+ const {foundation, mockAdapter, activateTab} = stubActivateTab();
+ const {fakeEvent: fakeKeyEvent} = mockKeyDownEvent({key: 'End'});
+ const {fakeEvent: fakeKeyCodeEvent} = mockKeyDownEvent({keyCode: 35});
+ td.when(mockAdapter.getTabListLength()).thenReturn(13);
+ foundation.handleKeyDown(fakeKeyEvent);
+ foundation.handleKeyDown(fakeKeyCodeEvent);
+ td.verify(activateTab(12), {times: 2});
+});
+
+test('#handleKeyDown() activates the tab at the previous index when the left arrow key is pressed', () => {
+ const {foundation, mockAdapter, activateTab} = stubActivateTab();
+ const {fakeEvent: fakeKeyEvent} = mockKeyDownEvent({key: 'ArrowLeft'});
+ const {fakeEvent: fakeKeyCodeEvent} = mockKeyDownEvent({keyCode: 37});
+ td.when(mockAdapter.getActiveTabIndex()).thenReturn(2);
+ td.when(mockAdapter.getTabListLength()).thenReturn(13);
+ foundation.handleKeyDown(fakeKeyEvent);
+ foundation.handleKeyDown(fakeKeyCodeEvent);
+ td.verify(activateTab(1), {times: 2});
+});
+
+test('#handleKeyDown() activates the tab at the next index when the right arrow key is pressed'
+ + ' and the text direction is RTL', () => {
+ const {foundation, mockAdapter, activateTab} = stubActivateTab();
+ const {fakeEvent: fakeKeyEvent} = mockKeyDownEvent({key: 'ArrowLeft'});
+ const {fakeEvent: fakeKeyCodeEvent} = mockKeyDownEvent({keyCode: 37});
+ td.when(mockAdapter.isRTL()).thenReturn(true);
+ td.when(mockAdapter.getActiveTabIndex()).thenReturn(2);
+ td.when(mockAdapter.getTabListLength()).thenReturn(13);
+ foundation.handleKeyDown(fakeKeyEvent);
+ foundation.handleKeyDown(fakeKeyCodeEvent);
+ td.verify(activateTab(3), {times: 2});
+});
+
+test('#handleKeyDown() activates the tab at the N - 1 index when the left arrow key is pressed'
+ + ' and the current active index is 0', () => {
+ const {foundation, mockAdapter, activateTab} = stubActivateTab();
+ const {fakeEvent: fakeKeyEvent} = mockKeyDownEvent({key: 'ArrowLeft'});
+ const {fakeEvent: fakeKeyCodeEvent} = mockKeyDownEvent({keyCode: 37});
+ td.when(mockAdapter.getActiveTabIndex()).thenReturn(0);
+ td.when(mockAdapter.getTabListLength()).thenReturn(13);
+ foundation.handleKeyDown(fakeKeyEvent);
+ foundation.handleKeyDown(fakeKeyCodeEvent);
+ td.verify(activateTab(12), {times: 2});
+});
+
+test('#handleKeyDown() activates the tab at the N - 1 index when the right arrow key is pressed'
+ + ' and the current active index is the 0th index and the text direction is RTL', () => {
+ const {foundation, mockAdapter, activateTab} = stubActivateTab();
+ const {fakeEvent: fakeKeyEvent} = mockKeyDownEvent({key: 'ArrowRight'});
+ const {fakeEvent: fakeKeyCodeEvent} = mockKeyDownEvent({keyCode: 39});
+ td.when(mockAdapter.isRTL()).thenReturn(true);
+ td.when(mockAdapter.getActiveTabIndex()).thenReturn(0);
+ td.when(mockAdapter.getTabListLength()).thenReturn(13);
+ foundation.handleKeyDown(fakeKeyEvent);
+ foundation.handleKeyDown(fakeKeyCodeEvent);
+ td.verify(activateTab(12), {times: 2});
+});
+
+test('#handleKeyDown() activates the tab at the next index when the right arrow key is pressed', () => {
+ const {foundation, mockAdapter, activateTab} = stubActivateTab();
+ const {fakeEvent: fakeKeyEvent} = mockKeyDownEvent({key: 'ArrowRight'});
+ const {fakeEvent: fakeKeyCodeEvent} = mockKeyDownEvent({keyCode: 39});
+ td.when(mockAdapter.getActiveTabIndex()).thenReturn(2);
+ td.when(mockAdapter.getTabListLength()).thenReturn(13);
+ foundation.handleKeyDown(fakeKeyEvent);
+ foundation.handleKeyDown(fakeKeyCodeEvent);
+ td.verify(activateTab(3), {times: 2});
+});
+
+test('#handleKeyDown() activates the tab at the previous index when the right arrow key is pressed'
+ + ' and the text direction is RTL', () => {
+ const {foundation, mockAdapter, activateTab} = stubActivateTab();
+ const {fakeEvent: fakeKeyEvent} = mockKeyDownEvent({key: 'ArrowRight'});
+ const {fakeEvent: fakeKeyCodeEvent} = mockKeyDownEvent({keyCode: 39});
+ td.when(mockAdapter.isRTL()).thenReturn(true);
+ td.when(mockAdapter.getActiveTabIndex()).thenReturn(2);
+ td.when(mockAdapter.getTabListLength()).thenReturn(13);
+ foundation.handleKeyDown(fakeKeyEvent);
+ foundation.handleKeyDown(fakeKeyCodeEvent);
+ td.verify(activateTab(1), {times: 2});
+});
+
+test('#handleKeyDown() activates the tab at the 0th index when the right arrow key is pressed'
+ + ' and the current active index is the max index', () => {
+ const {foundation, mockAdapter, activateTab} = stubActivateTab();
+ const {fakeEvent: fakeKeyEvent} = mockKeyDownEvent({key: 'ArrowRight'});
+ const {fakeEvent: fakeKeyCodeEvent} = mockKeyDownEvent({keyCode: 39});
+ td.when(mockAdapter.getActiveTabIndex()).thenReturn(12);
+ td.when(mockAdapter.getTabListLength()).thenReturn(13);
+ foundation.handleKeyDown(fakeKeyEvent);
+ foundation.handleKeyDown(fakeKeyCodeEvent);
+ td.verify(activateTab(0), {times: 2});
+});
+
+test('#handleKeyDown() activates the tab at the 0th index when the left arrow key is pressed'
+ + ' and the current active index is the max index and the text direction is RTL', () => {
+ const {foundation, mockAdapter, activateTab} = stubActivateTab();
+ const {fakeEvent: fakeKeyEvent} = mockKeyDownEvent({key: 'ArrowLeft'});
+ const {fakeEvent: fakeKeyCodeEvent} = mockKeyDownEvent({keyCode: 37});
+ td.when(mockAdapter.isRTL()).thenReturn(true);
+ td.when(mockAdapter.getActiveTabIndex()).thenReturn(12);
+ td.when(mockAdapter.getTabListLength()).thenReturn(13);
+ foundation.handleKeyDown(fakeKeyEvent);
+ foundation.handleKeyDown(fakeKeyCodeEvent);
+ td.verify(activateTab(0), {times: 2});
+});
+
+test('#handleKeyDown() prevents the default behavior when the pressed key is ArrowLeft, ArrowRight, End, or Home',
+ () => {
+ ['ArrowLeft', 'ArrowRight', 'Home', 'End'].forEach((evtName) => {
+ const {foundation} = stubActivateTab();
+ const {fakeEvent, preventDefault} = mockKeyDownEvent({key: evtName});
+ foundation.handleKeyDown(fakeEvent);
+ td.verify(preventDefault());
+ });
+ });
+
+test('#handleKeyDown() prevents the default behavior when the pressed keyCode is 35, 36, 37, or 39', () => {
+ [35, 36, 37, 39].forEach((keyCode) => {
+ const {foundation} = stubActivateTab();
+ const {fakeEvent, preventDefault} = mockKeyDownEvent({keyCode});
+ foundation.handleKeyDown(fakeEvent);
+ td.verify(preventDefault());
+ });
+});
+
+test('#handleKeyDown() does not prevent the default behavior when a non-directional key is pressed', () => {
+ const {foundation} = stubActivateTab();
+ const {fakeEvent, preventDefault} = mockKeyDownEvent({key: 'Shift'});
+ foundation.handleKeyDown(fakeEvent);
+ td.verify(preventDefault(), {times: 0});
+});
+
+test('#handleKeyDown() does not prevent the default behavior when a non-directional keyCode is pressed', () => {
+ const {foundation} = stubActivateTab();
+ const {fakeEvent, preventDefault} = mockKeyDownEvent({keyCode: 16});
+ foundation.handleKeyDown(fakeEvent);
+ td.verify(preventDefault(), {times: 0});
+});
+
+test('#handleKeyDown() does not activate a tab when a non-directional key is pressed', () => {
+ const {foundation, activateTab} = stubActivateTab();
+ const {fakeEvent: fakeKeyEvent} = mockKeyDownEvent({key: 'Shift'});
+ const {fakeEvent: fakeKeyCodeEvent} = mockKeyDownEvent({keyCode: 16});
+ foundation.handleKeyDown(fakeKeyEvent);
+ foundation.handleKeyDown(fakeKeyCodeEvent);
+ td.verify(activateTab(), {times: 0});
+});
+
+const setupActivateTabTest = () => {
+ const {foundation, mockAdapter} = setupTest();
+ const scrollIntoView = td.function();
+ foundation.scrollIntoView = scrollIntoView;
+ return {foundation, mockAdapter, scrollIntoView};
+};
+
+test('#activateTab() does nothing if the index overflows the tab list', () => {
+ const {foundation, mockAdapter} = setupActivateTabTest();
+ td.when(mockAdapter.getTabListLength()).thenReturn(13);
+ foundation.activateTab(13);
+ td.verify(mockAdapter.deactivateTabAtIndex(td.matchers.isA(Number)), {times: 0});
+ td.verify(mockAdapter.activateTabAtIndex(td.matchers.isA(Number)), {times: 0});
+});
+
+test('#activateTab() does nothing if the index underflows the tab list', () => {
+ const {foundation, mockAdapter} = setupActivateTabTest();
+ td.when(mockAdapter.getTabListLength()).thenReturn(13);
+ foundation.activateTab(-1);
+ td.verify(mockAdapter.deactivateTabAtIndex(td.matchers.isA(Number)), {times: 0});
+ td.verify(mockAdapter.activateTabAtIndex(td.matchers.isA(Number)), {times: 0});
+});
+
+test(`#activateTab() does not emit the ${MDCTabBarFoundation.strings.TAB_ACTIVATED_EVENT} event if the index` +
+ ' is the currently active index', () => {
+ const {foundation, mockAdapter} = setupActivateTabTest();
+ td.when(mockAdapter.getTabListLength()).thenReturn(13);
+ td.when(mockAdapter.getActiveTabIndex()).thenReturn(6);
+ foundation.activateTab(6);
+ td.verify(mockAdapter.notifyTabActivated(td.matchers.anything()), {times: 0});
+});
+
+test('#activateTab() deactivates the previously active tab', () => {
+ const {foundation, mockAdapter} = setupActivateTabTest();
+ td.when(mockAdapter.getTabListLength()).thenReturn(13);
+ td.when(mockAdapter.getActiveTabIndex()).thenReturn(6);
+ foundation.activateTab(1);
+ td.verify(mockAdapter.deactivateTabAtIndex(6), {times: 1});
+});
+
+test('#activateTab() activates the newly active tab with the previously active tab\'s indicatorClientRect', () => {
+ const {foundation, mockAdapter} = setupActivateTabTest();
+ td.when(mockAdapter.getTabListLength()).thenReturn(13);
+ td.when(mockAdapter.getActiveTabIndex()).thenReturn(6);
+ td.when(mockAdapter.getTabIndicatorClientRectAtIndex(6)).thenReturn({
+ left: 22, right: 33,
+ });
+ foundation.activateTab(1);
+ td.verify(mockAdapter.activateTabAtIndex(1, {left: 22, right: 33}), {times: 1});
+});
+
+test('#activateTab() scrolls the new tab index into view', () => {
+ const {foundation, mockAdapter, scrollIntoView} = setupActivateTabTest();
+ td.when(mockAdapter.getTabListLength()).thenReturn(13);
+ td.when(mockAdapter.getActiveTabIndex()).thenReturn(6);
+ td.when(mockAdapter.getTabIndicatorClientRectAtIndex(6)).thenReturn({
+ left: 22, right: 33,
+ });
+ foundation.activateTab(1);
+ td.verify(scrollIntoView(1));
+});
+
+test(`#activateTab() emits the ${MDCTabBarFoundation.strings.TAB_ACTIVATED_EVENT} with the index of the tab`, () => {
+ const {foundation, mockAdapter} = setupActivateTabTest();
+ td.when(mockAdapter.getTabListLength()).thenReturn(13);
+ td.when(mockAdapter.getActiveTabIndex()).thenReturn(6);
+ td.when(mockAdapter.getTabIndicatorClientRectAtIndex(6)).thenReturn({
+ left: 22, right: 33,
+ });
+ foundation.activateTab(1);
+ td.verify(mockAdapter.notifyTabActivated(1));
+});
+
+function setupScrollIntoViewTest({
+ activeIndex = 0,
+ tabListLength = 10,
+ indicatorClientRect = {},
+ scrollContentWidth = 1000,
+ scrollPosition = 0,
+ offsetWidth = 400,
+ tabDimensionsMap = {}} = {}) {
+ const {foundation, mockAdapter} = setupTest();
+ td.when(mockAdapter.getActiveTabIndex()).thenReturn(activeIndex);
+ td.when(mockAdapter.getTabListLength()).thenReturn(tabListLength);
+ td.when(mockAdapter.getTabIndicatorClientRectAtIndex(td.matchers.isA(Number))).thenReturn(indicatorClientRect);
+ td.when(mockAdapter.getScrollPosition()).thenReturn(scrollPosition);
+ td.when(mockAdapter.getScrollContentWidth()).thenReturn(scrollContentWidth);
+ td.when(mockAdapter.getOffsetWidth()).thenReturn(offsetWidth);
+ td.when(mockAdapter.getTabDimensionsAtIndex(td.matchers.isA(Number))).thenDo((index) => {
+ return tabDimensionsMap[index];
+ });
+
+ return {foundation, mockAdapter};
+}
+
+test('#scrollIntoView() does nothing if the index overflows the tab list', () => {
+ const {foundation, mockAdapter} = setupScrollIntoViewTest({
+ tabListLength: 13,
+ });
+ foundation.scrollIntoView(13);
+ td.verify(mockAdapter.scrollTo(td.matchers.isA(Number)), {times: 0});
+ td.verify(mockAdapter.incrementScroll(td.matchers.isA(Number)), {times: 0});
+});
+
+test('#scrollIntoView() does nothing if the index underflows the tab list', () => {
+ const {foundation, mockAdapter} = setupScrollIntoViewTest({
+ tabListLength: 9,
+ });
+ foundation.scrollIntoView(-1);
+ td.verify(mockAdapter.scrollTo(td.matchers.isA(Number)), {times: 0});
+ td.verify(mockAdapter.incrementScroll(td.matchers.isA(Number)), {times: 0});
+});
+
+test('#scrollIntoView() scrolls to 0 if the index is 0', () => {
+ const {foundation, mockAdapter} = setupScrollIntoViewTest({
+ tabListLength: 9,
+ });
+ foundation.scrollIntoView(0);
+ td.verify(mockAdapter.scrollTo(0), {times: 1});
+});
+
+test('#scrollIntoView() scrolls to the scroll content width if the index is the max possible', () => {
+ const {foundation, mockAdapter} = setupScrollIntoViewTest({
+ tabListLength: 9,
+ scrollContentWidth: 987,
+ });
+ foundation.scrollIntoView(8);
+ td.verify(mockAdapter.scrollTo(987), {times: 1});
+});
+
+test('#scrollIntoView() increments the scroll by 150 when the selected tab is 100px to the right'
+ + ' and the closest tab\'s left content edge is 30px from its left root edge', () => {
+ const {foundation, mockAdapter} = setupScrollIntoViewTest({
+ activeIndex: 0,
+ tabListLength: 9,
+ scrollContentWidth: 1000,
+ offsetWidth: 200,
+ tabDimensionsMap: {
+ 1: {
+ rootLeft: 0,
+ rootRight: 300,
+ },
+ 2: {
+ rootLeft: 300,
+ contentLeft: 330,
+ contentRight: 370,
+ rootRight: 400,
+ },
+ },
+ });
+ foundation.scrollIntoView(1);
+ td.verify(mockAdapter.incrementScroll(130 + MDCTabBarFoundation.numbers.EXTRA_SCROLL_AMOUNT), {times: 1});
+});
+
+test('#scrollIntoView() increments the scroll by 250 when the selected tab is 100px to the left, is 100px wide,'
+ + ' and the closest tab\'s left content edge is 30px from its left root edge and the text direction is RTL', () => {
+ const {foundation, mockAdapter} = setupScrollIntoViewTest({
+ activeIndex: 0,
+ tabListLength: 9,
+ scrollContentWidth: 1000,
+ offsetWidth: 200,
+ scrollPosition: 100,
+ tabDimensionsMap: {
+ 5: {
+ rootLeft: 400,
+ contentLeft: 430,
+ contentRight: 470,
+ rootRight: 500,
+ },
+ 4: {
+ rootLeft: 500,
+ rootRight: 600,
+ },
+ },
+ });
+ td.when(mockAdapter.isRTL()).thenReturn(true);
+ foundation.scrollIntoView(4);
+ td.verify(mockAdapter.incrementScroll(230 + MDCTabBarFoundation.numbers.EXTRA_SCROLL_AMOUNT), {times: 1});
+});
+
+test('#scrollIntoView() increments the scroll by -250 when the selected tab is 100px to the left, is 100px wide,'
+ + ' and the closest tab\'s right content edge is 30px from its right root edge', () => {
+ const {foundation, mockAdapter} = setupScrollIntoViewTest({
+ activeIndex: 3,
+ tabListLength: 9,
+ scrollContentWidth: 1000,
+ scrollPosition: 500,
+ offsetWidth: 200,
+ tabDimensionsMap: {
+ 1: {
+ rootLeft: 190,
+ contentLeft: 220,
+ contentRight: 270,
+ rootRight: 300,
+ },
+ 2: {
+ rootLeft: 300,
+ contentLeft: 330,
+ contentRight: 370,
+ rootRight: 400,
+ },
+ },
+ });
+ foundation.scrollIntoView(2);
+ td.verify(mockAdapter.incrementScroll(-230 - MDCTabBarFoundation.numbers.EXTRA_SCROLL_AMOUNT), {times: 1});
+});
+
+test('#scrollIntoView() increments the scroll by -150 when the selected tab is 100px wide,'
+ + ' and the closest tab\'s right content edge is 30px from its right root edge and the text direction is RTL', () => {
+ const {foundation, mockAdapter} = setupScrollIntoViewTest({
+ activeIndex: 3,
+ tabListLength: 9,
+ scrollContentWidth: 1000,
+ scrollPosition: 300,
+ offsetWidth: 200,
+ tabDimensionsMap: {
+ 2: {
+ rootLeft: 700,
+ contentLeft: 730,
+ contentRight: 770,
+ rootRight: 800,
+ },
+ 1: {
+ rootLeft: 800,
+ contentLeft: 830,
+ contentRight: 870,
+ rootRight: 900,
+ },
+ },
+ });
+ td.when(mockAdapter.isRTL()).thenReturn(true);
+ foundation.scrollIntoView(2);
+ td.verify(mockAdapter.incrementScroll(-130 - MDCTabBarFoundation.numbers.EXTRA_SCROLL_AMOUNT), {times: 1});
+});
+
+test('#scrollIntoView() does nothing when the tab is perfectly in the center', () => {
+ const {foundation, mockAdapter} = setupScrollIntoViewTest({
+ activeIndex: 3,
+ tabListLength: 9,
+ scrollContentWidth: 1000,
+ scrollPosition: 200,
+ offsetWidth: 300,
+ tabDimensionsMap: {
+ 1: {
+ rootLeft: 200,
+ contentLeft: 230,
+ contentRight: 270,
+ rootRight: 300,
+ },
+ 2: {
+ rootLeft: 300,
+ contentLeft: 330,
+ contentRight: 370,
+ rootRight: 400,
+ },
+ },
+ });
+
+ foundation.scrollIntoView(2);
+ td.verify(mockAdapter.scrollTo(td.matchers.isA(Number)), {times: 0});
+ td.verify(mockAdapter.incrementScroll(td.matchers.isA(Number)), {times: 0});
+});
+
+test('#scrollIntoView() does nothing when the tab is perfectly in the center and the text direction is RTL', () => {
+ const {foundation, mockAdapter} = setupScrollIntoViewTest({
+ activeIndex: 3,
+ tabListLength: 10,
+ scrollContentWidth: 1000,
+ scrollPosition: 500,
+ offsetWidth: 300,
+ tabDimensionsMap: {
+ 8: {
+ rootLeft: 200,
+ contentLeft: 230,
+ contentRight: 270,
+ rootRight: 300,
+ },
+ 7: {
+ rootLeft: 300,
+ contentLeft: 330,
+ contentRight: 370,
+ rootRight: 400,
+ },
+ },
+ });
+ td.when(mockAdapter.isRTL()).thenReturn(true);
+ foundation.scrollIntoView(7);
+ td.verify(mockAdapter.scrollTo(td.matchers.isA(Number)), {times: 0});
+ td.verify(mockAdapter.incrementScroll(td.matchers.isA(Number)), {times: 0});
+});
+
+test('#scrollIntoView() increments the scroll by 0 when the tab and its left neighbor\'s content are visible', () => {
+ const {foundation, mockAdapter} = setupScrollIntoViewTest({
+ activeIndex: 3,
+ tabListLength: 9,
+ scrollContentWidth: 1000,
+ scrollPosition: 200,
+ offsetWidth: 500,
+ tabDimensionsMap: {
+ 1: {
+ rootLeft: 200,
+ contentLeft: 230,
+ contentRight: 270,
+ rootRight: 300,
+ },
+ 2: {
+ rootLeft: 300,
+ contentLeft: 330,
+ contentRight: 370,
+ rootRight: 400,
+ },
+ },
+ });
+
+ foundation.scrollIntoView(2);
+ td.verify(mockAdapter.incrementScroll(0), {times: 1});
+});
+
+test('#scrollIntoView() increments the scroll by 0 when the tab and its right neighbor\'s content are visible', () => {
+ const {foundation, mockAdapter} = setupScrollIntoViewTest({
+ activeIndex: 3,
+ tabListLength: 9,
+ scrollContentWidth: 1000,
+ scrollPosition: 22,
+ offsetWidth: 400,
+ tabDimensionsMap: {
+ 1: {
+ rootLeft: 200,
+ contentLeft: 230,
+ contentRight: 270,
+ rootRight: 300,
+ },
+ 2: {
+ rootLeft: 300,
+ contentLeft: 330,
+ contentRight: 370,
+ rootRight: 400,
+ },
+ },
+ });
+
+ foundation.scrollIntoView(1);
+ td.verify(mockAdapter.incrementScroll(0), {times: 1});
+});
diff --git a/test/unit/mdc-tab-bar/mdc-tab-bar.test.js b/test/unit/mdc-tab-bar/mdc-tab-bar.test.js
new file mode 100644
index 00000000000..ac323c8a7ad
--- /dev/null
+++ b/test/unit/mdc-tab-bar/mdc-tab-bar.test.js
@@ -0,0 +1,225 @@
+/**
+ * @license
+ * Copyright 2018 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+import bel from 'bel';
+import {assert} from 'chai';
+import td from 'testdouble';
+import domEvents from 'dom-events';
+
+import {MDCTabBar, MDCTabBarFoundation} from '../../../packages/mdc-tab-bar';
+import {MDCTabFoundation} from '../../../packages/mdc-tab';
+
+const getFixture = () => bel`
+