@limetech/lime-elements
Version:
281 lines (280 loc) • 10.3 kB
JavaScript
import { h, } from '@stencil/core';
import { MDCTabBar } from '@material/tab-bar';
import { strings } from '@material/tab-bar/constants';
import { isEqual, difference } from 'lodash-es';
import { setActiveTab } from './tabs';
import { getIconColor, getIconName } from '../icon/get-icon-props';
const { TAB_ACTIVATED_EVENT } = strings;
const SCROLL_DISTANCE_ON_CLICK_PX = 150;
const HIDE_SCROLL_BUTTONS_WHEN_SCROLLED_LESS_THAN_PX = 40;
const TOTAL_WIDTH_PERCENTAGE = 100;
const OVERLAP_PERCENTAGE = 20;
/**
* Tabs are great to organize information hierarchically in the interface and divide it into distinct categories. Using tabs, you can create groups of content that are related and at the same level in the hierarchy.
* :::warning
* Tab bars should be strictly used for navigation at the top levels.
* They should never be used to perform actions, or navigate away from the view which contains them.
* :::
* An exception for using tab bars in a high level of hierarchy is their usage in modals. This is because modals are perceived as a separate place and not a part of the current context. Therefore you can use tab bars in a modal to group and organize its content.
* A tab bar can contain an unlimited number of tabs. However, depending on the device width and width of the tabs, the number of tabs that are visible at the same time will vary. When there is limited horizontal space, the component shows a left-arrow and/or right-arrow button, which scrolls and reveals the additional tabs. The tab bar can also be swiped left and right on a touch-device.
* :::tip Other things to consider
* Never divide the content of a tab using a nested tab bar.
* Never place two tab bars within the same screen.
* Never use background color for icons in tabs.
* Avoid having long labels for tabs.
* A tab will never be removed or get disabled, even if there is no content under it.
* :::
*
* @exampleComponent limel-example-tab-bar
* @exampleComponent limel-example-tab-bar-with-dynamic-tab-width
* @exampleComponent limel-example-tab-bar-with-equal-tab-width
*/
export class TabBar {
constructor() {
this.setupMdc = false;
this.tabs = [];
this.canScrollLeft = false;
this.canScrollRight = false;
this.handleTabActivated = this.handleTabActivated.bind(this);
this.handleScroll = this.handleScroll.bind(this);
this.handleLeftScrollClick = this.handleLeftScrollClick.bind(this);
this.handleRightScrollClick = this.handleRightScrollClick.bind(this);
this.renderTab = this.renderTab.bind(this);
}
connectedCallback() {
this.setup();
}
componentDidLoad() {
this.setup();
this.triggerIconColorWarning();
}
componentDidUpdate() {
if (!this.setupMdc) {
return;
}
this.setup();
this.setupMdc = false;
}
disconnectedCallback() {
this.tearDown();
}
render() {
return (h("div", { class: "mdc-tab-bar", role: "tablist" }, h("div", { class: {
'mdc-tab-scroller': true,
'can-scroll-left': this.canScrollLeft,
'can-scroll-right': this.canScrollRight,
} }, h("div", { class: "mdc-tab-scroller__scroll-area lime-hide-scrollbars" }, h("div", { class: "mdc-tab-scroller__scroll-content" }, this.tabs.map(this.renderTab))), h("div", { class: "scroll-fade left" }), h("div", { class: "scroll-button left" }, h("button", { type: "button", tabindex: "-1", "aria-hidden": "true", disabled: !this.canScrollLeft, onClick: this.handleLeftScrollClick }, h("limel-icon", { name: "angle_left" }))), h("div", { class: "scroll-fade right" }), h("div", { class: "scroll-button right" }, h("button", { type: "button", tabindex: "-1", "aria-hidden": "true", disabled: !this.canScrollRight, onClick: this.handleRightScrollClick }, h("limel-icon", { name: "angle_right" }))))));
}
tabsChanged(newTabs = [], oldTabs = []) {
const newIds = newTabs.map((tab) => tab.id);
const oldIds = oldTabs.map((tab) => tab.id);
if (isEqual(newIds, oldIds)) {
return;
}
this.setupMdc = true;
this.tearDown();
}
handleWindowResize() {
if (!this.scrollArea) {
return;
}
this.handleScroll();
}
setup() {
const element = this.host.shadowRoot.querySelector('.mdc-tab-bar');
if (!element) {
return;
}
this.mdcTabBar = new MDCTabBar(element);
this.mdcTabBar.focusOnActivate = true;
this.mdcTabBar.useAutomaticActivation = true;
this.scrollArea = element.querySelector('.mdc-tab-scroller__scroll-area');
this.scrollContent = element.querySelector('.mdc-tab-scroller__scroll-content');
this.setupListeners();
// Use timeout to avoid Stencil warning about re-renders. /Ads
setTimeout(this.handleScroll, 0);
}
tearDown() {
if (this.scrollArea) {
this.scrollArea.removeEventListener('scroll', this.handleScroll);
}
if (this.mdcTabBar) {
this.mdcTabBar.unlisten(TAB_ACTIVATED_EVENT, this.handleTabActivated);
this.mdcTabBar.destroy();
}
}
setupListeners() {
this.mdcTabBar.listen(TAB_ACTIVATED_EVENT, this.handleTabActivated);
this.scrollArea.addEventListener('scroll', this.handleScroll, {
passive: true,
});
}
handleTabActivated(event) {
const index = event.detail.index;
const newTabs = setActiveTab(this.tabs, index);
difference(newTabs, this.tabs)
.sort(this.sortByInactive)
// eslint-disable-next-line unicorn/no-array-for-each
.forEach((tab) => {
this.changeTab.emit(tab);
});
this.tabs = newTabs;
}
sortByInactive(a, b) {
return Number(a.active) - Number(b.active);
}
handleScroll() {
const scrollLeft = this.scrollArea.scrollLeft;
const scrollRight = Math.floor(this.scrollContent.getBoundingClientRect().width -
this.scrollArea.getBoundingClientRect().width -
scrollLeft);
if (scrollLeft > HIDE_SCROLL_BUTTONS_WHEN_SCROLLED_LESS_THAN_PX) {
this.canScrollLeft = true;
}
else {
this.canScrollLeft = false;
}
if (scrollRight > HIDE_SCROLL_BUTTONS_WHEN_SCROLLED_LESS_THAN_PX) {
this.canScrollRight = true;
}
else {
this.canScrollRight = false;
}
}
handleLeftScrollClick() {
const scrollDistance = this.getScrollDistance();
this.scrollArea.scroll({
left: this.scrollArea.scrollLeft - scrollDistance,
behavior: 'smooth',
});
}
handleRightScrollClick() {
const scrollDistance = this.getScrollDistance();
this.scrollArea.scroll({
left: this.scrollArea.scrollLeft + scrollDistance,
behavior: 'smooth',
});
}
/**
* Calculates how far to scroll when navigation buttons are clicked.
* Returns the visible width minus an overlap percentage to maintain context.
* Falls back to the constant value if something goes wrong.
*/
getScrollDistance() {
if (!this.scrollArea) {
return SCROLL_DISTANCE_ON_CLICK_PX;
}
const containerWidth = this.scrollArea.getBoundingClientRect().width;
const scrollDistance = containerWidth * (1 - OVERLAP_PERCENTAGE / TOTAL_WIDTH_PERCENTAGE);
return Math.max(scrollDistance, SCROLL_DISTANCE_ON_CLICK_PX);
}
renderIcon(tab) {
if (!tab.icon) {
return;
}
const name = getIconName(tab.icon);
const color = getIconColor(tab.icon, tab.iconColor);
const style = { color: '' };
if (color) {
style.color = color;
}
return (h("limel-icon", { class: "mdc-tab__icon", name: name, style: style, size: "small", "aria-hidden": "true" }));
}
renderTab(tab) {
return (h("button", { class: {
'mdc-tab': true,
'mdc-tab--active': !!tab.active,
}, role: "tab", "aria-selected": tab.active ? 'true' : 'false', tabindex: tab.active ? 0 : -1 }, h("span", { class: "mdc-tab__content" }, this.renderIcon(tab), h("span", { class: "mdc-tab__text-label" }, tab.text), tab.badge ? h("limel-badge", { label: tab.badge }) : ''), h("span", { class: {
'mdc-tab-indicator': true,
'mdc-tab-indicator--active': !!tab.active,
} }, h("span", { class: "mdc-tab-indicator__content mdc-tab-indicator__content--underline" })), h("span", { class: "mdc-tab__ripple" })));
}
triggerIconColorWarning() {
if (this.tabs.some((tab) => tab.iconColor)) {
console.warn("The `iconColor` prop is deprecated now! Use the new `Icon` interface and instead of `iconColor: 'color-name'` write `icon {name: 'icon-name', color: 'color-name'}`.");
}
}
static get is() { return "limel-tab-bar"; }
static get encapsulation() { return "shadow"; }
static get originalStyleUrls() {
return {
"$": ["tab-bar.scss"]
};
}
static get styleUrls() {
return {
"$": ["tab-bar.css"]
};
}
static get properties() {
return {
"tabs": {
"type": "unknown",
"mutable": true,
"complexType": {
"original": "Tab[]",
"resolved": "Tab[]",
"references": {
"Tab": {
"location": "import",
"path": "./tab.types"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "List of tabs to display"
},
"defaultValue": "[]"
}
};
}
static get states() {
return {
"canScrollLeft": {},
"canScrollRight": {}
};
}
static get events() {
return [{
"method": "changeTab",
"name": "changeTab",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Emitted when a tab has been changed"
},
"complexType": {
"original": "Tab",
"resolved": "Tab",
"references": {
"Tab": {
"location": "import",
"path": "./tab.types"
}
}
}
}];
}
static get elementRef() { return "host"; }
static get watchers() {
return [{
"propName": "tabs",
"methodName": "tabsChanged"
}];
}
static get listeners() {
return [{
"name": "resize",
"method": "handleWindowResize",
"target": "window",
"capture": false,
"passive": true
}];
}
}
//# sourceMappingURL=tab-bar.js.map