Add typed events map: true, listener options, listen<E>(), or custom<T>()
events maps a name to true, listener options, listen<E>(options?) for an explicit event type, or custom<T>(options?) for a CustomEvent<T> (the same as listen<CustomEvent<T>>()). Otherwise an event is typed from HTMLElementEventMap by name. The array form is gone. handleEvent gets HandledEvent<S>, each event tagged with its literal type so event.type narrows.
This commit is contained in:
parent
6ece00c8cc
commit
be8ee0198f
@ -9,7 +9,7 @@ const SPEC = {
|
|||||||
shadow: 'open', // omit for light DOM
|
shadow: 'open', // omit for light DOM
|
||||||
template: '<input class="name"><button>Go</button>',
|
template: '<input class="name"><button>Go</button>',
|
||||||
css: ':host { display: block; }',
|
css: ':host { display: block; }',
|
||||||
events: ['click'],
|
events: { click: true },
|
||||||
element: {
|
element: {
|
||||||
input: 'input.name', // refs.input: HTMLInputElement | null (from the tag)
|
input: 'input.name', // refs.input: HTMLInputElement | null (from the tag)
|
||||||
thing: '.thing', // refs.thing: Element | null (no tag to go on)
|
thing: '.thing', // refs.thing: Element | null (no tag to go on)
|
||||||
@ -47,6 +47,7 @@ ExampleElement.observedAttributes; // readonly ('user-id' | 'min' | 'is-ready' |
|
|||||||
- **Light DOM**: `template` is appended on first connect; `css` is added once per element type, as a `<style>` in `<head>` or adopted into the enclosing shadow root.
|
- **Light DOM**: `template` is appended on first connect; `css` is added once per element type, as a `<style>` in `<head>` or adopted into the enclosing shadow root.
|
||||||
- **Refs**: a selector string is typed from its tag (last compound: `'form > button#go'` → `HTMLButtonElement`), falling back to `Element`. `[selector, Class]` types the ref as `Class`; reading it throws a `TypeError` if a match isn't an instance of `Class`. A key can't be in both `element` and `elements`.
|
- **Refs**: a selector string is typed from its tag (last compound: `'form > button#go'` → `HTMLButtonElement`), falling back to `Element`. `[selector, Class]` types the ref as `Class`; reading it throws a `TypeError` if a match isn't an instance of `Class`. A key can't be in both `element` and `elements`.
|
||||||
- **`signal`** aborts on disconnect; a new one is created on each connect. `events` are listened on the host with it.
|
- **`signal`** aborts on disconnect; a new one is created on each connect. `events` are listened on the host with it.
|
||||||
|
- **Events**: `events` maps a name to `true`, listener options, `listen<E>(options?)` for an explicit event type, or `custom<T>(options?)` for a `CustomEvent<T>` (the same as `listen<CustomEvent<T>>()`): `{ click: true, scroll: { passive: true }, 'wc:key': listen<KeyboardEvent>(), 'wc:pick': custom<number>() }`. Other names are typed from `HTMLElementEventMap` (`click` → `PointerEvent`), else `Event`. `handleEvent` receives `HandledEvent<typeof SPEC>`, a union whose `type` is literal, so `if (event.type === 'wc:pick') event.detail` narrows. A host listener can't see into a closed shadow root, so `composedPath()` stops at the host there.
|
||||||
- **`base`** extends another autonomous element class; prop names that collide with its members (or `HTMLElement`'s, like `title`/`hidden`) are type errors.
|
- **`base`** extends another autonomous element class; prop names that collide with its members (or `HTMLElement`'s, like `title`/`hidden`) are type errors.
|
||||||
- Don't declare a subclass field with a prop's name; it would shadow the reflected accessor.
|
- Don't declare a subclass field with a prop's name; it would shadow the reflected accessor.
|
||||||
|
|
||||||
|
|||||||
111
src/events.ts
Normal file
111
src/events.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import type { Spec } from './types.ts';
|
||||||
|
|
||||||
|
/** Listener options for a host event. `signal` is the element's own and can't be set. */
|
||||||
|
export type EventOptions = Omit<AddEventListenerOptions, 'signal'>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event with an explicit type, made by {@link listen} or {@link custom}.
|
||||||
|
* `E` is type-only: it's what `handleEvent` receives for this entry; `never`
|
||||||
|
* means "type it from the name", like `true`.
|
||||||
|
*/
|
||||||
|
export interface EventDef<E extends Event = Event> {
|
||||||
|
readonly options: EventOptions | undefined;
|
||||||
|
/** Type-only carrier for `E`; never set at runtime. */
|
||||||
|
readonly event?: E;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One entry in a spec's `events` map:
|
||||||
|
*
|
||||||
|
* - `true` — listen; the event is typed from its name (`click` →
|
||||||
|
* `PointerEvent`), or `Event` for a name `HTMLElementEventMap` doesn't know.
|
||||||
|
* - `{ passive: true }` — the same, with listener options.
|
||||||
|
* - `listen<E>(options?)` — an event of type `E`, for a name
|
||||||
|
* `HTMLElementEventMap` doesn't know (or doesn't type the way it's dispatched).
|
||||||
|
* - `custom<T>(options?)` — a `CustomEvent<T>`; short for `listen<CustomEvent<T>>()`.
|
||||||
|
*/
|
||||||
|
export type EventEntry = true | EventOptions | EventDef;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event of type `E`, for a spec's `events` map. Without a type argument
|
||||||
|
* it's the same as `true` (or the options object on its own):
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* events: {
|
||||||
|
* 'wc:key': listen<KeyboardEvent>(),
|
||||||
|
* 'wc:pick': listen<CustomEvent<number>>({ capture: true }),
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
// `E` is only carried in the return type, for `handleEvent`; that's its purpose.
|
||||||
|
// `NoInfer` keeps the spec's contextual `EventDef<Event>` from inferring
|
||||||
|
// `E = Event`, so an untyped `listen()` stays `never` and is typed from its name.
|
||||||
|
// oxlint-disable-next-line no-unnecessary-type-parameters
|
||||||
|
export function listen<E extends Event = never>(options?: EventOptions): EventDef<NoInfer<E>> {
|
||||||
|
return { options };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A `CustomEvent` whose `detail` is `T`; the same as `listen<CustomEvent<T>>()`:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* events: {
|
||||||
|
* click: true,
|
||||||
|
* scroll: { passive: true },
|
||||||
|
* 'wc:pick': custom<number>(),
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
// oxlint-disable-next-line no-unnecessary-type-parameters
|
||||||
|
export function custom<T>(options?: EventOptions): EventDef<CustomEvent<T>> {
|
||||||
|
return listen<CustomEvent<T>>(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Types pulled out of a spec
|
||||||
|
|
||||||
|
/** The built-in type for an event name, or `Event` for a name `HTMLElementEventMap` doesn't know. */
|
||||||
|
type NamedEvent<N extends string> = N extends keyof HTMLElementEventMap
|
||||||
|
? HTMLElementEventMap[N]
|
||||||
|
: Event;
|
||||||
|
|
||||||
|
/** The event one `events` entry delivers: its explicit type, else the one its name gives. */
|
||||||
|
export type EventOf<N extends string, D> =
|
||||||
|
D extends EventDef<infer E> ? ([E] extends [never] ? NamedEvent<N> : E) : NamedEvent<N>;
|
||||||
|
|
||||||
|
/** No events given: an empty map, so `HandledEvent` is `never`. Empty on purpose. */
|
||||||
|
// oxlint-disable-next-line no-generated-empty-object-type
|
||||||
|
type NoEvents = Record<never, never>;
|
||||||
|
|
||||||
|
/** Event name → event type for a spec. */
|
||||||
|
export type EventMapOf<S> = S extends {
|
||||||
|
readonly events: infer M extends Readonly<Record<string, EventEntry>>;
|
||||||
|
}
|
||||||
|
? { [K in keyof M & string]: EventOf<K, M[K]> }
|
||||||
|
: NoEvents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What `handleEvent` receives: a union of the listed events, each tagged with
|
||||||
|
* its literal `type` so `switch (event.type)` narrows to that event (`type` is
|
||||||
|
* plain `string` on every DOM event otherwise).
|
||||||
|
*/
|
||||||
|
export type HandledEvent<S> = {
|
||||||
|
[K in keyof EventMapOf<S>]: EventMapOf<S>[K] & { readonly type: K };
|
||||||
|
}[keyof EventMapOf<S>];
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Runtime
|
||||||
|
|
||||||
|
function isEventDef(entry: EventOptions | EventDef): entry is EventDef {
|
||||||
|
return 'options' in entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `events` as `[type, options]` pairs, for `addEventListener`. */
|
||||||
|
export function resolveEvents(
|
||||||
|
events: Spec['events'],
|
||||||
|
): [type: string, options: EventOptions | undefined][] {
|
||||||
|
return Object.entries(events ?? {}).map(([type, entry]) => {
|
||||||
|
if (entry === true) return [type, undefined];
|
||||||
|
return [type, isEventDef(entry) ? entry.options : entry];
|
||||||
|
});
|
||||||
|
}
|
||||||
10
src/index.ts
10
src/index.ts
@ -1,4 +1,14 @@
|
|||||||
export { WebComponent } from './web-component.ts';
|
export { WebComponent } from './web-component.ts';
|
||||||
|
export {
|
||||||
|
custom,
|
||||||
|
listen,
|
||||||
|
type EventDef,
|
||||||
|
type EventEntry,
|
||||||
|
type EventMapOf,
|
||||||
|
type EventOf,
|
||||||
|
type EventOptions,
|
||||||
|
type HandledEvent,
|
||||||
|
} from './events.ts';
|
||||||
export {
|
export {
|
||||||
attributeName,
|
attributeName,
|
||||||
choice,
|
choice,
|
||||||
|
|||||||
20
src/types.ts
20
src/types.ts
@ -1,3 +1,4 @@
|
|||||||
|
import type { EventEntry, HandledEvent } from './events.ts';
|
||||||
import type { AnyPropDef, Kebab } from './props.ts';
|
import type { AnyPropDef, Kebab } from './props.ts';
|
||||||
|
|
||||||
/** A constructor {@link WebComponent} can extend: `HTMLElement` or any autonomous subclass of it. */
|
/** A constructor {@link WebComponent} can extend: `HTMLElement` or any autonomous subclass of it. */
|
||||||
@ -16,8 +17,11 @@ export interface Spec {
|
|||||||
readonly css?: string;
|
readonly css?: string;
|
||||||
/** Prefix every *derived* attribute name with `data-`. Explicit `attribute`s are used verbatim. */
|
/** Prefix every *derived* attribute name with `data-`. Explicit `attribute`s are used verbatim. */
|
||||||
readonly dataPrefix?: boolean;
|
readonly dataPrefix?: boolean;
|
||||||
/** Host events routed to `handleEvent`, listened with the element's `signal`. */
|
/**
|
||||||
readonly events?: readonly (keyof HTMLElementEventMap)[];
|
* Host events routed to `handleEvent`, listened with the element's `signal`:
|
||||||
|
* name → `true`, listener options, {@link custom} for a typed `CustomEvent`, or {@link listen}.
|
||||||
|
*/
|
||||||
|
readonly events?: Readonly<Record<string, EventEntry>>;
|
||||||
/** `refs.<name>`: the first match in `root`, or `null`. See {@link RefSpec}. */
|
/** `refs.<name>`: the first match in `root`, or `null`. See {@link RefSpec}. */
|
||||||
readonly element?: Readonly<Record<string, RefSpec>>;
|
readonly element?: Readonly<Record<string, RefSpec>>;
|
||||||
/** `refs.<name>`: every match in `root`, as an array. See {@link RefSpec}. */
|
/** `refs.<name>`: every match in `root`, as an array. See {@link RefSpec}. */
|
||||||
@ -42,11 +46,7 @@ export type BaseOf<S> = S extends { readonly base: infer B extends ElementConstr
|
|||||||
? B
|
? B
|
||||||
: typeof HTMLElement;
|
: typeof HTMLElement;
|
||||||
export type DataPrefixOf<S> = S extends { readonly dataPrefix: true } ? true : false;
|
export type DataPrefixOf<S> = S extends { readonly dataPrefix: true } ? true : false;
|
||||||
export type EventsOf<S> = S extends {
|
|
||||||
readonly events: readonly (infer E extends keyof HTMLElementEventMap)[];
|
|
||||||
}
|
|
||||||
? E
|
|
||||||
: never;
|
|
||||||
type ElementRefsOf<S> = S extends {
|
type ElementRefsOf<S> = S extends {
|
||||||
readonly element: infer E extends Readonly<Record<string, RefSpec>>;
|
readonly element: infer E extends Readonly<Record<string, RefSpec>>;
|
||||||
}
|
}
|
||||||
@ -183,10 +183,8 @@ export interface WebComponentMembers<S> {
|
|||||||
// Re-mapped inline (not `Refs<S>` directly) so editors show the resolved
|
// Re-mapped inline (not `Refs<S>` directly) so editors show the resolved
|
||||||
// `{ input: HTMLInputElement | null; ... }` rather than the alias.
|
// `{ input: HTMLInputElement | null; ... }` rather than the alias.
|
||||||
readonly refs: { readonly [K in keyof Refs<S>]: Refs<S>[K] };
|
readonly refs: { readonly [K in keyof Refs<S>]: Refs<S>[K] };
|
||||||
/** Receives every event listed in `events`. */
|
/** Receives every event listed in `events`; narrow it with `event.type`. */
|
||||||
handleEvent(
|
handleEvent(event: [HandledEvent<S>] extends [never] ? Event : HandledEvent<S>): void;
|
||||||
event: [EventsOf<S>] extends [never] ? Event : HTMLElementEventMap[EventsOf<S>],
|
|
||||||
): void;
|
|
||||||
/** Called at the end of `connectedCallback`. */
|
/** Called at the end of `connectedCallback`. */
|
||||||
onConnect(): void;
|
onConnect(): void;
|
||||||
/** Called at the end of `disconnectedCallback`, after `signal` aborts. */
|
/** Called at the end of `disconnectedCallback`, after `signal` aborts. */
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { resolveEvents } from './events.ts';
|
||||||
import { attributeName, type AnyPropDef } from './props.ts';
|
import { attributeName, type AnyPropDef } from './props.ts';
|
||||||
import type {
|
import type {
|
||||||
ElementClass,
|
ElementClass,
|
||||||
@ -47,7 +48,7 @@ export function WebComponent<const S extends Spec>(
|
|||||||
const byAttribute = new Map(resolved.map((prop) => [prop.attribute, prop]));
|
const byAttribute = new Map(resolved.map((prop) => [prop.attribute, prop]));
|
||||||
const element = resolveRefs(spec.element);
|
const element = resolveRefs(spec.element);
|
||||||
const elements = resolveRefs(spec.elements);
|
const elements = resolveRefs(spec.elements);
|
||||||
const events = spec.events ?? [];
|
const events = resolveEvents(spec.events);
|
||||||
|
|
||||||
// Parsed lazily, once per class, so defining the class needs no DOM work.
|
// Parsed lazily, once per class, so defining the class needs no DOM work.
|
||||||
let template: HTMLTemplateElement | undefined;
|
let template: HTMLTemplateElement | undefined;
|
||||||
@ -150,8 +151,8 @@ export function WebComponent<const S extends Spec>(
|
|||||||
this.#upgradeProperties();
|
this.#upgradeProperties();
|
||||||
}
|
}
|
||||||
if (!this.#root) this.#renderLightDom();
|
if (!this.#root) this.#renderLightDom();
|
||||||
for (const type of events) {
|
for (const [type, options] of events) {
|
||||||
this.addEventListener(type, this, { signal: this.signal });
|
this.addEventListener(type, this, { ...options, signal: this.signal });
|
||||||
}
|
}
|
||||||
this.onConnect();
|
this.onConnect();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,12 +2,15 @@ import { describe, expectTypeOf, test } from 'vitest';
|
|||||||
import {
|
import {
|
||||||
WebComponent,
|
WebComponent,
|
||||||
choice,
|
choice,
|
||||||
|
custom,
|
||||||
disable,
|
disable,
|
||||||
enable,
|
enable,
|
||||||
json,
|
json,
|
||||||
|
listen,
|
||||||
number,
|
number,
|
||||||
string,
|
string,
|
||||||
type AttributesOf,
|
type AttributesOf,
|
||||||
|
type HandledEvent,
|
||||||
type Kebab,
|
type Kebab,
|
||||||
type PropChange,
|
type PropChange,
|
||||||
type RefType,
|
type RefType,
|
||||||
@ -15,7 +18,7 @@ import {
|
|||||||
|
|
||||||
const SPEC = {
|
const SPEC = {
|
||||||
shadow: 'open',
|
shadow: 'open',
|
||||||
events: ['click', 'keydown'],
|
events: { click: true, keydown: true },
|
||||||
element: { input: 'input.name', go: 'form > button#go', thing: '.thing' },
|
element: { input: 'input.name', go: 'form > button#go', thing: '.thing' },
|
||||||
elements: { items: 'ul li' },
|
elements: { items: 'ul li' },
|
||||||
props: {
|
props: {
|
||||||
@ -124,10 +127,62 @@ describe('members', () => {
|
|||||||
expectTypeOf(new Light().root).toEqualTypeOf<ShadowRoot | HTMLElement>();
|
expectTypeOf(new Light().root).toEqualTypeOf<ShadowRoot | HTMLElement>();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleEvent gets the union of listed events', () => {
|
test('handleEvent gets the union of listed events, tagged by type', () => {
|
||||||
expectTypeOf<Parameters<Example['handleEvent']>[0]>().toEqualTypeOf<
|
expectTypeOf<Parameters<Example['handleEvent']>[0]>().toEqualTypeOf<
|
||||||
HTMLElementEventMap['click' | 'keydown']
|
(PointerEvent & { readonly type: 'click' }) | (KeyboardEvent & { readonly type: 'keydown' })
|
||||||
>();
|
>();
|
||||||
|
const Plain = WebComponent({});
|
||||||
|
expectTypeOf<Parameters<InstanceType<typeof Plain>['handleEvent']>[0]>().toEqualTypeOf<Event>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('events are typed by name, by listen<E>(), or by custom<T>()', () => {
|
||||||
|
type FieldChange = { name: string; value: string };
|
||||||
|
const Base = WebComponent({
|
||||||
|
events: {
|
||||||
|
click: true,
|
||||||
|
scroll: { passive: true },
|
||||||
|
keydown: listen(),
|
||||||
|
'wc:field-change': custom<FieldChange>(),
|
||||||
|
'wc:key': listen<KeyboardEvent>({ capture: true }),
|
||||||
|
'wc:untyped': true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
type Handled = Parameters<InstanceType<typeof Base>['handleEvent']>[0];
|
||||||
|
expectTypeOf<Extract<Handled, { type: 'click' }>>().toEqualTypeOf<
|
||||||
|
PointerEvent & { readonly type: 'click' }
|
||||||
|
>();
|
||||||
|
expectTypeOf<Extract<Handled, { type: 'scroll' }>>().toEqualTypeOf<
|
||||||
|
Event & { readonly type: 'scroll' }
|
||||||
|
>();
|
||||||
|
expectTypeOf<Extract<Handled, { type: 'wc:field-change' }>>().toEqualTypeOf<
|
||||||
|
CustomEvent<FieldChange> & { readonly type: 'wc:field-change' }
|
||||||
|
>();
|
||||||
|
expectTypeOf<Extract<Handled, { type: 'keydown' }>>().toEqualTypeOf<
|
||||||
|
KeyboardEvent & { readonly type: 'keydown' }
|
||||||
|
>();
|
||||||
|
expectTypeOf<Extract<Handled, { type: 'wc:key' }>>().toEqualTypeOf<
|
||||||
|
KeyboardEvent & { readonly type: 'wc:key' }
|
||||||
|
>();
|
||||||
|
expectTypeOf<Extract<Handled, { type: 'wc:untyped' }>>().toEqualTypeOf<
|
||||||
|
Event & { readonly type: 'wc:untyped' }
|
||||||
|
>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('custom<T>() is listen<CustomEvent<T>>()', () => {
|
||||||
|
expectTypeOf(custom<number>()).toEqualTypeOf(listen<CustomEvent<number>>());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('event.type narrows handleEvent', () => {
|
||||||
|
const PICK = {
|
||||||
|
events: { click: true, 'wc:pick': custom<number>() },
|
||||||
|
} as const;
|
||||||
|
class El extends WebComponent(PICK) {
|
||||||
|
override handleEvent(event: HandledEvent<typeof PICK>): void {
|
||||||
|
if (event.type === 'click') expectTypeOf(event.button).toEqualTypeOf<number>();
|
||||||
|
else expectTypeOf(event.detail).toEqualTypeOf<number>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void El;
|
||||||
});
|
});
|
||||||
|
|
||||||
test('refs are typed from their selectors', () => {
|
test('refs are typed from their selectors', () => {
|
||||||
|
|||||||
@ -2,11 +2,14 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
|
|||||||
import {
|
import {
|
||||||
WebComponent,
|
WebComponent,
|
||||||
choice,
|
choice,
|
||||||
|
custom,
|
||||||
disable,
|
disable,
|
||||||
enable,
|
enable,
|
||||||
json,
|
json,
|
||||||
|
listen,
|
||||||
number,
|
number,
|
||||||
string,
|
string,
|
||||||
|
type HandledEvent,
|
||||||
type PropChange,
|
type PropChange,
|
||||||
} from '../src/index.ts';
|
} from '../src/index.ts';
|
||||||
|
|
||||||
@ -218,7 +221,7 @@ describe('light DOM', () => {
|
|||||||
describe('signal and events', () => {
|
describe('signal and events', () => {
|
||||||
test('events go to handleEvent until disconnect; reconnect re-listens', () => {
|
test('events go to handleEvent until disconnect; reconnect re-listens', () => {
|
||||||
const handle = vi.fn();
|
const handle = vi.fn();
|
||||||
class El extends WebComponent({ events: ['click'] }) {
|
class El extends WebComponent({ events: { click: true } }) {
|
||||||
override handleEvent(event: PointerEvent): void {
|
override handleEvent(event: PointerEvent): void {
|
||||||
handle(event.type);
|
handle(event.type);
|
||||||
}
|
}
|
||||||
@ -239,6 +242,34 @@ describe('signal and events', () => {
|
|||||||
expect(handle).toHaveBeenCalledTimes(2);
|
expect(handle).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('custom events and listener options', () => {
|
||||||
|
const SPEC = {
|
||||||
|
events: {
|
||||||
|
'wc:pick': custom<number>(),
|
||||||
|
'wc:once': { once: true },
|
||||||
|
'wc:key': listen<KeyboardEvent>({ once: true }),
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
const seen: string[] = [];
|
||||||
|
class El extends WebComponent(SPEC) {
|
||||||
|
override handleEvent(event: HandledEvent<typeof SPEC>): void {
|
||||||
|
if (event.type === 'wc:pick') seen.push(`pick ${event.detail}`);
|
||||||
|
else if (event.type === 'wc:key') seen.push(`key ${event.key}`);
|
||||||
|
else seen.push(event.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const el = new (define(El))();
|
||||||
|
document.body.append(el);
|
||||||
|
el.dispatchEvent(new CustomEvent('wc:pick', { detail: 3 }));
|
||||||
|
el.dispatchEvent(new Event('wc:once'));
|
||||||
|
el.dispatchEvent(new Event('wc:once'));
|
||||||
|
el.dispatchEvent(new KeyboardEvent('wc:key', { key: 'a' }));
|
||||||
|
el.dispatchEvent(new KeyboardEvent('wc:key', { key: 'b' }));
|
||||||
|
el.remove();
|
||||||
|
el.dispatchEvent(new CustomEvent('wc:pick', { detail: 4 }));
|
||||||
|
expect(seen).toEqual(['pick 3', 'wc:once', 'key a']);
|
||||||
|
});
|
||||||
|
|
||||||
test('onConnect / onDisconnect', () => {
|
test('onConnect / onDisconnect', () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
class El extends WebComponent({}) {
|
class El extends WebComponent({}) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user