import { html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { USWDSBaseComponent } from '../../utils/base-component.js';
import { initializeInPageNavigation } from './usa-in-page-navigation-behavior.js';
// Import official USWDS compiled CSS
import '../../styles/styles.css';
/**
* USA In-Page Navigation Web Component
*
* Minimal wrapper around USWDS in-page navigation functionality.
* Uses USWDS-mirrored behavior pattern for 100% behavioral parity.
*
* @element usa-in-page-navigation
* @fires nav-click - Dispatched when a navigation item is clicked
*
* @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-in-page-navigation/src/index.js
* @uswds-css-reference https://github.com/uswds/uswds/tree/develop/packages/usa-in-page-navigation/src/styles/_usa-in-page-navigation.scss
* @uswds-docs https://designsystem.digital.gov/components/in-page-navigation/
* @uswds-guidance https://designsystem.digital.gov/components/in-page-navigation/#guidance
* @uswds-accessibility https://designsystem.digital.gov/components/in-page-navigation/#accessibility
*/
/**
* Represents a navigation item
*/
export interface InPageNavItem {
id: string;
text: string;
href: string;
level?: number;
children?: InPageNavItem[];
}
/**
* Represents a section for manual navigation
*/
export interface InPageNavSection {
id: string;
label: string;
}
@customElement('usa-in-page-navigation')
export class USAInPageNavigation extends USWDSBaseComponent {
static override styles = css`
:host {
display: block;
}
:host([hidden]) {
display: none;
}
`;
@property({ type: String })
override title = 'On this page';
@property({ type: String })
titleHeadingLevel = '4';
@property({ type: String })
rootSelector = 'main';
@property({ type: String })
headingSelector = 'h2 h3';
@property({ type: Boolean, reflect: true })
smoothScroll = true;
@property({ type: Number })
scrollOffset = 0;
@property({ type: String })
threshold = '0.5';
@property({ type: String })
rootMargin = '0px 0px -50% 0px';
@property({ type: Array, hasChanged: () => true })
sections: InPageNavSection[] = [];
// Alias for sections (alternative property name)
@property({ type: Array, hasChanged: () => true })
get items(): InPageNavItem[] {
return this.sections as unknown as InPageNavItem[];
}
set items(value: InPageNavItem[]) {
this.sections = value as unknown as InPageNavSection[];
}
// Alias for sections (alternative property name for compatibility)
@property({ type: Array, hasChanged: () => true })
get links(): InPageNavItem[] {
return this.sections as unknown as InPageNavItem[];
}
set links(value: InPageNavItem[]) {
this.sections = value as unknown as InPageNavSection[];
}
// Store cleanup function from behavior
private cleanup?: () => void;
override connectedCallback() {
super.connectedCallback();
// Set web component managed flag to prevent USWDS auto-initialization conflicts
this.setAttribute('data-web-component-managed', 'true');
}
// Observe data attributes and sync to properties
static override get observedAttributes() {
return [
'data-title-text',
'data-title-heading-level',
'data-heading-elements',
'data-main-content-selector',
'data-scroll-offset',
'data-threshold',
'data-root-margin',
];
}
override attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
super.attributeChangedCallback(name, oldValue, newValue);
// Sync data attributes to properties when they change
if (name === 'data-title-text' && newValue !== null) {
this.title = newValue;
} else if (name === 'data-title-heading-level' && newValue !== null) {
this.titleHeadingLevel = newValue.replace('h', '');
} else if (name === 'data-heading-elements' && newValue !== null) {
this.headingSelector = newValue;
} else if (name === 'data-main-content-selector' && newValue !== null) {
this.rootSelector = newValue;
} else if (name === 'data-scroll-offset' && newValue !== null) {
this.scrollOffset = Number(newValue);
} else if (name === 'data-threshold' && newValue !== null) {
this.threshold = newValue;
} else if (name === 'data-root-margin' && newValue !== null) {
this.rootMargin = newValue;
}
}
override async firstUpdated(changedProperties: Map) {
// ARCHITECTURE: Script Tag Pattern
// USWDS is loaded globally via script tag in .storybook/preview-head.html
// Components just render HTML - USWDS enhances automatically via window.USWDS
// ARCHITECTURE: USWDS-Mirrored Behavior Pattern
// Uses dedicated behavior file (usa-in-page-navigation-behavior.ts) that replicates USWDS source exactly
super.firstUpdated(changedProperties);
// Wait for DOM to be fully rendered
await this.updateComplete;
await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined)));
// Initialize using mirrored USWDS behavior
this.cleanup = initializeInPageNavigation(this);
}
override disconnectedCallback() {
super.disconnectedCallback();
this.cleanup?.();
}
private renderHeading() {
switch (this.titleHeadingLevel) {
case '1':
return html`${this.title} `;
case '2':
return html`${this.title} `;
case '3':
return html`${this.title} `;
case '4':
return html`${this.title} `;
case '5':
return html`${this.title} `;
case '6':
return html`${this.title} `;
default:
return html`${this.title} `;
}
}
private renderManualNavigation() {
if (!this.sections || this.sections.length === 0) {
return '';
}
return html`
${this.renderHeading()}
${this.renderSectionItems()}
`;
}
private renderSectionItems() {
return this.sections.map((section) => this.renderSectionItem(section));
}
private renderSectionItem(section: InPageNavSection | InPageNavItem) {
// Support both InPageNavSection (id/label) and InPageNavItem (text/href) formats
const item = section as any;
const href = item.href || `#${item.id}`;
const text = item.text || item.label || '';
return html`
${text}
`;
}
// Use light DOM for USWDS compatibility
protected override createRenderRoot(): HTMLElement {
return this as any;
}
override render() {
// If sections are provided manually, render the structure
if (this.sections && this.sections.length > 0) {
return html`
${this.renderManualNavigation()}
`;
}
// Render empty container - USWDS will populate it with navigation
return html`
`;
}
}