From 409a6a6bffe8903f810f3b74d165939b90e18425 Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Fri, 15 Feb 2019 10:58:53 -0800 Subject: [PATCH] feat(tabs): Convert JS to TypeScript (#4412) Refs #4225 --- packages/mdc-tabs/{index.js => index.ts} | 6 +- packages/mdc-tabs/tab-bar-scroller/adapter.ts | 55 ++++++ .../{index.js => component.ts} | 71 +++---- .../{constants.js => constants.ts} | 6 +- .../{foundation.js => foundation.ts} | 186 ++++++++++-------- packages/mdc-tabs/tab-bar-scroller/index.ts | 26 +++ packages/mdc-tabs/tab-bar/adapter.ts | 48 +++++ .../tab-bar/{index.js => component.ts} | 54 ++--- .../tab-bar/{constants.js => constants.ts} | 4 +- .../tab-bar/{foundation.js => foundation.ts} | 150 +++++++------- packages/mdc-tabs/tab-bar/index.ts | 27 +++ packages/mdc-tabs/tab-bar/types.ts | 28 +++ packages/mdc-tabs/tab/adapter.ts | 36 ++++ .../mdc-tabs/tab/{index.js => component.ts} | 26 ++- .../tab/{constants.js => constants.ts} | 0 .../tab/{foundation.js => foundation.ts} | 45 +++-- packages/mdc-tabs/tab/index.ts | 26 +++ scripts/webpack/js-bundle-factory.js | 2 +- .../mdc-tabs/mdc-tab-bar-foundation.test.js | 1 + 19 files changed, 546 insertions(+), 251 deletions(-) rename packages/mdc-tabs/{index.js => index.ts} (85%) create mode 100644 packages/mdc-tabs/tab-bar-scroller/adapter.ts rename packages/mdc-tabs/tab-bar-scroller/{index.js => component.ts} (53%) rename packages/mdc-tabs/tab-bar-scroller/{constants.js => constants.ts} (100%) rename packages/mdc-tabs/tab-bar-scroller/{foundation.js => foundation.ts} (69%) create mode 100644 packages/mdc-tabs/tab-bar-scroller/index.ts create mode 100644 packages/mdc-tabs/tab-bar/adapter.ts rename packages/mdc-tabs/tab-bar/{index.js => component.ts} (68%) rename packages/mdc-tabs/tab-bar/{constants.js => constants.ts} (100%) rename packages/mdc-tabs/tab-bar/{foundation.js => foundation.ts} (71%) create mode 100644 packages/mdc-tabs/tab-bar/index.ts create mode 100644 packages/mdc-tabs/tab-bar/types.ts create mode 100644 packages/mdc-tabs/tab/adapter.ts rename packages/mdc-tabs/tab/{index.js => component.ts} (78%) rename packages/mdc-tabs/tab/{constants.js => constants.ts} (100%) rename packages/mdc-tabs/tab/{foundation.js => foundation.ts} (69%) create mode 100644 packages/mdc-tabs/tab/index.ts diff --git a/packages/mdc-tabs/index.js b/packages/mdc-tabs/index.ts similarity index 85% rename from packages/mdc-tabs/index.js rename to packages/mdc-tabs/index.ts index e2ac803e59c..1752cfa1d60 100644 --- a/packages/mdc-tabs/index.js +++ b/packages/mdc-tabs/index.ts @@ -21,6 +21,6 @@ * THE SOFTWARE. */ -export {MDCTabFoundation, MDCTab} from './tab'; -export {MDCTabBarFoundation, MDCTabBar} from './tab-bar'; -export {MDCTabBarScrollerFoundation, MDCTabBarScroller} from './tab-bar-scroller'; +export * from './tab'; +export * from './tab-bar'; +export * from './tab-bar-scroller'; diff --git a/packages/mdc-tabs/tab-bar-scroller/adapter.ts b/packages/mdc-tabs/tab-bar-scroller/adapter.ts new file mode 100644 index 00000000000..428d5301acd --- /dev/null +++ b/packages/mdc-tabs/tab-bar-scroller/adapter.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright 2019 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {EventType, SpecificEventListener} from '@material/base/types'; + +export interface MDCTabBarScrollerAdapter { + addClass: (className: string) => void; + removeClass: (className: string) => void; + eventTargetHasClass: (target: Element, className: string) => boolean; + addClassToForwardIndicator: (className: string) => void; + removeClassFromForwardIndicator: (className: string) => void; + addClassToBackIndicator: (className: string) => void; + removeClassFromBackIndicator: (className: string) => void; + isRTL: () => boolean; + registerBackIndicatorClickHandler: (handler: SpecificEventListener<'click'>) => void; + deregisterBackIndicatorClickHandler: (handler: SpecificEventListener<'click'>) => void; + registerForwardIndicatorClickHandler: (handler: SpecificEventListener<'click'>) => void; + deregisterForwardIndicatorClickHandler: (handler: SpecificEventListener<'click'>) => void; + registerCapturedInteractionHandler: (evt: K, handler: SpecificEventListener) => void; + deregisterCapturedInteractionHandler: (evt: K, handler: SpecificEventListener) => void; + registerWindowResizeHandler: (handler: SpecificEventListener<'resize'>) => void; + deregisterWindowResizeHandler: (handler: SpecificEventListener<'resize'>) => void; + getNumberOfTabs: () => number; + getComputedWidthForTabAtIndex: (index: number) => number; + getComputedLeftForTabAtIndex: (index: number) => number; + getOffsetWidthForScrollFrame: () => number; + getScrollLeftForScrollFrame: () => number; + setScrollLeftForScrollFrame: (scrollLeftAmount: number) => void; + getOffsetWidthForTabBar: () => number; + setTransformStyleForTabBar: (value: string) => void; + getOffsetLeftForEventTarget: (target: HTMLElement) => number; + getOffsetWidthForEventTarget: (target: HTMLElement) => number; +} + +export default MDCTabBarScrollerAdapter; diff --git a/packages/mdc-tabs/tab-bar-scroller/index.js b/packages/mdc-tabs/tab-bar-scroller/component.ts similarity index 53% rename from packages/mdc-tabs/tab-bar-scroller/index.js rename to packages/mdc-tabs/tab-bar-scroller/component.ts index eb59306eaac..5ebb9719d49 100644 --- a/packages/mdc-tabs/tab-bar-scroller/index.js +++ b/packages/mdc-tabs/tab-bar-scroller/component.ts @@ -21,16 +21,14 @@ * THE SOFTWARE. */ -import {getCorrectPropertyName} from '@material/animation/index.ts'; +import {getCorrectPropertyName} from '@material/animation/index'; import {MDCComponent} from '@material/base/component'; +import {MDCTabBar, MDCTabBarFactory} from '../tab-bar/index'; +import {MDCTabBarScrollerAdapter} from './adapter'; +import {MDCTabBarScrollerFoundation} from './foundation'; -import {MDCTabBar} from '../tab-bar/index'; -import MDCTabBarScrollerFoundation from './foundation'; - -export {MDCTabBarScrollerFoundation}; - -export class MDCTabBarScroller extends MDCComponent { - static attachTo(root) { +export class MDCTabBarScroller extends MDCComponent { + static attachTo(root: Element) { return new MDCTabBarScroller(root); } @@ -38,16 +36,30 @@ export class MDCTabBarScroller extends MDCComponent { return this.tabBar_; } - initialize(tabBarFactory = (root) => new MDCTabBar(root)) { - this.scrollFrame_ = this.root_.querySelector(MDCTabBarScrollerFoundation.strings.FRAME_SELECTOR); - this.tabBarEl_ = this.root_.querySelector(MDCTabBarScrollerFoundation.strings.TABS_SELECTOR); - this.forwardIndicator_ = this.root_.querySelector(MDCTabBarScrollerFoundation.strings.INDICATOR_FORWARD_SELECTOR); - this.backIndicator_ = this.root_.querySelector(MDCTabBarScrollerFoundation.strings.INDICATOR_BACK_SELECTOR); + protected root_!: HTMLElement; // assigned in MDCComponent constructor + + private tabBar_!: MDCTabBar; // assigned in initialize() + private scrollFrame_!: HTMLElement; // assigned in initialize() + private tabBarEl_!: HTMLElement; // assigned in initialize() + private forwardIndicator_!: HTMLElement; // assigned in initialize() + private backIndicator_!: HTMLElement; // assigned in initialize() + + initialize(tabBarFactory: MDCTabBarFactory = (el) => new MDCTabBar(el)) { + this.scrollFrame_ = + this.root_.querySelector(MDCTabBarScrollerFoundation.strings.FRAME_SELECTOR)!; + this.tabBarEl_ = + this.root_.querySelector(MDCTabBarScrollerFoundation.strings.TABS_SELECTOR)!; + this.forwardIndicator_ = + this.root_.querySelector(MDCTabBarScrollerFoundation.strings.INDICATOR_FORWARD_SELECTOR)!; + this.backIndicator_ = + this.root_.querySelector(MDCTabBarScrollerFoundation.strings.INDICATOR_BACK_SELECTOR)!; + this.tabBar_ = tabBarFactory(this.tabBarEl_); } getDefaultFoundation() { - return new MDCTabBarScrollerFoundation({ + // tslint:disable:object-literal-sort-keys + const adapter: MDCTabBarScrollerAdapter = { addClass: (className) => this.root_.classList.add(className), removeClass: (className) => this.root_.classList.remove(className), eventTargetHasClass: (target, className) => target.classList.contains(className), @@ -55,24 +67,15 @@ export class MDCTabBarScroller extends MDCComponent { removeClassFromForwardIndicator: (className) => this.forwardIndicator_.classList.remove(className), addClassToBackIndicator: (className) => this.backIndicator_.classList.add(className), removeClassFromBackIndicator: (className) => this.backIndicator_.classList.remove(className), - isRTL: () => - getComputedStyle(this.root_).getPropertyValue('direction') === 'rtl', - registerBackIndicatorClickHandler: (handler) => - this.backIndicator_.addEventListener('click', handler), - deregisterBackIndicatorClickHandler: (handler) => - this.backIndicator_.removeEventListener('click', handler), - registerForwardIndicatorClickHandler: (handler) => - this.forwardIndicator_.addEventListener('click', handler), - deregisterForwardIndicatorClickHandler: (handler) => - this.forwardIndicator_.removeEventListener('click', handler), - registerCapturedInteractionHandler: (evt, handler) => - this.root_.addEventListener(evt, handler, true), - deregisterCapturedInteractionHandler: (evt, handler) => - this.root_.removeEventListener(evt, handler, true), - registerWindowResizeHandler: (handler) => - window.addEventListener('resize', handler), - deregisterWindowResizeHandler: (handler) => - window.removeEventListener('resize', handler), + isRTL: () => getComputedStyle(this.root_).getPropertyValue('direction') === 'rtl', + registerBackIndicatorClickHandler: (handler) => this.backIndicator_.addEventListener('click', handler), + deregisterBackIndicatorClickHandler: (handler) => this.backIndicator_.removeEventListener('click', handler), + registerForwardIndicatorClickHandler: (handler) => this.forwardIndicator_.addEventListener('click', handler), + deregisterForwardIndicatorClickHandler: (handler) => this.forwardIndicator_.removeEventListener('click', handler), + registerCapturedInteractionHandler: (evt, handler) => this.root_.addEventListener(evt, handler, true), + deregisterCapturedInteractionHandler: (evt, handler) => this.root_.removeEventListener(evt, handler, true), + registerWindowResizeHandler: (handler) => window.addEventListener('resize', handler), + deregisterWindowResizeHandler: (handler) => window.removeEventListener('resize', handler), getNumberOfTabs: () => this.tabBar.tabs.length, getComputedWidthForTabAtIndex: (index) => this.tabBar.tabs[index].computedWidth, getComputedLeftForTabAtIndex: (index) => this.tabBar.tabs[index].computedLeft, @@ -85,7 +88,9 @@ export class MDCTabBarScroller extends MDCComponent { }, getOffsetLeftForEventTarget: (target) => target.offsetLeft, getOffsetWidthForEventTarget: (target) => target.offsetWidth, - }); + }; + // tslint:enable:object-literal-sort-keys + return new MDCTabBarScrollerFoundation(adapter); } layout() { diff --git a/packages/mdc-tabs/tab-bar-scroller/constants.js b/packages/mdc-tabs/tab-bar-scroller/constants.ts similarity index 100% rename from packages/mdc-tabs/tab-bar-scroller/constants.js rename to packages/mdc-tabs/tab-bar-scroller/constants.ts index d3e45b669a6..6f8f45dba9e 100644 --- a/packages/mdc-tabs/tab-bar-scroller/constants.js +++ b/packages/mdc-tabs/tab-bar-scroller/constants.ts @@ -22,16 +22,16 @@ */ export const cssClasses = { - INDICATOR_FORWARD: 'mdc-tab-bar-scroller__indicator--forward', INDICATOR_BACK: 'mdc-tab-bar-scroller__indicator--back', INDICATOR_ENABLED: 'mdc-tab-bar-scroller__indicator--enabled', + INDICATOR_FORWARD: 'mdc-tab-bar-scroller__indicator--forward', TAB: 'mdc-tab', }; export const strings = { FRAME_SELECTOR: '.mdc-tab-bar-scroller__scroll-frame', + INDICATOR_BACK_SELECTOR: '.mdc-tab-bar-scroller__indicator--back', + INDICATOR_FORWARD_SELECTOR: '.mdc-tab-bar-scroller__indicator--forward', TABS_SELECTOR: '.mdc-tab-bar-scroller__scroll-frame__tabs', TAB_SELECTOR: '.mdc-tab', - INDICATOR_FORWARD_SELECTOR: '.mdc-tab-bar-scroller__indicator--forward', - INDICATOR_BACK_SELECTOR: '.mdc-tab-bar-scroller__indicator--back', }; diff --git a/packages/mdc-tabs/tab-bar-scroller/foundation.js b/packages/mdc-tabs/tab-bar-scroller/foundation.ts similarity index 69% rename from packages/mdc-tabs/tab-bar-scroller/foundation.js rename to packages/mdc-tabs/tab-bar-scroller/foundation.ts index 0ab4dd07814..9922a162a4b 100644 --- a/packages/mdc-tabs/tab-bar-scroller/foundation.js +++ b/packages/mdc-tabs/tab-bar-scroller/foundation.ts @@ -21,12 +21,16 @@ * THE SOFTWARE. */ - +import {SpecificEventListener} from '@material/base'; import {MDCFoundation} from '@material/base/foundation'; - +import {MDCTabBarScrollerAdapter} from './adapter'; import {cssClasses, strings} from './constants'; -export default class MDCTabBarScrollerFoundation extends MDCFoundation { +export type InteractionEventType = 'touchstart' | 'mousedown' | 'focus'; + +const INTERACTION_EVENTS: InteractionEventType[] = ['touchstart', 'mousedown', 'focus']; + +export class MDCTabBarScrollerFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -35,55 +39,62 @@ export default class MDCTabBarScrollerFoundation extends MDCFoundation { return strings; } - static get defaultAdapter() { + static get defaultAdapter(): MDCTabBarScrollerAdapter { + // tslint:disable:object-literal-sort-keys return { - addClass: (/* className: string */) => {}, - removeClass: (/* className: string */) => {}, - eventTargetHasClass: (/* target: EventTarget, className: string */) => /* boolean */ false, - addClassToForwardIndicator: (/* className: string */) => {}, - removeClassFromForwardIndicator: (/* className: string */) => {}, - addClassToBackIndicator: (/* className: string */) => {}, - removeClassFromBackIndicator: (/* className: string */) => {}, - isRTL: () => /* boolean */ false, - registerBackIndicatorClickHandler: (/* handler: EventListener */) => {}, - deregisterBackIndicatorClickHandler: (/* handler: EventListener */) => {}, - registerForwardIndicatorClickHandler: (/* handler: EventListener */) => {}, - deregisterForwardIndicatorClickHandler: (/* handler: EventListener */) => {}, - registerCapturedInteractionHandler: (/* evt: string, handler: EventListener */) => {}, - deregisterCapturedInteractionHandler: (/* evt: string, handler: EventListener */) => {}, - registerWindowResizeHandler: (/* handler: EventListener */) => {}, - deregisterWindowResizeHandler: (/* handler: EventListener */) => {}, - getNumberOfTabs: () => /* number */ 0, - getComputedWidthForTabAtIndex: () => /* number */ 0, - getComputedLeftForTabAtIndex: () => /* number */ 0, - getOffsetWidthForScrollFrame: () => /* number */ 0, - getScrollLeftForScrollFrame: () => /* number */ 0, - setScrollLeftForScrollFrame: (/* scrollLeftAmount: number */) => {}, - getOffsetWidthForTabBar: () => /* number */ 0, - setTransformStyleForTabBar: (/* value: string */) => {}, - getOffsetLeftForEventTarget: (/* target: EventTarget */) => /* number */ 0, - getOffsetWidthForEventTarget: (/* target: EventTarget */) => /* number */ 0, + addClass: () => undefined, + removeClass: () => undefined, + eventTargetHasClass: () => false, + addClassToForwardIndicator: () => undefined, + removeClassFromForwardIndicator: () => undefined, + addClassToBackIndicator: () => undefined, + removeClassFromBackIndicator: () => undefined, + isRTL: () => false, + registerBackIndicatorClickHandler: () => undefined, + deregisterBackIndicatorClickHandler: () => undefined, + registerForwardIndicatorClickHandler: () => undefined, + deregisterForwardIndicatorClickHandler: () => undefined, + registerCapturedInteractionHandler: () => undefined, + deregisterCapturedInteractionHandler: () => undefined, + registerWindowResizeHandler: () => undefined, + deregisterWindowResizeHandler: () => undefined, + getNumberOfTabs: () => 0, + getComputedWidthForTabAtIndex: () => 0, + getComputedLeftForTabAtIndex: () => 0, + getOffsetWidthForScrollFrame: () => 0, + getScrollLeftForScrollFrame: () => 0, + setScrollLeftForScrollFrame: () => undefined, + getOffsetWidthForTabBar: () => 0, + setTransformStyleForTabBar: () => undefined, + getOffsetLeftForEventTarget: () => 0, + getOffsetWidthForEventTarget: () => 0, }; + // tslint:enable:object-literal-sort-keys } - constructor(adapter) { - super(Object.assign(MDCTabBarScrollerFoundation.defaultAdapter, adapter)); + private pointerDownRecognized_ = false; + private currentTranslateOffset_ = 0; + private focusedTarget_: HTMLElement | null = null; + private layoutFrame_ = 0; + private scrollFrameScrollLeft_ = 0; + + private readonly forwardIndicatorClickHandler_: SpecificEventListener<'click'>; + private readonly backIndicatorClickHandler_: SpecificEventListener<'click'>; + private readonly resizeHandler_: SpecificEventListener<'resize'>; + private readonly interactionHandler_: SpecificEventListener; + + constructor(adapter?: Partial) { + super({...MDCTabBarScrollerFoundation.defaultAdapter, ...adapter}); - this.pointerDownRecognized_ = false; - this.currentTranslateOffset_ = 0; - this.focusedTarget_ = null; - this.layoutFrame_ = 0; - this.scrollFrameScrollLeft_ = 0; this.forwardIndicatorClickHandler_ = (evt) => this.scrollForward(evt); this.backIndicatorClickHandler_ = (evt) => this.scrollBack(evt); this.resizeHandler_ = () => this.layout(); this.interactionHandler_ = (evt) => { - if (evt.type == 'touchstart' || evt.type == 'mousedown') { + if (evt.type === 'touchstart' || evt.type === 'mousedown') { this.pointerDownRecognized_ = true; } this.handlePossibleTabKeyboardFocus_(evt); - - if (evt.type == 'focus') { + if (evt.type === 'focus') { this.pointerDownRecognized_ = false; } }; @@ -93,7 +104,7 @@ export default class MDCTabBarScrollerFoundation extends MDCFoundation { this.adapter_.registerBackIndicatorClickHandler(this.backIndicatorClickHandler_); this.adapter_.registerForwardIndicatorClickHandler(this.forwardIndicatorClickHandler_); this.adapter_.registerWindowResizeHandler(this.resizeHandler_); - ['touchstart', 'mousedown', 'focus'].forEach((evtType) => { + INTERACTION_EVENTS.forEach((evtType) => { this.adapter_.registerCapturedInteractionHandler(evtType, this.interactionHandler_); }); this.layout(); @@ -103,12 +114,12 @@ export default class MDCTabBarScrollerFoundation extends MDCFoundation { this.adapter_.deregisterBackIndicatorClickHandler(this.backIndicatorClickHandler_); this.adapter_.deregisterForwardIndicatorClickHandler(this.forwardIndicatorClickHandler_); this.adapter_.deregisterWindowResizeHandler(this.resizeHandler_); - ['touchstart', 'mousedown', 'focus'].forEach((evtType) => { + INTERACTION_EVENTS.forEach((evtType) => { this.adapter_.deregisterCapturedInteractionHandler(evtType, this.interactionHandler_); }); } - scrollBack(evt = null) { + scrollBack(evt?: MouseEvent) { if (evt) { evt.preventDefault(); } @@ -141,7 +152,7 @@ export default class MDCTabBarScrollerFoundation extends MDCFoundation { this.scrollToTabAtIndex(scrollTargetIndex); } - scrollForward(evt = null) { + scrollForward(evt?: MouseEvent) { if (evt) { evt.preventDefault(); } @@ -157,8 +168,6 @@ export default class MDCTabBarScrollerFoundation extends MDCFoundation { if (this.isRTL_()) { const frameOffsetAndTabWidth = scrollFrameOffsetWidth - this.adapter_.getComputedWidthForTabAtIndex(i); - const tabOffsetLeftAndWidth = - this.adapter_.getComputedLeftForTabAtIndex(i) + this.adapter_.getComputedWidthForTabAtIndex(i); const tabRightOffset = this.adapter_.getOffsetWidthForTabBar() - tabOffsetLeftAndWidth; @@ -180,19 +189,48 @@ export default class MDCTabBarScrollerFoundation extends MDCFoundation { this.layoutFrame_ = requestAnimationFrame(() => this.layout_()); } - isRTL_() { - return this.adapter_.isRTL(); + scrollToTabAtIndex(index: number) { + const scrollTargetOffsetLeft = this.adapter_.getComputedLeftForTabAtIndex(index); + const scrollTargetOffsetWidth = this.adapter_.getComputedWidthForTabAtIndex(index); + + this.currentTranslateOffset_ = + this.normalizeForRTL_(scrollTargetOffsetLeft, scrollTargetOffsetWidth); + + requestAnimationFrame(() => this.shiftFrame_()); } - handlePossibleTabKeyboardFocus_(evt) { - if (!this.adapter_.eventTargetHasClass(evt.target, cssClasses.TAB) || this.pointerDownRecognized_) { + private layout_() { + const frameWidth = this.adapter_.getOffsetWidthForScrollFrame(); + const isOverflowing = this.adapter_.getOffsetWidthForTabBar() > frameWidth; + + if (!isOverflowing) { + this.currentTranslateOffset_ = 0; + } + + this.shiftFrame_(); + this.updateIndicatorEnabledStates_(); + } + + private shiftFrame_() { + const shiftAmount = this.isRTL_() ? + this.currentTranslateOffset_ : -this.currentTranslateOffset_; + + this.adapter_.setTransformStyleForTabBar(`translateX(${shiftAmount}px)`); + this.updateIndicatorEnabledStates_(); + } + + private handlePossibleTabKeyboardFocus_(evt: MouseEvent | TouchEvent | FocusEvent) { + const target = evt.target as HTMLElement; + + if (!this.adapter_.eventTargetHasClass(target, cssClasses.TAB) || this.pointerDownRecognized_) { return; } const resetAmt = this.isRTL_() ? this.scrollFrameScrollLeft_ : 0; this.adapter_.setScrollLeftForScrollFrame(resetAmt); - this.focusedTarget_ = evt.target; + this.focusedTarget_ = target; + const scrollFrameWidth = this.adapter_.getOffsetWidthForScrollFrame(); const tabBarWidth = this.adapter_.getOffsetWidthForTabBar(); const leftEdge = this.adapter_.getOffsetLeftForEventTarget(this.focusedTarget_); @@ -216,41 +254,7 @@ export default class MDCTabBarScrollerFoundation extends MDCFoundation { this.pointerDownRecognized_ = false; } - layout_() { - const frameWidth = this.adapter_.getOffsetWidthForScrollFrame(); - const isOverflowing = this.adapter_.getOffsetWidthForTabBar() > frameWidth; - - if (!isOverflowing) { - this.currentTranslateOffset_ = 0; - } - - this.shiftFrame_(); - this.updateIndicatorEnabledStates_(); - } - - scrollToTabAtIndex(index) { - const scrollTargetOffsetLeft = this.adapter_.getComputedLeftForTabAtIndex(index); - const scrollTargetOffsetWidth = this.adapter_.getComputedWidthForTabAtIndex(index); - - this.currentTranslateOffset_ = - this.normalizeForRTL_(scrollTargetOffsetLeft, scrollTargetOffsetWidth); - - requestAnimationFrame(() => this.shiftFrame_()); - } - - normalizeForRTL_(left, width) { - return this.isRTL_() ? this.adapter_.getOffsetWidthForTabBar() - (left + width) : left; - } - - shiftFrame_() { - const shiftAmount = this.isRTL_() ? - this.currentTranslateOffset_ : -this.currentTranslateOffset_; - - this.adapter_.setTransformStyleForTabBar(`translateX(${shiftAmount}px)`); - this.updateIndicatorEnabledStates_(); - } - - updateIndicatorEnabledStates_() { + private updateIndicatorEnabledStates_() { const {INDICATOR_ENABLED} = cssClasses; if (this.currentTranslateOffset_ === 0) { this.adapter_.removeClassFromBackIndicator(INDICATOR_ENABLED); @@ -265,4 +269,14 @@ export default class MDCTabBarScrollerFoundation extends MDCFoundation { this.adapter_.removeClassFromForwardIndicator(INDICATOR_ENABLED); } } + + private normalizeForRTL_(left: number, width: number) { + return this.isRTL_() ? this.adapter_.getOffsetWidthForTabBar() - (left + width) : left; + } + + private isRTL_() { + return this.adapter_.isRTL(); + } } + +export default MDCTabBarScrollerFoundation; diff --git a/packages/mdc-tabs/tab-bar-scroller/index.ts b/packages/mdc-tabs/tab-bar-scroller/index.ts new file mode 100644 index 00000000000..f8c89ac94f3 --- /dev/null +++ b/packages/mdc-tabs/tab-bar-scroller/index.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2019 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +export * from './adapter'; +export * from './component'; +export * from './foundation'; diff --git a/packages/mdc-tabs/tab-bar/adapter.ts b/packages/mdc-tabs/tab-bar/adapter.ts new file mode 100644 index 00000000000..522a4bdf2c0 --- /dev/null +++ b/packages/mdc-tabs/tab-bar/adapter.ts @@ -0,0 +1,48 @@ +/** + * @license + * Copyright 2019 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {SpecificEventListener} from '@material/base/types'; +import {MDCTabBarEventDetail} from './types'; + +export interface MDCTabBarAdapter { + addClass: (className: string) => void; + removeClass: (className: string) => void; + bindOnMDCTabSelectedEvent: () => void; + unbindOnMDCTabSelectedEvent: () => void; + registerResizeHandler: (handler: SpecificEventListener<'resize'>) => void; + deregisterResizeHandler: (handler: SpecificEventListener<'resize'>) => void; + getOffsetWidth: () => number; + setStyleForIndicator: (propertyName: string, value: string) => void; + getOffsetWidthForIndicator: () => number; + notifyChange: (evtData: MDCTabBarEventDetail) => void; + getNumberOfTabs: () => number; + isTabActiveAtIndex: (index: number) => boolean; + setTabActiveAtIndex: (index: number, isActive: boolean) => void; + isDefaultPreventedOnClickForTabAtIndex: (index: number) => boolean; + setPreventDefaultOnClickForTabAtIndex: (index: number, preventDefaultOnClick: boolean) => void; + measureTabAtIndex: (index: number) => void; + getComputedWidthForTabAtIndex: (index: number) => number; + getComputedLeftForTabAtIndex: (index: number) => number; +} + +export default MDCTabBarAdapter; diff --git a/packages/mdc-tabs/tab-bar/index.js b/packages/mdc-tabs/tab-bar/component.ts similarity index 68% rename from packages/mdc-tabs/tab-bar/index.js rename to packages/mdc-tabs/tab-bar/component.ts index 69407692b38..8ff92804580 100644 --- a/packages/mdc-tabs/tab-bar/index.js +++ b/packages/mdc-tabs/tab-bar/component.ts @@ -22,14 +22,14 @@ */ import {MDCComponent} from '@material/base/component'; +import {MDCTab, MDCTabFactory, MDCTabFoundation, MDCTabSelectedEvent} from '../tab/index'; +import {MDCTabBarAdapter} from './adapter'; +import {MDCTabBarFoundation} from './foundation'; -import {MDCTab, MDCTabFoundation} from '../tab/index'; -import MDCTabBarFoundation from './foundation'; +export type MDCTabBarFactory = (el: Element) => MDCTabBar; -export {MDCTabBarFoundation}; - -export class MDCTabBar extends MDCComponent { - static attachTo(root) { +export class MDCTabBar extends MDCComponent { + static attachTo(root: Element) { return new MDCTabBar(root); } @@ -54,8 +54,14 @@ export class MDCTabBar extends MDCComponent { this.setActiveTabIndex_(index, false); } - initialize(tabFactory = (el) => new MDCTab(el)) { - this.indicator_ = this.root_.querySelector(MDCTabBarFoundation.strings.INDICATOR_SELECTOR); + protected root_!: HTMLElement; // assigned in MDCComponent constructor + + private tabs_!: MDCTab[]; // assigned in initialize() + private indicator_!: HTMLElement; // assigned in initialize() + private tabSelectedHandler_!: (evt: MDCTabSelectedEvent) => void; // assigned in initialize() + + initialize(tabFactory: MDCTabFactory = (el) => new MDCTab(el)) { + this.indicator_ = this.root_.querySelector(MDCTabBarFoundation.strings.INDICATOR_SELECTOR)!; this.tabs_ = this.gatherTabs_(tabFactory); this.tabSelectedHandler_ = ({detail}) => { const {tab} = detail; @@ -64,13 +70,14 @@ export class MDCTabBar extends MDCComponent { } getDefaultFoundation() { - return new MDCTabBarFoundation({ + // tslint:disable:object-literal-sort-keys + const adapter: MDCTabBarAdapter = { addClass: (className) => this.root_.classList.add(className), removeClass: (className) => this.root_.classList.remove(className), - bindOnMDCTabSelectedEvent: () => this.listen( - MDCTabFoundation.strings.SELECTED_EVENT, this.tabSelectedHandler_), - unbindOnMDCTabSelectedEvent: () => this.unlisten( - MDCTabFoundation.strings.SELECTED_EVENT, this.tabSelectedHandler_), + bindOnMDCTabSelectedEvent: () => + this.listen(MDCTabFoundation.strings.SELECTED_EVENT, this.tabSelectedHandler_), + unbindOnMDCTabSelectedEvent: () => + this.unlisten(MDCTabFoundation.strings.SELECTED_EVENT, this.tabSelectedHandler_), registerResizeHandler: (handler) => window.addEventListener('resize', handler), deregisterResizeHandler: (handler) => window.removeEventListener('resize', handler), getOffsetWidth: () => this.root_.offsetWidth, @@ -89,23 +96,26 @@ export class MDCTabBar extends MDCComponent { measureTabAtIndex: (index) => this.tabs[index].measureSelf(), getComputedWidthForTabAtIndex: (index) => this.tabs[index].computedWidth, getComputedLeftForTabAtIndex: (index) => this.tabs[index].computedLeft, - }); + }; + // tslint:enable:object-literal-sort-keys + return new MDCTabBarFoundation(adapter); } - gatherTabs_(tabFactory) { - const tabElements = [].slice.call(this.root_.querySelectorAll(MDCTabBarFoundation.strings.TAB_SELECTOR)); - return tabElements.map((el) => tabFactory(el)); + layout() { + this.foundation_.layout(); } - setActiveTabIndex_(activeTabIndex, notifyChange) { - this.foundation_.switchToTabAtIndex(activeTabIndex, notifyChange); + private gatherTabs_(tabFactory: MDCTabFactory): MDCTab[] { + const tabElements: HTMLElement[] = + [].slice.call(this.root_.querySelectorAll(MDCTabBarFoundation.strings.TAB_SELECTOR)); + return tabElements.map((el: Element) => tabFactory(el)); } - layout() { - this.foundation_.layout(); + private setActiveTabIndex_(activeTabIndex: number, notifyChange: boolean) { + this.foundation_.switchToTabAtIndex(activeTabIndex, notifyChange); } - setActiveTab_(activeTab, notifyChange) { + private setActiveTab_(activeTab: MDCTab, notifyChange: boolean) { const indexOfTab = this.tabs.indexOf(activeTab); if (indexOfTab < 0) { throw new Error('Invalid tab component given as activeTab: Tab not found within this component\'s tab list'); diff --git a/packages/mdc-tabs/tab-bar/constants.js b/packages/mdc-tabs/tab-bar/constants.ts similarity index 100% rename from packages/mdc-tabs/tab-bar/constants.js rename to packages/mdc-tabs/tab-bar/constants.ts index 4e11d01f518..c06ac9278a2 100644 --- a/packages/mdc-tabs/tab-bar/constants.js +++ b/packages/mdc-tabs/tab-bar/constants.ts @@ -26,7 +26,7 @@ export const cssClasses = { }; export const strings = { - TAB_SELECTOR: '.mdc-tab', - INDICATOR_SELECTOR: '.mdc-tab-bar__indicator', CHANGE_EVENT: 'MDCTabBar:change', + INDICATOR_SELECTOR: '.mdc-tab-bar__indicator', + TAB_SELECTOR: '.mdc-tab', }; diff --git a/packages/mdc-tabs/tab-bar/foundation.js b/packages/mdc-tabs/tab-bar/foundation.ts similarity index 71% rename from packages/mdc-tabs/tab-bar/foundation.js rename to packages/mdc-tabs/tab-bar/foundation.ts index 354f9370b42..4acae3fbe60 100644 --- a/packages/mdc-tabs/tab-bar/foundation.js +++ b/packages/mdc-tabs/tab-bar/foundation.ts @@ -21,12 +21,13 @@ * THE SOFTWARE. */ +import {getCorrectPropertyName} from '@material/animation/index'; +import {SpecificEventListener} from '@material/base'; import {MDCFoundation} from '@material/base/foundation'; -import {getCorrectPropertyName} from '@material/animation/index.ts'; - +import {MDCTabBarAdapter} from './adapter'; import {cssClasses, strings} from './constants'; -export default class MDCTabBarFoundation extends MDCFoundation { +export class MDCTabBarFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -35,41 +36,43 @@ export default class MDCTabBarFoundation extends MDCFoundation { return strings; } - static get defaultAdapter() { + static get defaultAdapter(): MDCTabBarAdapter { + // tslint:disable:object-literal-sort-keys return { - addClass: (/* className: string */) => {}, - removeClass: (/* className: string */) => {}, - bindOnMDCTabSelectedEvent: () => {}, - unbindOnMDCTabSelectedEvent: () => {}, - registerResizeHandler: (/* handler: EventListener */) => {}, - deregisterResizeHandler: (/* handler: EventListener */) => {}, - getOffsetWidth: () => /* number */ 0, - setStyleForIndicator: (/* propertyName: string, value: string */) => {}, - getOffsetWidthForIndicator: () => /* number */ 0, - notifyChange: (/* evtData: {activeTabIndex: number} */) => {}, - getNumberOfTabs: () => /* number */ 0, - isTabActiveAtIndex: (/* index: number */) => /* boolean */ false, - setTabActiveAtIndex: (/* index: number, isActive: true */) => {}, - isDefaultPreventedOnClickForTabAtIndex: (/* index: number */) => /* boolean */ false, - setPreventDefaultOnClickForTabAtIndex: (/* index: number, preventDefaultOnClick: boolean */) => {}, - measureTabAtIndex: (/* index: number */) => {}, - getComputedWidthForTabAtIndex: (/* index: number */) => /* number */ 0, - getComputedLeftForTabAtIndex: (/* index: number */) => /* number */ 0, + addClass: () => undefined, + removeClass: () => undefined, + bindOnMDCTabSelectedEvent: () => undefined, + unbindOnMDCTabSelectedEvent: () => undefined, + registerResizeHandler: () => undefined, + deregisterResizeHandler: () => undefined, + getOffsetWidth: () => 0, + setStyleForIndicator: () => undefined, + getOffsetWidthForIndicator: () => 0, + notifyChange: () => undefined, + getNumberOfTabs: () => 0, + isTabActiveAtIndex: () => false, + setTabActiveAtIndex: () => undefined, + isDefaultPreventedOnClickForTabAtIndex: () => false, + setPreventDefaultOnClickForTabAtIndex: () => undefined, + measureTabAtIndex: () => undefined, + getComputedWidthForTabAtIndex: () => 0, + getComputedLeftForTabAtIndex: () => 0, }; + // tslint:enable:object-literal-sort-keys } - constructor(adapter) { - super(Object.assign(MDCTabBarFoundation.defaultAdapter, adapter)); + private isIndicatorShown_ = false; + private activeTabIndex_ = 0; + private layoutFrame_ = 0; - this.isIndicatorShown_ = false; - this.computedWidth_ = 0; - this.computedLeft_ = 0; - this.activeTabIndex_ = 0; - this.layoutFrame_ = 0; - this.resizeHandler_ = () => this.layout(); + private resizeHandler_!: SpecificEventListener<'resize'>; // assigned in init() + + constructor(adapter?: Partial) { + super({...MDCTabBarFoundation.defaultAdapter, ...adapter}); } init() { + this.resizeHandler_ = () => this.layout(); this.adapter_.addClass(cssClasses.UPGRADED); this.adapter_.bindOnMDCTabSelectedEvent(); this.adapter_.registerResizeHandler(this.resizeHandler_); @@ -86,13 +89,50 @@ export default class MDCTabBarFoundation extends MDCFoundation { this.adapter_.deregisterResizeHandler(this.resizeHandler_); } - layoutInternal_() { + layout() { + if (this.layoutFrame_) { + cancelAnimationFrame(this.layoutFrame_); + } + + this.layoutFrame_ = requestAnimationFrame(() => { + this.layoutInternal_(); + this.layoutFrame_ = 0; + }); + } + + switchToTabAtIndex(index: number, shouldNotify: boolean) { + if (index === this.activeTabIndex_) { + return; + } + + if (index < 0 || index >= this.adapter_.getNumberOfTabs()) { + throw new Error(`Out of bounds index specified for tab: ${index}`); + } + + const prevActiveTabIndex = this.activeTabIndex_; + this.activeTabIndex_ = index; + requestAnimationFrame(() => { + if (prevActiveTabIndex >= 0) { + this.adapter_.setTabActiveAtIndex(prevActiveTabIndex, false); + } + this.adapter_.setTabActiveAtIndex(this.activeTabIndex_, true); + this.layoutIndicator_(); + if (shouldNotify) { + this.adapter_.notifyChange({activeTabIndex: this.activeTabIndex_}); + } + }); + } + + getActiveTabIndex() { + return this.findActiveTabIndex_(); + } + + private layoutInternal_() { this.forEachTabIndex_((index) => this.adapter_.measureTabAtIndex(index)); - this.computedWidth_ = this.adapter_.getOffsetWidth(); this.layoutIndicator_(); } - layoutIndicator_() { + private layoutIndicator_() { const isIndicatorFirstRender = !this.isIndicatorShown_; // Ensure that indicator appears in the right position immediately for correct first render. @@ -116,7 +156,7 @@ export default class MDCTabBarFoundation extends MDCFoundation { } } - findActiveTabIndex_() { + private findActiveTabIndex_() { let activeTabIndex = -1; this.forEachTabIndex_((index) => { if (this.adapter_.isTabActiveAtIndex(index)) { @@ -127,7 +167,7 @@ export default class MDCTabBarFoundation extends MDCFoundation { return activeTabIndex; } - forEachTabIndex_(iterator) { + private forEachTabIndex_(iterator: (index: number) => boolean | void) { const numTabs = this.adapter_.getNumberOfTabs(); for (let index = 0; index < numTabs; index++) { const shouldBreak = iterator(index); @@ -136,42 +176,6 @@ export default class MDCTabBarFoundation extends MDCFoundation { } } } - - layout() { - if (this.layoutFrame_) { - cancelAnimationFrame(this.layoutFrame_); - } - - this.layoutFrame_ = requestAnimationFrame(() => { - this.layoutInternal_(); - this.layoutFrame_ = 0; - }); - } - - switchToTabAtIndex(index, shouldNotify) { - if (index === this.activeTabIndex_) { - return; - } - - if (index < 0 || index >= this.adapter_.getNumberOfTabs()) { - throw new Error(`Out of bounds index specified for tab: ${index}`); - } - - const prevActiveTabIndex = this.activeTabIndex_; - this.activeTabIndex_ = index; - requestAnimationFrame(() => { - if (prevActiveTabIndex >= 0) { - this.adapter_.setTabActiveAtIndex(prevActiveTabIndex, false); - } - this.adapter_.setTabActiveAtIndex(this.activeTabIndex_, true); - this.layoutIndicator_(); - if (shouldNotify) { - this.adapter_.notifyChange({activeTabIndex: this.activeTabIndex_}); - } - }); - } - - getActiveTabIndex() { - return this.findActiveTabIndex_(); - } } + +export default MDCTabBarFoundation; diff --git a/packages/mdc-tabs/tab-bar/index.ts b/packages/mdc-tabs/tab-bar/index.ts new file mode 100644 index 00000000000..37eb9683f7f --- /dev/null +++ b/packages/mdc-tabs/tab-bar/index.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright 2019 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +export * from './adapter'; +export * from './component'; +export * from './foundation'; +export * from './types'; diff --git a/packages/mdc-tabs/tab-bar/types.ts b/packages/mdc-tabs/tab-bar/types.ts new file mode 100644 index 00000000000..20e32139db4 --- /dev/null +++ b/packages/mdc-tabs/tab-bar/types.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2019 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +export type MDCTabBarEvent = CustomEvent; + +export interface MDCTabBarEventDetail { + activeTabIndex: number; +} diff --git a/packages/mdc-tabs/tab/adapter.ts b/packages/mdc-tabs/tab/adapter.ts new file mode 100644 index 00000000000..a4b504d2c1d --- /dev/null +++ b/packages/mdc-tabs/tab/adapter.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright 2019 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {EventType, SpecificEventListener} from '@material/base/types'; + +export interface MDCTabAdapter { + addClass: (className: string) => void; + removeClass: (className: string) => void; + registerInteractionHandler: (type: K, handler: SpecificEventListener) => void; + deregisterInteractionHandler: (type: K, handler: SpecificEventListener) => void; + getOffsetWidth: () => number; + getOffsetLeft: () => number; + notifySelected: () => void; +} + +export default MDCTabAdapter; diff --git a/packages/mdc-tabs/tab/index.js b/packages/mdc-tabs/tab/component.ts similarity index 78% rename from packages/mdc-tabs/tab/index.js rename to packages/mdc-tabs/tab/component.ts index 98da64afe64..44799c33d83 100644 --- a/packages/mdc-tabs/tab/index.js +++ b/packages/mdc-tabs/tab/component.ts @@ -23,14 +23,19 @@ import {MDCComponent} from '@material/base/component'; import {MDCRipple} from '@material/ripple/index'; - import {cssClasses} from './constants'; -import MDCTabFoundation from './foundation'; +import {MDCTabFoundation} from './foundation'; + +export type MDCTabFactory = (el: Element) => MDCTab; -export {MDCTabFoundation}; +export type MDCTabSelectedEvent = CustomEvent; -export class MDCTab extends MDCComponent { - static attachTo(root) { +export interface MDCTabSelectedEventDetail { + tab: MDCTab; +} + +export class MDCTab extends MDCComponent { + static attachTo(root: Element) { return new MDCTab(root); } @@ -58,11 +63,9 @@ export class MDCTab extends MDCComponent { this.foundation_.setPreventDefaultOnClick(preventDefaultOnClick); } - constructor(...args) { - super(...args); + protected root_!: HTMLElement; // assigned in MDCComponent constructor - this.ripple_ = MDCRipple.attachTo(this.root_); - } + private ripple_ = MDCRipple.attachTo(this.root_); destroy() { this.ripple_.destroy(); @@ -70,6 +73,7 @@ export class MDCTab extends MDCComponent { } getDefaultFoundation() { + // tslint:disable:object-literal-sort-keys return new MDCTabFoundation({ addClass: (className) => this.root_.classList.add(className), removeClass: (className) => this.root_.classList.remove(className), @@ -77,8 +81,10 @@ export class MDCTab extends MDCComponent { deregisterInteractionHandler: (type, handler) => this.root_.removeEventListener(type, handler), getOffsetWidth: () => this.root_.offsetWidth, getOffsetLeft: () => this.root_.offsetLeft, - notifySelected: () => this.emit(MDCTabFoundation.strings.SELECTED_EVENT, {tab: this}, true), + notifySelected: () => + this.emit(MDCTabFoundation.strings.SELECTED_EVENT, {tab: this}, true), }); + // tslint:enable:object-literal-sort-keys } initialSyncWithDOM() { diff --git a/packages/mdc-tabs/tab/constants.js b/packages/mdc-tabs/tab/constants.ts similarity index 100% rename from packages/mdc-tabs/tab/constants.js rename to packages/mdc-tabs/tab/constants.ts diff --git a/packages/mdc-tabs/tab/foundation.js b/packages/mdc-tabs/tab/foundation.ts similarity index 69% rename from packages/mdc-tabs/tab/foundation.js rename to packages/mdc-tabs/tab/foundation.ts index 76906e3d1cf..38086136c2b 100644 --- a/packages/mdc-tabs/tab/foundation.js +++ b/packages/mdc-tabs/tab/foundation.ts @@ -22,9 +22,11 @@ */ import {MDCFoundation} from '@material/base/foundation'; +import {SpecificEventListener} from '@material/base/types'; +import {MDCTabAdapter} from './adapter'; import {cssClasses, strings} from './constants'; -export default class MDCTabFoundation extends MDCFoundation { +export class MDCTabFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -33,25 +35,30 @@ export default class MDCTabFoundation extends MDCFoundation { return strings; } - static get defaultAdapter() { + static get defaultAdapter(): MDCTabAdapter { + // tslint:disable:object-literal-sort-keys return { - addClass: (/* className: string */) => {}, - removeClass: (/* className: string */) => {}, - registerInteractionHandler: (/* type: string, handler: EventListener */) => {}, - deregisterInteractionHandler: (/* type: string, handler: EventListener */) => {}, - getOffsetWidth: () => /* number */ 0, - getOffsetLeft: () => /* number */ 0, - notifySelected: () => {}, + addClass: () => undefined, + removeClass: () => undefined, + registerInteractionHandler: () => undefined, + deregisterInteractionHandler: () => undefined, + getOffsetWidth: () => 0, + getOffsetLeft: () => 0, + notifySelected: () => undefined, }; + // tslint:enable:object-literal-sort-keys } - constructor(adapter = {}) { - super(Object.assign(MDCTabFoundation.defaultAdapter, adapter)); + private computedWidth_ = 0; + private computedLeft_ = 0; + private isActive_ = false; + private preventDefaultOnClick_ = false; - this.computedWidth_ = 0; - this.computedLeft_ = 0; - this.isActive_ = false; - this.preventDefaultOnClick_ = false; + private readonly clickHandler_: SpecificEventListener<'click'>; + private readonly keydownHandler_: SpecificEventListener<'keydown'>; + + constructor(adapter?: Partial) { + super({...MDCTabFoundation.defaultAdapter, ...adapter}); this.clickHandler_ = (evt) => { if (this.preventDefaultOnClick_) { @@ -61,7 +68,7 @@ export default class MDCTabFoundation extends MDCFoundation { }; this.keydownHandler_ = (evt) => { - if (evt.key && evt.key === 'Enter' || evt.keyCode === 13) { + if (evt.key === 'Enter' || evt.keyCode === 13) { this.adapter_.notifySelected(); } }; @@ -89,7 +96,7 @@ export default class MDCTabFoundation extends MDCFoundation { return this.isActive_; } - setActive(isActive) { + setActive(isActive: boolean) { this.isActive_ = isActive; if (this.isActive_) { this.adapter_.addClass(cssClasses.ACTIVE); @@ -102,7 +109,7 @@ export default class MDCTabFoundation extends MDCFoundation { return this.preventDefaultOnClick_; } - setPreventDefaultOnClick(preventDefaultOnClick) { + setPreventDefaultOnClick(preventDefaultOnClick: boolean) { this.preventDefaultOnClick_ = preventDefaultOnClick; } @@ -111,3 +118,5 @@ export default class MDCTabFoundation extends MDCFoundation { this.computedLeft_ = this.adapter_.getOffsetLeft(); } } + +export default MDCTabFoundation; diff --git a/packages/mdc-tabs/tab/index.ts b/packages/mdc-tabs/tab/index.ts new file mode 100644 index 00000000000..f8c89ac94f3 --- /dev/null +++ b/packages/mdc-tabs/tab/index.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2019 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +export * from './adapter'; +export * from './component'; +export * from './foundation'; diff --git a/scripts/webpack/js-bundle-factory.js b/scripts/webpack/js-bundle-factory.js index 028bc376196..eaf91f932b7 100644 --- a/scripts/webpack/js-bundle-factory.js +++ b/scripts/webpack/js-bundle-factory.js @@ -182,7 +182,7 @@ class JsBundleFactory { tabBar: getAbsolutePath('/packages/mdc-tab-bar/index.ts'), tabIndicator: getAbsolutePath('/packages/mdc-tab-indicator/index.ts'), tabScroller: getAbsolutePath('/packages/mdc-tab-scroller/index.ts'), - tabs: getAbsolutePath('/packages/mdc-tabs/index.js'), + tabs: getAbsolutePath('/packages/mdc-tabs/index.ts'), textfield: getAbsolutePath('/packages/mdc-textfield/index.ts'), toolbar: getAbsolutePath('/packages/mdc-toolbar/index.ts'), topAppBar: getAbsolutePath('/packages/mdc-top-app-bar/index.ts'), diff --git a/test/unit/mdc-tabs/mdc-tab-bar-foundation.test.js b/test/unit/mdc-tabs/mdc-tab-bar-foundation.test.js index b18354f277f..62406d63df9 100644 --- a/test/unit/mdc-tabs/mdc-tab-bar-foundation.test.js +++ b/test/unit/mdc-tabs/mdc-tab-bar-foundation.test.js @@ -113,6 +113,7 @@ test('#destroy deregisters tab event handlers', () => { const {foundation, mockAdapter} = setupTest(); const {isA} = td.matchers; + foundation.init(); foundation.destroy(); td.verify(mockAdapter.unbindOnMDCTabSelectedEvent()); td.verify(mockAdapter.deregisterResizeHandler(isA(Function)));