Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.

Commit 2d35220

Browse files
authored
feat(tab): Get tabs by their ID (#4149)
Use unique identifiers to reference tabs instead of the object equality comparison. Change the getIndexOfTab method to getIndexOfTabByID. Update tests. This makes it more feasible to wrap MDC Tab Bar for frameworks, which shouldn't need to reference the vanilla component. BREAKING CHANGE: MDCTabBar#getIndexOfTab(tab: MDCTab): boolean is now MDCTabBar#getIndexOfTabByID(id: string): boolean
1 parent 2f6dda2 commit 2d35220

File tree

9 files changed

+73
-29
lines changed

9 files changed

+73
-29
lines changed

packages/mdc-tab-bar/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ Method Signature | Description
133133
`getTabListLength() => number` | Returns the number of child Tab components.
134134
`getPreviousActiveTabIndex() => number` | Returns the index of the previously active Tab.
135135
`getFocusedTabIndex() => number` | Returns the index of the focused Tab.
136-
`getIndexOfTab(tab: MDCTab) => number` | Returns the index of the given Tab instance.
136+
`getIndexOfTabById(id: string) => number` | Returns the index of the given Tab ID.
137137
`notifyTabActivated(index: number) => void` | Emits the `MDCTabBar:activated` event.
138138

139139
### `MDCTabBarFoundation`

packages/mdc-tab-bar/adapter.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525

2626
/* eslint-disable no-unused-vars */
2727
import {MDCTabDimensions} from '@material/tab/adapter';
28-
import {MDCTab} from '@material/tab/index';
2928
/* eslint-enable no-unused-vars */
3029

3130
/**
@@ -134,10 +133,10 @@ class MDCTabBarAdapter {
134133

135134
/**
136135
* Returns the index of the given tab
137-
* @param {!MDCTab} tab The tab whose index to determin
136+
* @param {string} id The ID of the tab whose index to determine
138137
* @return {number}
139138
*/
140-
getIndexOfTab(tab) {}
139+
getIndexOfTabById(id) {}
141140

142141
/**
143142
* Emits the MDCTabBar:activated event

packages/mdc-tab-bar/foundation.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class MDCTabBarFoundation extends MDCFoundation {
8989
getTabDimensionsAtIndex: () => {},
9090
getPreviousActiveTabIndex: () => {},
9191
getFocusedTabIndex: () => {},
92-
getIndexOfTab: () => {},
92+
getIndexOfTabById: () => {},
9393
getTabListLength: () => {},
9494
notifyTabActivated: () => {},
9595
});
@@ -174,7 +174,7 @@ class MDCTabBarFoundation extends MDCFoundation {
174174
* @param {!Event} evt
175175
*/
176176
handleTabInteraction(evt) {
177-
this.adapter_.setActiveTab(this.adapter_.getIndexOfTab(evt.detail.tab));
177+
this.adapter_.setActiveTab(this.adapter_.getIndexOfTabById(evt.detail.tabId));
178178
}
179179

180180
/**

packages/mdc-tab-bar/index.js

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import {MDCTabScroller} from '@material/tab-scroller/index';
2929
import MDCTabBarAdapter from './adapter';
3030
import MDCTabBarFoundation from './foundation';
3131

32+
let tabIdCounter = 0;
33+
3234
/**
3335
* @extends {MDCComponent<!MDCTabBarFoundation>}
3436
* @final
@@ -43,15 +45,9 @@ class MDCTabBar extends MDCComponent {
4345
/** @private {!Array<!MDCTab>} */
4446
this.tabList_;
4547

46-
/** @type {(function(!Element): !MDCTab)} */
47-
this.tabFactory_;
48-
4948
/** @private {?MDCTabScroller} */
5049
this.tabScroller_;
5150

52-
/** @type {(function(!Element): !MDCTabScroller)} */
53-
this.tabScrollerFactory_;
54-
5551
/** @private {?function(?Event): undefined} */
5652
this.handleTabInteraction_;
5753

@@ -82,15 +78,8 @@ class MDCTabBar extends MDCComponent {
8278
initialize(
8379
tabFactory = (el) => new MDCTab(el),
8480
tabScrollerFactory = (el) => new MDCTabScroller(el)) {
85-
this.tabFactory_ = tabFactory;
86-
this.tabScrollerFactory_ = tabScrollerFactory;
87-
88-
this.tabList_ = this.getTabElements_().map((el) => this.tabFactory_(el));
89-
90-
const tabScrollerElement = this.root_.querySelector(MDCTabBarFoundation.strings.TAB_SCROLLER_SELECTOR);
91-
if (tabScrollerElement) {
92-
this.tabScroller_ = this.tabScrollerFactory_(tabScrollerElement);
93-
}
81+
this.tabList_ = this.instantiateTabs_(tabFactory);
82+
this.tabScroller_ = this.instantiateTabScroller_(tabScrollerFactory);
9483
}
9584

9685
initialSyncWithDOM() {
@@ -147,7 +136,14 @@ class MDCTabBar extends MDCComponent {
147136
const activeElement = document.activeElement;
148137
return tabElements.indexOf(activeElement);
149138
},
150-
getIndexOfTab: (tabToFind) => this.tabList_.indexOf(tabToFind),
139+
getIndexOfTabById: (id) => {
140+
for (let i = 0; i < this.tabList_.length; i++) {
141+
if (this.tabList_[i].id === id) {
142+
return i;
143+
}
144+
}
145+
return -1;
146+
},
151147
getTabListLength: () => this.tabList_.length,
152148
notifyTabActivated: (index) => this.emit(MDCTabBarFoundation.strings.TAB_ACTIVATED_EVENT, {index}, true),
153149
})
@@ -170,9 +166,41 @@ class MDCTabBar extends MDCComponent {
170166
this.foundation_.scrollIntoView(index);
171167
}
172168

169+
/**
170+
* Returns all the tab elements in a nice clean array
171+
* @return {!Array<!Element>}
172+
* @private
173+
*/
173174
getTabElements_() {
174175
return [].slice.call(this.root_.querySelectorAll(MDCTabBarFoundation.strings.TAB_SELECTOR));
175176
}
177+
178+
/**
179+
* Instantiates tab components on all child tab elements
180+
* @param {(function(!Element): !MDCTab)} tabFactory
181+
* @return {!Array<!MDCTab>}
182+
* @private
183+
*/
184+
instantiateTabs_(tabFactory) {
185+
return this.getTabElements_().map((el) => {
186+
el.id = el.id || `mdc-tab-${++tabIdCounter}`;
187+
return tabFactory(el);
188+
});
189+
}
190+
191+
/**
192+
* Instantiates tab scroller component on the child tab scroller element
193+
* @param {(function(!Element): !MDCTabScroller)} tabScrollerFactory
194+
* @return {?MDCTabScroller}
195+
* @private
196+
*/
197+
instantiateTabScroller_(tabScrollerFactory) {
198+
const tabScrollerElement = this.root_.querySelector(MDCTabBarFoundation.strings.TAB_SCROLLER_SELECTOR);
199+
if (tabScrollerElement) {
200+
return tabScrollerFactory(tabScrollerElement);
201+
}
202+
return null;
203+
}
176204
}
177205

