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

Commit

Permalink
feat(tab-bar): Support manual and automatic activation behavior (#3303)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Adds focusTabAtIndex and getFocusedTabIndex MDCTabBarAdapter APIs; adds focus MDCTab component API used by MDCTabBar.
  • Loading branch information
kfranqueiro authored Aug 6, 2018
1 parent 9000000 commit 7182fa1
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 94 deletions.
2 changes: 2 additions & 0 deletions packages/mdc-tab-bar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,12 @@ Method Signature | Description
`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.
`focusTabAtIndex(index: number) => void` | Focuses 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.
`getFocusedTabIndex() => number` | Returns the index of the focused Tab.
`getIndexOfTab(tab: MDCTab) => number` | Returns the index of the given Tab instance.
`notifyTabActivated(index: number) => void` | Emits the `MDCTabBar:activated` event.

Expand Down
14 changes: 13 additions & 1 deletion packages/mdc-tab-bar/adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,16 @@ class MDCTabBarAdapter {

/**
* Deactivates the tab at the given index
* @param {number} index The index of the tab to activate
* @param {number} index The index of the tab to deactivate
*/
deactivateTabAtIndex(index) {}

/**
* Focuses the tab at the given index
* @param {number} index The index of the tab to focus
*/
focusTabAtIndex(index) {}

/**
* Returns the client rect of the tab's indicator
* @param {number} index The index of the tab
Expand All @@ -108,6 +114,12 @@ class MDCTabBarAdapter {
*/
getActiveTabIndex() {}

/**
* Returns the index of the focused tab
* @return {number}
*/
getFocusedTabIndex() {}

/**
* Returns the index of the given tab
* @param {!MDCTab} tab The tab whose index to determin
Expand Down
12 changes: 8 additions & 4 deletions packages/mdc-tab-bar/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,23 @@ 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',
END_KEY: 'End',
HOME_KEY: 'Home',
ENTER_KEY: 'Enter',
SPACE_KEY: 'Space',
};

/** @enum {number} */
const numbers = {
EXTRA_SCROLL_AMOUNT: 20,
END_KEYCODE: 35,
HOME_KEYCODE: 36,
ARROW_LEFT_KEYCODE: 37,
ARROW_RIGHT_KEYCODE: 39,
END_KEYCODE: 35,
HOME_KEYCODE: 36,
ENTER_KEYCODE: 13,
SPACE_KEYCODE: 32,
};

export {
Expand Down
81 changes: 63 additions & 18 deletions packages/mdc-tab-bar/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,20 @@ 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);
ACCEPTABLE_KEYS.add(strings.ENTER_KEY);
ACCEPTABLE_KEYS.add(strings.SPACE_KEY);

/**
* @type {Map<number, string>}
*/
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);
KEYCODE_MAP.set(numbers.END_KEYCODE, strings.END_KEY);
KEYCODE_MAP.set(numbers.HOME_KEYCODE, strings.HOME_KEY);
KEYCODE_MAP.set(numbers.ENTER_KEYCODE, strings.ENTER_KEY);
KEYCODE_MAP.set(numbers.SPACE_KEYCODE, strings.SPACE_KEY);

/**
* @extends {MDCFoundation<!MDCTabBarAdapter>}
Expand Down Expand Up @@ -74,9 +78,11 @@ class MDCTabBarFoundation extends MDCFoundation {
isRTL: () => {},
activateTabAtIndex: () => {},
deactivateTabAtIndex: () => {},
focusTabAtIndex: () => {},
getTabIndicatorClientRectAtIndex: () => {},
getTabDimensionsAtIndex: () => {},
getActiveTabIndex: () => {},
getFocusedTabIndex: () => {},
getIndexOfTab: () => {},
getTabListLength: () => {},
notifyTabActivated: () => {},
Expand All @@ -88,13 +94,25 @@ class MDCTabBarFoundation extends MDCFoundation {
* */
constructor(adapter) {
super(Object.assign(MDCTabBarFoundation.defaultAdapter, adapter));

/** @private {boolean} */
this.useAutomaticActivation_ = false;
}

init() {
const activeIndex = this.adapter_.getActiveTabIndex();
this.scrollIntoView(activeIndex);
}

/**
* Switches between automatic and manual activation modes.
* See https://www.w3.org/TR/wai-aria-practices/#tabpanel for examples.
* @param {boolean} useAutomaticActivation
*/
setUseAutomaticActivation(useAutomaticActivation) {
this.useAutomaticActivation_ = useAutomaticActivation;
}

/**
* Activates the tab at the given index
* @param {number} index
Expand Down Expand Up @@ -128,8 +146,29 @@ class MDCTabBarFoundation extends MDCFoundation {
return;
}

evt.preventDefault();
this.activateTabFromKey_(key);
// Prevent default behavior for movement keys, but not for activation keys, since :active is used to apply ripple
if (!this.isActivationKey_(key)) {
evt.preventDefault();
}

if (this.useAutomaticActivation_) {
if (this.isActivationKey_(key)) {
return;
}

const index = this.determineTargetFromKey_(this.adapter_.getActiveTabIndex(), key);
this.activateTab(index);
this.scrollIntoView(index);
} else {
const focusedTabIndex = this.adapter_.getFocusedTabIndex();
if (this.isActivationKey_(key)) {
this.activateTab(focusedTabIndex);
} else {
const index = this.determineTargetFromKey_(focusedTabIndex, key);
this.adapter_.focusTabAtIndex(index);
this.scrollIntoView(index);
}
}
}

/**
Expand Down Expand Up @@ -169,35 +208,37 @@ class MDCTabBarFoundation extends MDCFoundation {
}

/**
* Private method for activating a tab from a key
* Private method for determining the index of the destination tab based on what key was pressed
* @param {number} origin The original index from which to determine the destination
* @param {string} key The name of the key
* @return {number}
* @private
*/
activateTabFromKey_(key) {
determineTargetFromKey_(origin, key) {
const isRTL = this.isRTL_();
const maxTabIndex = this.adapter_.getTabListLength() - 1;
const maxIndex = 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();
let index = origin;

if (shouldGoToEnd) {
tabIndex = maxTabIndex;
index = maxIndex;
} else if (shouldDecrement) {
tabIndex -= 1;
index -= 1;
} else if (shouldIncrement) {
tabIndex += 1;
index += 1;
} else {
tabIndex = 0;
index = 0;
}

if (tabIndex < 0) {
tabIndex = maxTabIndex;
} else if (tabIndex > maxTabIndex) {
tabIndex = 0;
if (index < 0) {
index = maxIndex;
} else if (index > maxIndex) {
index = 0;
}

this.activateTab(tabIndex);
return index;
}

/**
Expand Down Expand Up @@ -233,7 +274,7 @@ class MDCTabBarFoundation extends MDCFoundation {
* @return {number}
* @private
*/
calculateScrollIncrementRTL_(index, nextIndex, scrollPosition, barWidth, scrollContentWidth, ) {
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;
Expand Down Expand Up @@ -340,6 +381,10 @@ class MDCTabBarFoundation extends MDCFoundation {
return KEYCODE_MAP.get(evt.keyCode);
}

isActivationKey_(key) {
return key === strings.SPACE_KEY || key === strings.ENTER_KEY;
}

/**
* Returns whether a given index is inclusively between the ends
* @param {number} index The index to test
Expand Down
17 changes: 15 additions & 2 deletions packages/mdc-tab-bar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ class MDCTabBar extends MDCComponent {
return new MDCTabBar(root);
}

set useAutomaticActivation(useAutomaticActivation) {
this.foundation_.setUseAutomaticActivation(useAutomaticActivation);
}

/**
* @param {(function(!Element): !MDCTab)=} tabFactory A function which creates a new MDCTab
* @param {(function(!Element): !MDCTabScroller)=} tabScrollerFactory A function which creates a new MDCTabScroller
Expand All @@ -72,8 +76,7 @@ class MDCTabBar extends MDCComponent {
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));
this.tabList_ = this.getTabElements_().map((el) => this.tabFactory_(el));

const tabScrollerElement = this.root_.querySelector(MDCTabBarFoundation.strings.TAB_SCROLLER_SELECTOR);
if (tabScrollerElement) {
Expand Down Expand Up @@ -111,6 +114,7 @@ class MDCTabBar extends MDCComponent {
isRTL: () => window.getComputedStyle(this.root_).getPropertyValue('direction') === 'rtl',
activateTabAtIndex: (index, clientRect) => this.tabList_[index].activate(clientRect),
deactivateTabAtIndex: (index) => this.tabList_[index].deactivate(),
focusTabAtIndex: (index) => this.tabList_[index].focus(),
getTabIndicatorClientRectAtIndex: (index) => this.tabList_[index].computeIndicatorClientRect(),
getTabDimensionsAtIndex: (index) => this.tabList_[index].computeDimensions(),
getActiveTabIndex: () => {
Expand All @@ -121,6 +125,11 @@ class MDCTabBar extends MDCComponent {
}
return -1;
},
getFocusedTabIndex: () => {
const tabElements = this.getTabElements_();
const activeElement = document.activeElement;
return tabElements.indexOf(activeElement);
},
getIndexOfTab: (tabToFind) => this.tabList_.indexOf(tabToFind),
getTabListLength: () => this.tabList_.length,
notifyTabActivated: (index) => this.emit(MDCTabBarFoundation.strings.TAB_ACTIVATED_EVENT, {index}, true),
Expand All @@ -143,6 +152,10 @@ class MDCTabBar extends MDCComponent {
scrollIntoView(index) {
this.foundation_.scrollIntoView(index);
}

getTabElements_() {
return [].slice.call(this.root_.querySelectorAll(MDCTabBarFoundation.strings.TAB_SELECTOR));
}
}

export {MDCTabBar, MDCTabBarFoundation};
1 change: 1 addition & 0 deletions packages/mdc-tab/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ Method Signature | Description
--- | ---
`activate(previousIndicatorClientRect: ClientRect=) => void` | Activates the indicator. `previousIndicatorClientRect` is an optional argument.
`deactivate() => void` | Deactivates the indicator.
`focus() => void` | Focuses the tab.
`computeIndicatorClientRect() => ClientRect` | Returns the bounding client rect of the indicator.
`computeDimensions() => MDCTabDimensions` | Returns the dimensions of the Tab.

Expand Down
7 changes: 7 additions & 0 deletions packages/mdc-tab/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ class MDCTab extends MDCComponent {
computeDimensions() {
return this.foundation_.computeDimensions();
}

/**
* Focuses the tab
*/
focus() {
this.root_.focus();
}
}

export {MDCTab, MDCTabFoundation};
Loading

0 comments on commit 7182fa1

Please sign in to comment.