UNPKG

@oslokommune/punkt-elements

Version:

Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo

162 lines (136 loc) 5.41 kB
import { PktElementWithSlot } from '@/base-elements/element-with-slot' import { slotContent } from '@/directives/slot-content' import { html, PropertyValues } from 'lit' import { customElement, property, state } from 'lit/decorators.js' import { provide } from '@lit/context' import { tabsContext, type TabsContext } from './tabs-context' export interface IPktTabs { arrowNav?: boolean disableArrowNav?: boolean separatorIconName?: string separatorIconPath?: string } export class PktTabs extends PktElementWithSlot<IPktTabs> implements IPktTabs { @property({ type: Boolean, reflect: true, attribute: 'arrow-nav' }) arrowNav: boolean = true @property({ type: Boolean, reflect: true, attribute: 'disable-arrow-nav' }) disableArrowNav: boolean = false @property({ type: String, reflect: true, attribute: 'separator-icon-name' }) separatorIconName: string = '' @property({ type: String, reflect: true, attribute: 'separator-icon-path' }) separatorIconPath: string = '' @state() private tabRefs: Array<HTMLAnchorElement | HTMLButtonElement | null> = [] @state() private disabledMap: Record<number, boolean> = {} @state() private tabCount: number = 0 private get useArrowNav(): boolean { return this.arrowNav && !this.disableArrowNav } // Provide context to child tab items @provide({ context: tabsContext }) @state() private context: TabsContext = { useArrowNav: this.useArrowNav, registerTab: this.registerTab.bind(this), handleClick: this.handleClick.bind(this), handleKeyUp: this.handleKeyUp.bind(this), } // Update context when properties change updated(changedProperties: PropertyValues) { if (changedProperties.has('arrowNav') || changedProperties.has('disableArrowNav')) { this.context = { ...this.context, useArrowNav: this.useArrowNav, } } this.updateComplete.then(() => this.syncSeparators()) } private syncSeparators() { // Slotted tab items require post-render DOM sync for decorative separators. const list = this.querySelector('.pkt-tabs__list') if (!list) return list.querySelectorAll('.pkt-tabs__separator').forEach((separator) => separator.remove()) const hasSeparator = !!(this.separatorIconName || this.separatorIconPath) if (!hasSeparator) return const items = Array.from(list.children).filter((child) => !child.classList.contains('pkt-tabs__separator')) for (let i = 0; i < items.length - 1; i++) { const separator = document.createElement('span') separator.className = 'pkt-tabs__separator' separator.setAttribute('aria-hidden', 'true') separator.setAttribute('role', 'presentation') if (this.separatorIconPath) { const img = document.createElement('img') img.setAttribute('src', this.separatorIconPath) img.setAttribute('alt', '') img.setAttribute('aria-hidden', 'true') separator.appendChild(img) } else if (this.separatorIconName) { const icon = document.createElement('pkt-icon') icon.setAttribute('name', this.separatorIconName) icon.classList.add('pkt-tabs__separator-icon') separator.appendChild(icon) } items[i].after(separator) } } private registerTab(element: HTMLElement, index: number, disabled = false) { this.tabRefs[index] = element as HTMLAnchorElement | HTMLButtonElement this.disabledMap[index] = disabled this.tabCount = Math.max(this.tabCount, index + 1) } private isTabDisabled(index: number): boolean { return !!this.disabledMap[index] } private findEnabledIndex(startIndex: number, direction: -1 | 1): number | null { let current = startIndex + direction while (current >= 0 && current < this.tabCount) { if (!this.isTabDisabled(current)) return current current += direction } return null } private handleClick(index: number) { if (this.isTabDisabled(index)) return this.dispatchEvent( new CustomEvent('tab-selected', { detail: { index }, bubbles: true, composed: true, }), ) } private handleKeyUp(keyEvent: KeyboardEvent, index: number) { if (!this.useArrowNav) return if (keyEvent.key === 'ArrowLeft') { keyEvent.preventDefault() const previousEnabled = this.findEnabledIndex(index, -1) if (previousEnabled !== null) this.tabRefs[previousEnabled]?.focus() } if (keyEvent.key === 'ArrowRight') { keyEvent.preventDefault() const nextEnabled = this.findEnabledIndex(index, 1) if (nextEnabled !== null) this.tabRefs[nextEnabled]?.focus() } if ( keyEvent.key === 'Enter' || keyEvent.key === ' ' || keyEvent.key === 'Spacebar' || keyEvent.key === 'ArrowDown' ) { keyEvent.preventDefault() this.handleClick(index) } } render() { const role = this.useArrowNav ? 'tablist' : 'navigation' const hasSeparator = !!(this.separatorIconName || this.separatorIconPath) const tabsClass = hasSeparator ? 'pkt-tabs pkt-tabs--with-separator' : 'pkt-tabs' return html` <div class=${tabsClass}> <div class="pkt-tabs__list" role=${role}>${slotContent(this)}</div> </div> ` } } export default PktTabs try { customElement('pkt-tabs')(PktTabs) } catch (e) { console.warn('Forsøker å definere <pkt-tabs>, men den er allerede definert') }