From 7cbcb84aa7df8b82ed1dec63db865eb0bc4ebcd4 Mon Sep 17 00:00:00 2001 From: Elliott Marquez <5981958+e111077@users.noreply.github.com> Date: Thu, 2 Oct 2025 12:18:00 -0700 Subject: [PATCH] feat(events): re-fire the tabchange and change events to expose them as non-bubling --- README.md | 19 +++++++++++++++++++ src/playground-file-editor.ts | 7 +++++-- src/playground-ide.ts | 13 +++++++++++++ src/playground-tab-bar.ts | 8 ++++++++ src/shared/util.ts | 20 ++++++++++++++++++++ 5 files changed, 65 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 40c39bc1..0e129c6a 100644 --- a/README.md +++ b/README.md @@ -756,6 +756,13 @@ All-in-one project, editor, file switcher, and preview with a horizontal side-by | `default` | Inline files ([details](#inline-scripts)). | | `extensions` | Declarative CodeMirror extensions ([details](#extending-the-editor)). | +### Events + +| Event | Description | +| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `tabchange` | A tab has been activated. | +| `change` | User made an edit to the active file (note: this event is not fired for programmatic changes to the value property nor for the user changing tabs) | + --- ## `` @@ -813,6 +820,12 @@ project element. --- +### Events + +| Event | Description | +| ----------- | ------------------------- | +| `tabchange` | A tab has been activated. | + ## `` ### Properties @@ -834,6 +847,12 @@ project element. | ------------ | --------------------------------------------------------------------- | | `extensions` | Declarative CodeMirror extensions ([details](#extending-the-editor)). | +### Events + +| Event | Description | +| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `change` | User made an edit to the active file (note: this event is not fired for programmatic changes to the value property nor for the user changing tabs) | + --- ## `` diff --git a/src/playground-file-editor.ts b/src/playground-file-editor.ts index 6235d4f7..0467161b 100644 --- a/src/playground-file-editor.ts +++ b/src/playground-file-editor.ts @@ -15,6 +15,7 @@ import {Extension} from '@codemirror/state'; import {PlaygroundProject} from './playground-project.js'; import {PlaygroundCodeEditor} from './playground-code-editor.js'; import {CodeEditorChangeData} from './shared/worker-api.js'; +import {refireEvent} from './shared/util.js'; /** * A text editor associated with a . @@ -172,7 +173,7 @@ export class PlaygroundFileEditor extends PlaygroundConnectedElement { )} .extensions=${this.extensions} .noCompletions=${this.noCompletions} - @change=${this._onEdit} + @change=${this._onChange} @request-completions=${this._onRequestCompletions} > @@ -197,7 +198,7 @@ export class PlaygroundFileEditor extends PlaygroundConnectedElement { this.requestUpdate(); }; - private _onEdit() { + private _onChange(event: Event) { if ( this._project === undefined || this._currentFile === undefined || @@ -206,6 +207,8 @@ export class PlaygroundFileEditor extends PlaygroundConnectedElement { return; } this._project.editFile(this._currentFile, this._editor.value); + // Re-fire the change event on the host element for external consumers + refireEvent(this, event); } private async _onRequestCompletions(e: CustomEvent) { diff --git a/src/playground-ide.ts b/src/playground-ide.ts index ed25f439..acf54af4 100644 --- a/src/playground-ide.ts +++ b/src/playground-ide.ts @@ -15,6 +15,7 @@ import './playground-preview.js'; import {PlaygroundProject} from './playground-project.js'; import {ProjectManifest} from './shared/worker-api.js'; import {npmVersion, serviceWorkerHash} from './shared/version.js'; +import {refireEvent} from './shared/util.js'; /** * A multi-file code editor component with live preview that works without a @@ -327,6 +328,7 @@ export class PlaygroundIde extends LitElement { .project=${projectId} .editor=${editorId} .editableFileSystem=${this.editableFileSystem} + @tabchange=${this._onTabChange} > @@ -339,6 +341,7 @@ export class PlaygroundIde extends LitElement { .pragmas=${this.pragmas} .noCompletions=${this.noCompletions} .extensions=${this.extensions} + @change=${this._onChange} > @@ -388,6 +391,16 @@ export class PlaygroundIde extends LitElement { super.update(changedProperties); } + private _onTabChange(event: Event) { + // Re-fire the tabchange event on the host element for external consumers + refireEvent(this, event); + } + + private _onChange(event: Event) { + // Re-fire the change event on the host element for external consumers + refireEvent(this, event); + } + private _onResizeBarPointerdown({pointerId}: PointerEvent) { const bar = this._resizeBar; bar.setPointerCapture(pointerId); diff --git a/src/playground-tab-bar.ts b/src/playground-tab-bar.ts index 7aeb07d7..5659d470 100644 --- a/src/playground-tab-bar.ts +++ b/src/playground-tab-bar.ts @@ -18,6 +18,7 @@ import {PlaygroundConnectedElement} from './playground-connected-element.js'; import {PlaygroundFileEditor} from './playground-file-editor.js'; import {PlaygroundFileSystemControls} from './playground-file-system-controls.js'; import {FilesChangedEvent, PlaygroundProject} from './playground-project.js'; +import {refireEvent} from './shared/util.js'; import {PlaygroundInternalTab} from './internal/tab.js'; /** @@ -257,6 +258,13 @@ export class PlaygroundTabBar extends PlaygroundConnectedElement { if (name !== this._activeFileName) { this._activeFileName = name; this._activeFileIndex = index; + // Re-fire the tabchange event on the host element for external consumers + refireEvent( + this, + new CustomEvent('tabchange', { + detail: {filename: name}, + }) + ); } } diff --git a/src/shared/util.ts b/src/shared/util.ts index 2ed6aab3..b5a7a30b 100644 --- a/src/shared/util.ts +++ b/src/shared/util.ts @@ -28,3 +28,23 @@ export const forceSkypackRawMode = (url: URL): URL => { export type Result = | {result: V; error?: undefined} | {result?: undefined; error: E}; + +/** + * Re-fires an event on the given element, without bubbles or composed flags. + * This is useful for re-firing internal events on public component boundaries + * to avoid them crossing shadow DOM boundaries while still making them available + * to external consumers. + */ +export const refireEvent = ( + element: EventTarget, + event: Event | CustomEvent +): void => { + const detail = (event as CustomEvent).detail; + element.dispatchEvent( + new CustomEvent(event.type, { + detail, + bubbles: false, + composed: false, + }) + ); +};