178206
export {MDCTabBar, MDCTabBarFoundation};

packages/mdc-tab/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ Method Signature | Description
154154

155155
Event Name | Event Data Structure | Description
156156
--- | --- | ---
157-
`MDCTab:interacted` | `{"detail": {"tab": MDCTab}}` | Emitted when the Tab is interacted with, regardless of its active state. Used by parent components to know which Tab to activate.
157+
`MDCTab:interacted` | `{"detail": {"tabId": string}}` | Emitted when the Tab is interacted with, regardless of its active state. Used by parent components to know which Tab to activate.
158158

159159
## Usage within Web Frameworks
160160

packages/mdc-tab/adapter.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@
3131
*/
3232
let MDCTabDimensions;
3333

34+
/**
35+
* @typedef {{
36+
* detail: {
37+
* tabId: string,
38+
* },
39+
* bubbles: boolean,
40+
* }}
41+
*/
42+
let MDCTabInteractionEventType;
43+
3444
/**
3545
* Adapter for MDC Tab.
3646
*
@@ -112,4 +122,4 @@ class MDCTabAdapter {
112122
focus() {}
113123
}
114124

115-
export {MDCTabDimensions, MDCTabAdapter};
125+
export {MDCTabDimensions, MDCTabInteractionEventType, MDCTabAdapter};

packages/mdc-tab/index.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import MDCComponent from '@material/base/component';
2626
/* eslint-disable no-unused-vars */
2727
import {MDCRipple, MDCRippleFoundation, RippleCapableSurface} from '@material/ripple/index';
2828
import {MDCTabIndicator, MDCTabIndicatorFoundation} from '@material/tab-indicator/index';
29-
import {MDCTabAdapter, MDCTabDimensions} from './adapter';
29+
import {MDCTabAdapter, MDCTabDimensions, MDCTabInteractionEventType} from './adapter';
3030
/* eslint-enable no-unused-vars */
3131

