/** * Copyright Aquera Inc 2023 * * This source code is licensed under the BSD-3-Clause license found in the * LICENSE file in the root directory of this source tree. */ import { html } from 'lit'; import { styles } from './nile-dot-stepper.css'; import { classMap } from 'lit/directives/class-map.js'; import { customElement, property } from 'lit/decorators.js'; import NileElement, { setupA11yMetadata } from '../internal/nile-element'; import type { CSSResultGroup, TemplateResult } from 'lit'; import { A11yProperty } from '../internal/accessibility/a11y.property.enum'; import { A11yRole } from '../internal/accessibility/a11y.role.enum'; import { Role } from '../internal/accessibility/role.enum'; /** * Nile dot stepper component. * * @tag nile-dot-stepper * */ /** * @summary Dot steppers display a horizontal row of small dots indicating progress or position, with one active dot and the rest inactive. * @status stable * * @event nile-step-change - Emitted when a dot is selected via click or keyboard. The `detail` contains the 1-based `value` of the selected dot. * * @csspart base - The component's base wrapper (the dots container). * @csspart dot - An individual dot button. */ @customElement('nile-dot-stepper') export class NileDotStepper extends NileElement { static styles: CSSResultGroup = styles; connectedCallback(): void { super.connectedCallback(); setupA11yMetadata(this, { [Role.Role]: A11yRole.Tablist, [A11yProperty.AriaLabel]: 'Progress', }); } /** The total number of dots. */ @property({ type: Number, reflect: true }) total = 0; /** The 1-based index of the active dot. */ @property({ type: Number, reflect: true }) current = 1; /** The size of each dot. */ @property({ reflect: true }) size: 'sm' | 'md' | 'lg' = 'md'; /** Disables click and keyboard interaction. */ @property({ type: Boolean, reflect: true }) disabled = false; /** Clamps the active value to the valid `[1, total]` range. */ private clamp(value: number): number { if (this.total < 1) return 1; return Math.min(Math.max(value, 1), this.total); } private selectStep(value: number): void { if (this.disabled) return; const next = this.clamp(value); this.current = next; this.emit('nile-step-change', { value: next }); } private handleDotClick(value: number): void { this.selectStep(value); } private focusDot(value: number): void { const dot = this.shadowRoot?.querySelector( `[part="dot"][data-index="${value}"]` ); dot?.focus(); } private handleKeyDown(event: KeyboardEvent): void { if (this.disabled || this.total < 1) return; const active = this.clamp(this.current); let next: number | null = null; switch (event.key) { case 'ArrowRight': next = this.clamp(active + 1); break; case 'ArrowLeft': next = this.clamp(active - 1); break; case 'Home': next = 1; break; case 'End': next = this.total; break; default: return; } event.preventDefault(); if (next !== null) { this.selectStep(next); this.focusDot(next); } } render(): TemplateResult { const active = this.clamp(this.current); return html`
${Array.from({ length: this.total }).map((_, index) => { const value = index + 1; const isActive = value === active; return html` `; })}
`; } } export default NileDotStepper; declare global { interface HTMLElementTagNameMap { 'nile-dot-stepper': NileDotStepper; } }