import { WebComponent, enable, string, type PropChange } from '../src/index.ts'; import { register } from './main-context.ts'; import css from './secret-note.css' with { type: 'css' }; import template from './secret-note.html' with { type: 'text' }; /** * Closed shadow root built from `template`: `el.shadowRoot` is `null` from the * outside, but the component still reaches its own DOM through `this.root` and * `this.refs`. Closed hides the internals, not the data; `message` is still a * reflected attribute. */ const SPEC = { shadow: 'closed', template, css, element: { toggle: 'button', hint: '.hint', lock: '.lock', message: 'p.message', }, props: { message: string({ default: '' }), revealed: enable(), }, } as const; export class SecretNote extends WebComponent(SPEC) { override onConnect(): void { // Not `events: { click: true }`: a listener on the host can't see into a closed // root (`composedPath()` stops at the host), so listen on the button itself. this.refs.toggle?.addEventListener('click', this, { signal: this.signal }); this.#render(); register(this, { onSecret: (secret) => { this.message = secret; }, }); } override onPropChange(_change: PropChange): void { this.#render(); } override handleEvent(): void { this.revealed = !this.revealed; } #render(): void { const { toggle, hint, lock, message } = this.refs; if (message) message.textContent = this.message; if (hint) hint.textContent = this.revealed ? 'Hide' : 'Click to reveal'; if (lock) lock.textContent = this.revealed ? '🔓' : '🔒'; toggle?.setAttribute('aria-expanded', String(this.revealed)); } } customElements.define('secret-note', SecretNote); declare global { interface HTMLElementTagNameMap { 'secret-note': SecretNote; } }