3232
import MDCTabFoundation from './foundation';
@@ -41,6 +41,9 @@ class MDCTab extends MDCComponent {
4141
*/
4242
constructor(...args) {
4343
super(...args);
44+
45+
/** @type {string} */
46+
this.id;
4447
/** @private {?MDCRipple} */
4548
this.ripple_;
4649
/** @private {?MDCTabIndicator} */
@@ -63,6 +66,7 @@ class MDCTab extends MDCComponent {
6366
initialize(
6467
rippleFactory = (el, foundation) => new MDCRipple(el, foundation),
6568
tabIndicatorFactory = (el) => new MDCTabIndicator(el)) {
69+
this.id = this.root_.id;
6670
const rippleSurface = this.root_.querySelector(MDCTabFoundation.strings.RIPPLE_SELECTOR);
6771
const rippleAdapter = Object.assign(MDCRipple.createAdapter(/** @type {!RippleCapableSurface} */ (this)), {
6872
addClass: (className) => rippleSurface.classList.add(className),
@@ -101,7 +105,8 @@ class MDCTab extends MDCComponent {
101105
hasClass: (className) => this.root_.classList.contains(className),
102106
activateIndicator: (previousIndicatorClientRect) => this.tabIndicator_.activate(previousIndicatorClientRect),
103107
deactivateIndicator: () => this.tabIndicator_.deactivate(),
104-
notifyInteracted: () => this.emit(MDCTabFoundation.strings.INTERACTED_EVENT, {tab: this}, true /* bubble */),
108+
notifyInteracted: () => this.emit(
109+
MDCTabFoundation.strings.INTERACTED_EVENT, {tabId: this.id}, true /* bubble */),
105110
getOffsetLeft: () => this.root_.offsetLeft,
106111
getOffsetWidth: () => this.root_.offsetWidth,
107112
getContentOffsetLeft: () => this.content_.offsetLeft,

test/unit/mdc-tab-bar/foundation.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ test('defaultAdapter returns a complete adapter implementation', () => {
4848
'getOffsetWidth', 'isRTL', 'setActiveTab',
4949
'activateTabAtIndex', 'deactivateTabAtIndex', 'focusTabAtIndex',
5050
'getTabIndicatorClientRectAtIndex', 'getTabDimensionsAtIndex',
51-
'getPreviousActiveTabIndex', 'getFocusedTabIndex', 'getIndexOfTab', 'getTabListLength',
51+
'getPreviousActiveTabIndex', 'getFocusedTabIndex', 'getIndexOfTabById', 'getTabListLength',
5252
'notifyTabActivated',
5353
]);
5454
});

test/unit/mdc-tab-bar/mdc-tab-bar.test.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ test('attachTo returns an MDCTabBar instance', () => {
5858
assert.isOk(MDCTabBar.attachTo(getFixture()) instanceof MDCTabBar);
5959
});
6060

61+
let fakeTabIdCounter = 0;
6162
class FakeTab {
6263
constructor() {
64+
this.id = `mdc-tab-${++fakeTabIdCounter}`;
6365
this.destroy = td.function();
6466
this.activate = td.function();
6567
this.deactivate = td.function();
@@ -206,10 +208,10 @@ test('#adapter.getPreviousActiveTabIndex returns the index of the active tab', (
206208
assert.strictEqual(component.getDefaultFoundation().adapter_.getPreviousActiveTabIndex(), 1);
207209
});
208210

209-
test('#adapter.getIndexOfTab returns the index of the given tab', () => {
211+
test('#adapter.getIndexOfTabById returns the index of the given tab', () => {
210212
const {component} = setupTest();
211213
const tab = component.tabList_[2];
212-
assert.strictEqual(component.getDefaultFoundation().adapter_.getIndexOfTab(tab), 2);
214+
assert.strictEqual(component.getDefaultFoundation().adapter_.getIndexOfTabById(tab.id), 2);
213215
});
214216

215217
test('#adapter.getTabListLength returns the length of the tab list', () => {

0 commit comments

Comments
 (0)