import { html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { USWDSBaseComponent } from '../../utils/base-component.js';
// Import official USWDS compiled CSS
import '../../styles/styles.css';
export interface SkipLinkDetail {
href: string;
text: string;
}
/**
* USA Skip Link Web Component
*
* Implements the official USWDS skip link behavior pattern.
* Based on the USWDS JavaScript implementation for consistent functionality.
*
* @element usa-skip-link
*
* @see README.mdx - Complete API documentation, usage examples, and implementation notes
* @see CHANGELOG.mdx - Component version history and breaking changes
* @see TESTING.mdx - Testing documentation and coverage reports
*
* @uswds-js-reference https://github.com/uswds/uswds/tree/develop/packages/usa-skip-link/src/index.js
* @uswds-css-reference https://github.com/uswds/uswds/tree/develop/packages/usa-skip-link/src/styles/_usa-skip-link.scss
* @uswds-docs https://designsystem.digital.gov/components/skip-link/
* @uswds-guidance https://designsystem.digital.gov/components/skip-link/#guidance
* @uswds-accessibility https://designsystem.digital.gov/components/skip-link/#accessibility
*/
@customElement('usa-skip-link')
export class USASkipLink extends USWDSBaseComponent {
static override styles = css`
:host {
display: block;
}
`;
@property({ type: String })
href = '#main-content';
@property({ type: String })
text = 'Skip to main content';
@property({ type: Boolean, reflect: true })
multiple = false;
// Store USWDS module for cleanup
private uswdsModule: any = null;
private usingUSWDSEnhancement = false;
// Light DOM is handled by USWDSBaseComponent
override connectedCallback() {
super.connectedCallback();
// Set web component managed flag to prevent USWDS auto-initialization conflicts
this.setAttribute('data-web-component-managed', 'true');
console.log('🔗 Skip Link: Using USWDS pattern (no external JavaScript needed)');
// Initialize progressive enhancement
this.initializeUSWDSSkipLink();
}
override disconnectedCallback() {
super.disconnectedCallback();
console.log('🔗 Skip Link: Cleaning up USWDS patterns');
this.cleanupUSWDS();
// Clean up timeout to prevent memory leaks and DOM access after disconnect
if (this.timeoutId !== null) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
// Reset enhancement flag to allow reinitialization
this.usingUSWDSEnhancement = false;
}
}
// USWDS-style skip link methods
// Based on: https://github.com/uswds/uswds/blob/develop/packages/usa-skip-link/src/index.js
private timeoutId: number | null = null;
private getSkipLinkClasses(): string {
const classes = ['usa-skipnav'];
if (this.multiple) {
classes.push('usa-skipnav--multiple');
}
return classes.join(' ');
}
private async initializeUSWDSSkipLink() {
// Prevent multiple initializations
if (this.usingUSWDSEnhancement) {
console.log(`⚠️ ${this.constructor.name}: Already initialized, skipping duplicate initialization`);
return;
}
console.log(
'🎯 Skip Link: Initializing USWDS skipnav for focus management and accessibility'
);
try {
// Use standardized USWDS loader utility for consistency with other components
const { initializeUSWDSComponent } = await import('../../utils/uswds-loader.js');
await this.updateComplete;
const skipLinkElement = this.querySelector('.usa-skipnav');
if (skipLinkElement) {
// Let USWDS handle skip link clicks and focus management using standard loader
this.uswdsModule = await initializeUSWDSComponent(skipLinkElement, 'skipnav');
this.usingUSWDSEnhancement = true;
console.log(
'✅ USWDS skipnav initialized successfully - focus management handled by USWDS'
);
return; // USWDS owns all skip link behavior now
} else {
console.warn('⚠️ Skip Link: No .usa-skipnav element found, using fallback focus management');
this.setupFallbackBehavior();
}
} catch (error) {
console.warn('🔧 Skip Link: USWDS integration failed, using fallback:', error);
await this.loadFullUSWDSLibrary();
}
}
/**
* Fallback: Check for existing USWDS or use component fallback
*/
private async loadFullUSWDSLibrary(): Promise {
// Check if USWDS is already loaded globally
if (typeof window !== 'undefined' && typeof (window as any).USWDS !== 'undefined') {
console.log(`📦 USWDS already available globally`);
this.initializeWithGlobalUSWDS();
return;
}
// If not in browser environment, use fallback
if (typeof window === 'undefined' || typeof document === 'undefined') {
console.log(`📦 Not in browser environment, using component fallback`);
this.setupFallbackBehavior();
return;
}
console.log(`📦 USWDS not available, using component fallback behavior`);
this.setupFallbackBehavior();
}
/**
* Initialize using global USWDS object (fallback mode)
*/
private initializeWithGlobalUSWDS() {
// Prevent multiple initializations
if (this.usingUSWDSEnhancement) {
console.log(`⚠️ Skip Link: Already initialized globally, skipping duplicate initialization`);
return;
}
if (typeof window !== 'undefined' && typeof (window as any).USWDS !== 'undefined') {
const USWDS = (window as any).USWDS;
if (USWDS['skip-link'] && typeof USWDS['skip-link'].on === 'function') {
USWDS['skip-link'].on(this);
this.usingUSWDSEnhancement = true;
console.log(`🎯 USWDS skip link initialized (fallback mode)`);
}
}
}
/**
* Setup basic component functionality without USWDS enhancement
*/
private setupFallbackBehavior() {
console.log(`🔍 Setting up basic skip link functionality`);
// Component already has full fallback behavior implemented
console.log(`🎯 Skip Link ready with basic functionality`);
}
/**
* Clean up USWDS module on component destruction
*/
private async cleanupUSWDS() {
try {
const { cleanupUSWDSComponent } = await import('../../utils/uswds-loader.js');
cleanupUSWDSComponent(this, this.uswdsModule);
} catch (error) {
console.warn('⚠️ Skip Link: Error importing cleanup utility:', error);
}
this.uswdsModule = null;
this.usingUSWDSEnhancement = false;
}
// Use light DOM for USWDS compatibility
protected override createRenderRoot(): HTMLElement {
return this as any;
}
private handleClick(e: Event) {
e.preventDefault();
// Dispatch custom event
this.dispatchEvent(
new CustomEvent('skip-link-click', {
detail: {
href: this.href,
text: this.text,
},
bubbles: true,
composed: true,
})
);
// Focus target element
this.focusTarget();
}
private focusTarget() {
const target = this.getTargetElement();
if (target) {
// Add tabindex if not present to make element focusable
if (!target.hasAttribute('tabindex')) {
target.setAttribute('tabindex', '-1');
}
target.focus();
}
}
override render() {
return html`
${this.text}
`;
}
// Public API methods
override focus() {
const link = this.querySelector('a');
if (link) {
link.focus();
}
}
setHref(href: string) {
this.href = href;
}
setText(text: string) {
this.text = text;
}
setMultiple(multiple: boolean) {
this.multiple = multiple;
}
getTargetElement(): HTMLElement | null {
// Guard against accessing document when component is not connected
if (!this.isConnected) return null;
// Guard against malformed selectors
try {
return document.querySelector(this.href);
} catch (error) {
console.warn(`Skip link: Invalid selector "${this.href}"`, error);
return null;
}
}
}