2023-01-13 20:43:55 +00:00
|
|
|
import '../icon-button/icon-button';
|
2021-09-29 12:40:26 +00:00
|
|
|
import { classMap } from 'lit/directives/class-map.js';
|
2023-01-13 20:43:55 +00:00
|
|
|
import { customElement, property, query, state } from 'lit/decorators.js';
|
|
|
|
import { html } from 'lit';
|
|
|
|
import { LocalizeController } from '../../utilities/localize';
|
2022-03-24 12:01:09 +00:00
|
|
|
import { scrollIntoView } from '../../internal/scroll';
|
|
|
|
import { watch } from '../../internal/watch';
|
2023-01-13 20:43:55 +00:00
|
|
|
import ShoelaceElement from '../../internal/shoelace-element';
|
2021-07-10 00:45:44 +00:00
|
|
|
import styles from './tab-group.styles';
|
2022-07-19 12:27:39 +00:00
|
|
|
import type { CSSResultGroup } from 'lit';
|
2023-01-13 20:43:55 +00:00
|
|
|
import type SlTab from '../tab/tab';
|
|
|
|
import type SlTabPanel from '../tab-panel/tab-panel';
|
2021-07-12 14:36:06 +00:00
|
|
|
|
2020-07-15 21:30:37 +00:00
|
|
|
/**
|
2022-10-21 13:56:35 +00:00
|
|
|
* @summary Tab groups organize content into a container that shows one section at a time.
|
2023-01-12 15:26:25 +00:00
|
|
|
* @documentation https://shoelace.style/components/tab-group
|
2020-07-15 21:30:37 +00:00
|
|
|
* @status stable
|
2023-01-12 15:26:25 +00:00
|
|
|
* @since 2.0
|
2020-07-15 21:30:37 +00:00
|
|
|
*
|
2021-02-26 14:09:13 +00:00
|
|
|
* @dependency sl-icon-button
|
|
|
|
*
|
2022-12-06 16:18:14 +00:00
|
|
|
* @slot - Used for grouping tab panels in the tab group. Must be `<sl-tab-panel>` elements.
|
|
|
|
* @slot nav - Used for grouping tabs in the tab group. Must be `<sl-tab>` elements.
|
2020-07-15 21:30:37 +00:00
|
|
|
*
|
2021-06-25 20:25:46 +00:00
|
|
|
* @event {{ name: String }} sl-tab-show - Emitted when a tab is shown.
|
|
|
|
* @event {{ name: String }} sl-tab-hide - Emitted when a tab is hidden.
|
2021-05-17 22:04:28 +00:00
|
|
|
*
|
2022-12-06 16:18:14 +00:00
|
|
|
* @csspart base - The component's base wrapper.
|
|
|
|
* @csspart nav - The tab group's navigation container where tabs are slotted in.
|
|
|
|
* @csspart tabs - The container that wraps the tabs.
|
|
|
|
* @csspart active-tab-indicator - The line that highlights the currently selected tab.
|
|
|
|
* @csspart body - The tab group's body where tab panels are slotted in.
|
|
|
|
* @csspart scroll-button - The previous/next scroll buttons that show when tabs are scrollable, an `<sl-icon-button>`.
|
|
|
|
* @csspart scroll-button--start - The starting scroll button.
|
|
|
|
* @csspart scroll-button--end - The ending scroll button.
|
|
|
|
* @csspart scroll-button__base - The scroll button's exported `base` part.
|
2021-06-24 22:24:54 +00:00
|
|
|
*
|
2021-08-18 21:54:35 +00:00
|
|
|
* @cssproperty --indicator-color - The color of the active tab indicator.
|
2022-12-06 16:18:14 +00:00
|
|
|
* @cssproperty --track-color - The color of the indicator's track (the line that separates tabs from panels).
|
2022-06-07 14:26:04 +00:00
|
|
|
* @cssproperty --track-width - The width of the indicator's track (the line that separates tabs from panels).
|
2020-07-15 21:30:37 +00:00
|
|
|
*/
|
2021-03-18 13:04:23 +00:00
|
|
|
@customElement('sl-tab-group')
|
2022-08-17 15:37:37 +00:00
|
|
|
export default class SlTabGroup extends ShoelaceElement {
|
2022-07-19 12:27:39 +00:00
|
|
|
static styles: CSSResultGroup = styles;
|
2022-01-16 05:47:14 +00:00
|
|
|
private readonly localize = new LocalizeController(this);
|
2021-03-06 17:01:39 +00:00
|
|
|
|
2022-01-16 05:47:14 +00:00
|
|
|
private activeTab?: SlTab;
|
2021-02-26 14:09:13 +00:00
|
|
|
private mutationObserver: MutationObserver;
|
|
|
|
private resizeObserver: ResizeObserver;
|
|
|
|
private tabs: SlTab[] = [];
|
2021-03-06 17:01:39 +00:00
|
|
|
private panels: SlTabPanel[] = [];
|
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
@query('.tab-group') tabGroup: HTMLElement;
|
|
|
|
@query('.tab-group__body') body: HTMLSlotElement;
|
|
|
|
@query('.tab-group__nav') nav: HTMLElement;
|
|
|
|
@query('.tab-group__indicator') indicator: HTMLElement;
|
|
|
|
|
2021-03-24 14:21:21 +00:00
|
|
|
@state() private hasScrollControls = false;
|
2020-08-03 11:40:02 +00:00
|
|
|
|
2020-07-15 21:30:37 +00:00
|
|
|
/** The placement of the tabs. */
|
2021-05-26 11:32:51 +00:00
|
|
|
@property() placement: 'top' | 'bottom' | 'start' | 'end' = 'top';
|
2020-07-15 21:30:37 +00:00
|
|
|
|
2021-04-05 20:58:33 +00:00
|
|
|
/**
|
|
|
|
* When set to auto, navigating tabs with the arrow keys will instantly show the corresponding tab panel. When set to
|
|
|
|
* manual, the tab will receive focus but will not show until the user presses spacebar or enter.
|
|
|
|
*/
|
|
|
|
@property() activation: 'auto' | 'manual' = 'auto';
|
|
|
|
|
2020-10-22 18:00:06 +00:00
|
|
|
/** Disables the scroll arrows that appear when tabs overflow. */
|
2021-07-01 00:04:46 +00:00
|
|
|
@property({ attribute: 'no-scroll-controls', type: Boolean }) noScrollControls = false;
|
2020-10-22 18:00:06 +00:00
|
|
|
|
2021-06-02 12:47:55 +00:00
|
|
|
connectedCallback() {
|
|
|
|
super.connectedCallback();
|
2020-07-15 21:30:37 +00:00
|
|
|
|
2021-03-11 14:11:06 +00:00
|
|
|
this.resizeObserver = new ResizeObserver(() => {
|
2021-03-11 14:20:54 +00:00
|
|
|
this.repositionIndicator();
|
|
|
|
this.updateScrollControls();
|
2021-03-11 14:11:06 +00:00
|
|
|
});
|
2020-08-03 11:40:02 +00:00
|
|
|
|
2020-07-30 21:27:43 +00:00
|
|
|
this.mutationObserver = new MutationObserver(mutations => {
|
2021-04-07 21:05:38 +00:00
|
|
|
// Update aria labels when the DOM changes
|
2022-01-16 05:47:14 +00:00
|
|
|
if (mutations.some(m => !['aria-labelledby', 'aria-controls'].includes(m.attributeName!))) {
|
2022-01-17 04:44:10 +00:00
|
|
|
setTimeout(() => this.setAriaLabels());
|
2020-07-30 21:27:43 +00:00
|
|
|
}
|
2021-04-07 21:05:38 +00:00
|
|
|
|
|
|
|
// Sync tabs when disabled states change
|
2021-06-02 12:47:55 +00:00
|
|
|
if (mutations.some(m => m.attributeName === 'disabled')) {
|
2021-04-07 21:05:38 +00:00
|
|
|
this.syncTabsAndPanels();
|
|
|
|
}
|
2020-07-30 21:27:43 +00:00
|
|
|
});
|
2021-06-02 12:47:55 +00:00
|
|
|
|
2022-01-17 04:44:10 +00:00
|
|
|
this.updateComplete.then(() => {
|
2021-06-02 12:47:55 +00:00
|
|
|
this.syncTabsAndPanels();
|
|
|
|
this.mutationObserver.observe(this, { attributes: true, childList: true, subtree: true });
|
|
|
|
this.resizeObserver.observe(this.nav);
|
|
|
|
|
|
|
|
// Set initial tab state when the tabs first become visible
|
|
|
|
const intersectionObserver = new IntersectionObserver((entries, observer) => {
|
|
|
|
if (entries[0].intersectionRatio > 0) {
|
|
|
|
this.setAriaLabels();
|
2022-01-16 05:47:14 +00:00
|
|
|
this.setActiveTab(this.getActiveTab() ?? this.tabs[0], { emitEvents: false });
|
2021-06-02 12:47:55 +00:00
|
|
|
observer.unobserve(entries[0].target);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
intersectionObserver.observe(this.tabGroup);
|
|
|
|
});
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
|
2021-03-06 17:01:39 +00:00
|
|
|
disconnectedCallback() {
|
2020-07-15 21:30:37 +00:00
|
|
|
this.mutationObserver.disconnect();
|
2020-08-03 11:40:02 +00:00
|
|
|
this.resizeObserver.unobserve(this.nav);
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
private getAllTabs(options: { includeDisabled: boolean } = { includeDisabled: true }) {
|
2022-01-16 05:47:14 +00:00
|
|
|
const slot = this.shadowRoot!.querySelector<HTMLSlotElement>('slot[name="nav"]')!;
|
2022-08-02 12:26:03 +00:00
|
|
|
|
|
|
|
return [...(slot.assignedElements() as SlTab[])].filter(el => {
|
|
|
|
return options.includeDisabled
|
|
|
|
? el.tagName.toLowerCase() === 'sl-tab'
|
|
|
|
: el.tagName.toLowerCase() === 'sl-tab' && !el.disabled;
|
|
|
|
});
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
private getAllPanels() {
|
2022-12-02 22:03:59 +00:00
|
|
|
return [...this.body.assignedElements()].filter(el => el.tagName.toLowerCase() === 'sl-tab-panel') as [SlTabPanel];
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
private getActiveTab() {
|
2021-02-26 14:09:13 +00:00
|
|
|
return this.tabs.find(el => el.active);
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
private handleClick(event: MouseEvent) {
|
2020-08-03 11:40:53 +00:00
|
|
|
const target = event.target as HTMLElement;
|
2022-01-16 05:47:14 +00:00
|
|
|
const tab = target.closest('sl-tab');
|
2021-01-18 18:34:41 +00:00
|
|
|
const tabGroup = tab?.closest('sl-tab-group');
|
2021-01-05 13:41:56 +00:00
|
|
|
|
|
|
|
// Ensure the target tab is in this tab group
|
2021-02-26 14:09:13 +00:00
|
|
|
if (tabGroup !== this) {
|
|
|
|
return;
|
2021-01-05 13:41:56 +00:00
|
|
|
}
|
2020-08-03 11:40:53 +00:00
|
|
|
|
2022-01-16 05:47:14 +00:00
|
|
|
if (tab !== null) {
|
2021-06-02 12:47:55 +00:00
|
|
|
this.setActiveTab(tab, { scrollBehavior: 'smooth' });
|
2020-08-03 11:40:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
private handleKeyDown(event: KeyboardEvent) {
|
2021-01-05 13:41:56 +00:00
|
|
|
const target = event.target as HTMLElement;
|
2022-01-16 05:47:14 +00:00
|
|
|
const tab = target.closest('sl-tab');
|
2021-01-18 18:34:41 +00:00
|
|
|
const tabGroup = tab?.closest('sl-tab-group');
|
2021-01-05 13:41:56 +00:00
|
|
|
|
|
|
|
// Ensure the target tab is in this tab group
|
2021-02-26 14:09:13 +00:00
|
|
|
if (tabGroup !== this) {
|
|
|
|
return;
|
2021-01-05 13:41:56 +00:00
|
|
|
}
|
|
|
|
|
2020-08-03 11:40:53 +00:00
|
|
|
// Activate a tab
|
|
|
|
if (['Enter', ' '].includes(event.key)) {
|
2022-01-16 05:47:14 +00:00
|
|
|
if (tab !== null) {
|
2021-06-02 12:47:55 +00:00
|
|
|
this.setActiveTab(tab, { scrollBehavior: 'smooth' });
|
2020-08-03 11:40:53 +00:00
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Move focus left or right
|
|
|
|
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) {
|
2022-08-25 13:37:20 +00:00
|
|
|
const activeEl = this.tabs.find(t => t.matches(':focus'));
|
2022-06-08 21:15:40 +00:00
|
|
|
const isRtl = this.localize.dir() === 'rtl';
|
2020-08-03 11:40:53 +00:00
|
|
|
|
2022-01-16 05:47:14 +00:00
|
|
|
if (activeEl?.tagName.toLowerCase() === 'sl-tab') {
|
2022-08-25 13:37:20 +00:00
|
|
|
let index = this.tabs.indexOf(activeEl);
|
2020-08-03 11:40:53 +00:00
|
|
|
|
|
|
|
if (event.key === 'Home') {
|
|
|
|
index = 0;
|
|
|
|
} else if (event.key === 'End') {
|
2021-02-26 14:09:13 +00:00
|
|
|
index = this.tabs.length - 1;
|
2021-08-27 12:26:48 +00:00
|
|
|
} else if (
|
2022-06-08 21:15:40 +00:00
|
|
|
(['top', 'bottom'].includes(this.placement) && event.key === (isRtl ? 'ArrowRight' : 'ArrowLeft')) ||
|
2021-08-27 12:26:48 +00:00
|
|
|
(['start', 'end'].includes(this.placement) && event.key === 'ArrowUp')
|
|
|
|
) {
|
2022-03-24 12:50:44 +00:00
|
|
|
index--;
|
2021-08-27 12:26:48 +00:00
|
|
|
} else if (
|
2022-06-08 21:15:40 +00:00
|
|
|
(['top', 'bottom'].includes(this.placement) && event.key === (isRtl ? 'ArrowLeft' : 'ArrowRight')) ||
|
2021-08-27 12:26:48 +00:00
|
|
|
(['start', 'end'].includes(this.placement) && event.key === 'ArrowDown')
|
|
|
|
) {
|
2022-03-24 12:50:44 +00:00
|
|
|
index++;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (index < 0) {
|
|
|
|
index = this.tabs.length - 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (index > this.tabs.length - 1) {
|
|
|
|
index = 0;
|
2020-08-03 11:40:53 +00:00
|
|
|
}
|
|
|
|
|
2021-03-29 11:16:40 +00:00
|
|
|
this.tabs[index].focus({ preventScroll: true });
|
2020-08-03 11:40:53 +00:00
|
|
|
|
2021-04-05 20:58:33 +00:00
|
|
|
if (this.activation === 'auto') {
|
2021-06-02 12:47:55 +00:00
|
|
|
this.setActiveTab(this.tabs[index], { scrollBehavior: 'smooth' });
|
2021-04-05 20:58:33 +00:00
|
|
|
}
|
|
|
|
|
2020-08-03 11:40:53 +00:00
|
|
|
if (['top', 'bottom'].includes(this.placement)) {
|
2021-02-26 14:09:13 +00:00
|
|
|
scrollIntoView(this.tabs[index], this.nav, 'horizontal');
|
2020-08-03 11:40:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
private handleScrollToStart() {
|
2020-08-03 11:40:02 +00:00
|
|
|
this.nav.scroll({
|
2022-06-08 21:15:40 +00:00
|
|
|
left:
|
|
|
|
this.localize.dir() === 'rtl'
|
|
|
|
? this.nav.scrollLeft + this.nav.clientWidth
|
|
|
|
: this.nav.scrollLeft - this.nav.clientWidth,
|
2020-08-03 11:40:02 +00:00
|
|
|
behavior: 'smooth'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
private handleScrollToEnd() {
|
2020-08-03 11:40:02 +00:00
|
|
|
this.nav.scroll({
|
2022-06-08 21:15:40 +00:00
|
|
|
left:
|
|
|
|
this.localize.dir() === 'rtl'
|
|
|
|
? this.nav.scrollLeft - this.nav.clientWidth
|
|
|
|
: this.nav.scrollLeft + this.nav.clientWidth,
|
2020-08-03 11:40:02 +00:00
|
|
|
behavior: 'smooth'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
private setActiveTab(tab: SlTab, options?: { emitEvents?: boolean; scrollBehavior?: 'auto' | 'smooth' }) {
|
2022-01-16 05:47:14 +00:00
|
|
|
options = {
|
|
|
|
emitEvents: true,
|
|
|
|
scrollBehavior: 'auto',
|
|
|
|
...options
|
|
|
|
};
|
2021-06-02 12:47:55 +00:00
|
|
|
|
2022-01-19 15:01:22 +00:00
|
|
|
if (tab !== this.activeTab && !tab.disabled) {
|
2020-07-15 21:30:37 +00:00
|
|
|
const previousTab = this.activeTab;
|
|
|
|
this.activeTab = tab;
|
|
|
|
|
2021-02-26 14:09:13 +00:00
|
|
|
// Sync active tab and panel
|
2022-01-19 15:01:22 +00:00
|
|
|
this.tabs.map(el => (el.active = el === this.activeTab));
|
|
|
|
this.panels.map(el => (el.active = el.name === this.activeTab?.panel));
|
2021-03-11 14:20:54 +00:00
|
|
|
this.syncIndicator();
|
2020-07-15 21:30:37 +00:00
|
|
|
|
|
|
|
if (['top', 'bottom'].includes(this.placement)) {
|
2021-06-02 12:47:55 +00:00
|
|
|
scrollIntoView(this.activeTab, this.nav, 'horizontal', options.scrollBehavior);
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Emit events
|
2022-01-19 15:01:22 +00:00
|
|
|
if (options.emitEvents) {
|
2022-01-19 14:37:07 +00:00
|
|
|
if (previousTab) {
|
2022-09-16 20:21:40 +00:00
|
|
|
this.emit('sl-tab-hide', { detail: { name: previousTab.panel } });
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
2022-01-19 15:01:22 +00:00
|
|
|
|
2022-09-16 20:21:40 +00:00
|
|
|
this.emit('sl-tab-show', { detail: { name: this.activeTab.panel } });
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
private setAriaLabels() {
|
2020-07-15 21:30:37 +00:00
|
|
|
// Link each tab with its corresponding panel
|
2022-01-16 05:47:14 +00:00
|
|
|
this.tabs.forEach(tab => {
|
|
|
|
const panel = this.panels.find(el => el.name === tab.panel);
|
2022-01-19 14:37:07 +00:00
|
|
|
if (panel) {
|
2022-01-16 05:47:14 +00:00
|
|
|
tab.setAttribute('aria-controls', panel.getAttribute('id')!);
|
|
|
|
panel.setAttribute('aria-labelledby', tab.getAttribute('id')!);
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
private repositionIndicator() {
|
2021-03-11 14:11:06 +00:00
|
|
|
const currentTab = this.getActiveTab();
|
|
|
|
|
2022-01-19 14:37:07 +00:00
|
|
|
if (!currentTab) {
|
2021-03-11 14:11:06 +00:00
|
|
|
return;
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
2021-03-11 14:11:06 +00:00
|
|
|
|
|
|
|
const width = currentTab.clientWidth;
|
|
|
|
const height = currentTab.clientHeight;
|
2022-06-07 14:26:04 +00:00
|
|
|
const isRtl = this.localize.dir() === 'rtl';
|
2022-02-13 21:50:11 +00:00
|
|
|
|
2022-02-22 14:12:39 +00:00
|
|
|
// We can't used offsetLeft/offsetTop here due to a shadow parent issue where neither can getBoundingClientRect
|
|
|
|
// because it provides invalid values for animating elements: https://bugs.chromium.org/p/chromium/issues/detail?id=920069
|
2022-07-20 19:29:19 +00:00
|
|
|
const allTabs = this.getAllTabs();
|
2022-02-13 21:53:13 +00:00
|
|
|
const precedingTabs = allTabs.slice(0, allTabs.indexOf(currentTab));
|
|
|
|
const offset = precedingTabs.reduce(
|
2022-02-13 21:50:11 +00:00
|
|
|
(previous, current) => ({
|
|
|
|
left: previous.left + current.clientWidth,
|
|
|
|
top: previous.top + current.clientHeight
|
|
|
|
}),
|
|
|
|
{ left: 0, top: 0 }
|
|
|
|
);
|
2021-03-11 14:11:06 +00:00
|
|
|
|
|
|
|
switch (this.placement) {
|
|
|
|
case 'top':
|
|
|
|
case 'bottom':
|
2021-03-11 14:20:54 +00:00
|
|
|
this.indicator.style.width = `${width}px`;
|
2021-03-22 21:26:56 +00:00
|
|
|
this.indicator.style.height = 'auto';
|
2022-12-01 20:38:59 +00:00
|
|
|
this.indicator.style.translate = isRtl ? `${-1 * offset.left}px` : `${offset.left}px`;
|
2021-03-11 14:11:06 +00:00
|
|
|
break;
|
|
|
|
|
2021-05-26 11:32:51 +00:00
|
|
|
case 'start':
|
|
|
|
case 'end':
|
2021-03-22 21:26:56 +00:00
|
|
|
this.indicator.style.width = 'auto';
|
2021-03-11 14:20:54 +00:00
|
|
|
this.indicator.style.height = `${height}px`;
|
2022-12-06 16:41:51 +00:00
|
|
|
this.indicator.style.translate = `0 ${offset.top}px`;
|
2021-03-11 14:11:06 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-07 21:05:38 +00:00
|
|
|
// This stores tabs and panels so we can refer to a cache instead of calling querySelectorAll() multiple times.
|
2023-01-03 20:04:07 +00:00
|
|
|
private syncTabsAndPanels() {
|
2022-08-02 12:26:03 +00:00
|
|
|
this.tabs = this.getAllTabs({ includeDisabled: false });
|
2021-02-26 14:09:13 +00:00
|
|
|
this.panels = this.getAllPanels();
|
2021-03-11 14:20:54 +00:00
|
|
|
this.syncIndicator();
|
2021-02-26 14:09:13 +00:00
|
|
|
}
|
|
|
|
|
2023-01-03 20:04:07 +00:00
|
|
|
@watch('noScrollControls', { waitUntilFirstUpdate: true })
|
|
|
|
updateScrollControls() {
|
|
|
|
if (this.noScrollControls) {
|
|
|
|
this.hasScrollControls = false;
|
|
|
|
} else {
|
|
|
|
this.hasScrollControls =
|
|
|
|
['top', 'bottom'].includes(this.placement) && this.nav.scrollWidth > this.nav.clientWidth;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@watch('placement', { waitUntilFirstUpdate: true })
|
|
|
|
syncIndicator() {
|
|
|
|
const tab = this.getActiveTab();
|
|
|
|
|
|
|
|
if (tab) {
|
|
|
|
this.indicator.style.display = 'block';
|
|
|
|
this.repositionIndicator();
|
|
|
|
} else {
|
|
|
|
this.indicator.style.display = 'none';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Shows the specified tab panel. */
|
|
|
|
show(panel: string) {
|
|
|
|
const tab = this.tabs.find(el => el.panel === panel);
|
|
|
|
|
|
|
|
if (tab) {
|
|
|
|
this.setActiveTab(tab, { scrollBehavior: 'smooth' });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-15 21:30:37 +00:00
|
|
|
render() {
|
2022-06-08 21:15:40 +00:00
|
|
|
const isRtl = this.localize.dir() === 'rtl';
|
|
|
|
|
2021-02-26 14:09:13 +00:00
|
|
|
return html`
|
2020-07-15 21:30:37 +00:00
|
|
|
<div
|
|
|
|
part="base"
|
2021-02-26 14:09:13 +00:00
|
|
|
class=${classMap({
|
2020-07-15 21:30:37 +00:00
|
|
|
'tab-group': true,
|
|
|
|
'tab-group--top': this.placement === 'top',
|
|
|
|
'tab-group--bottom': this.placement === 'bottom',
|
2021-05-26 11:32:51 +00:00
|
|
|
'tab-group--start': this.placement === 'start',
|
|
|
|
'tab-group--end': this.placement === 'end',
|
2022-06-07 14:26:04 +00:00
|
|
|
'tab-group--rtl': this.localize.dir() === 'rtl',
|
2020-10-22 18:00:06 +00:00
|
|
|
'tab-group--has-scroll-controls': this.hasScrollControls
|
2021-02-26 14:09:13 +00:00
|
|
|
})}
|
2021-03-06 17:01:39 +00:00
|
|
|
@click=${this.handleClick}
|
|
|
|
@keydown=${this.handleKeyDown}
|
2020-07-15 21:30:37 +00:00
|
|
|
>
|
2021-10-18 21:07:07 +00:00
|
|
|
<div class="tab-group__nav-container" part="nav">
|
2021-02-26 14:09:13 +00:00
|
|
|
${this.hasScrollControls
|
|
|
|
? html`
|
|
|
|
<sl-icon-button
|
2022-03-09 20:54:18 +00:00
|
|
|
part="scroll-button scroll-button--start"
|
|
|
|
exportparts="base:scroll-button__base"
|
2021-06-01 12:21:10 +00:00
|
|
|
class="tab-group__scroll-button tab-group__scroll-button--start"
|
2022-06-08 21:15:40 +00:00
|
|
|
name=${isRtl ? 'chevron-right' : 'chevron-left'}
|
2021-04-20 13:37:19 +00:00
|
|
|
library="system"
|
2022-01-16 05:47:14 +00:00
|
|
|
label=${this.localize.term('scrollToStart')}
|
2021-06-01 12:21:10 +00:00
|
|
|
@click=${this.handleScrollToStart}
|
2021-03-06 17:01:39 +00:00
|
|
|
></sl-icon-button>
|
2021-02-26 14:09:13 +00:00
|
|
|
`
|
|
|
|
: ''}
|
|
|
|
|
2021-10-18 21:07:07 +00:00
|
|
|
<div class="tab-group__nav">
|
2021-02-26 14:09:13 +00:00
|
|
|
<div part="tabs" class="tab-group__tabs" role="tablist">
|
2021-03-11 14:20:54 +00:00
|
|
|
<div part="active-tab-indicator" class="tab-group__indicator"></div>
|
2021-03-06 17:01:39 +00:00
|
|
|
<slot name="nav" @slotchange=${this.syncTabsAndPanels}></slot>
|
2020-08-03 11:40:02 +00:00
|
|
|
</div>
|
2020-07-15 21:30:37 +00:00
|
|
|
</div>
|
2021-02-26 14:09:13 +00:00
|
|
|
|
|
|
|
${this.hasScrollControls
|
|
|
|
? html`
|
|
|
|
<sl-icon-button
|
2022-03-09 20:54:18 +00:00
|
|
|
part="scroll-button scroll-button--end"
|
|
|
|
exportparts="base:scroll-button__base"
|
2021-06-01 12:21:10 +00:00
|
|
|
class="tab-group__scroll-button tab-group__scroll-button--end"
|
2022-06-08 21:15:40 +00:00
|
|
|
name=${isRtl ? 'chevron-left' : 'chevron-right'}
|
2021-04-20 13:37:19 +00:00
|
|
|
library="system"
|
2022-01-16 05:47:14 +00:00
|
|
|
label=${this.localize.term('scrollToEnd')}
|
2021-06-01 12:21:10 +00:00
|
|
|
@click=${this.handleScrollToEnd}
|
2021-03-06 17:01:39 +00:00
|
|
|
></sl-icon-button>
|
2021-02-26 14:09:13 +00:00
|
|
|
`
|
|
|
|
: ''}
|
2020-07-15 21:30:37 +00:00
|
|
|
</div>
|
|
|
|
|
2022-12-02 22:03:59 +00:00
|
|
|
<slot part="body" class="tab-group__body" @slotchange=${this.syncTabsAndPanels}></slot>
|
2020-07-15 21:30:37 +00:00
|
|
|
</div>
|
2021-02-26 14:09:13 +00:00
|
|
|
`;
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
}
|
2021-03-12 14:07:38 +00:00
|
|
|
|
2021-03-12 14:09:08 +00:00
|
|
|
declare global {
|
|
|
|
interface HTMLElementTagNameMap {
|
|
|
|
'sl-tab-group': SlTabGroup;
|
|
|
|
}
|
|
|
|
}
|