From 0419ab781d75d002628b7184e8a6acc27118a07d Mon Sep 17 00:00:00 2001 From: Buddy Date: Sat, 22 Aug 2026 13:30:07 -0700 Subject: [PATCH] feat: type leaflet-* components and their leaflet: events Two augmentations, so TypeScript actually knows about the custom elements: - HTMLElementTagNameMap (src/index.ts): createElement/querySelector now infer the exact component class for all 24 tags instead of HTMLElement. - Per-component addEventListener/removeEventListener overrides, typing `leaflet:` events against the real Leaflet event payload (PopupEvent, DragEndEvent, LeafletMouseEvent, etc.) while still accepting ordinary DOM events normally, and rejecting event names that component doesn't fire. src/core/event-types.ts holds reusable event-name -> payload-type fragments (mirroring shared-props.ts's fragment reuse), composed per family: MapEvents, MarkerEvents, PathEvents, TileLayerEvents, DivOverlayLayerEvents, GroupEvents. LeafletAddEventListener/ LeafletRemoveEventListener in with-props.ts are type-only intersection-of-overloads helpers applied via `declare addEventListener: ...`, the same pattern every component already uses for `declare readonly leafletObject?: X` -- zero runtime cost. Deliberately no generic `string` fallback overload: a fallback would silently accept unrecognized `leaflet:*` names too, defeating the point. Fixed a real bug found while building this: #forwardEvents was dispatching the raw pre-merge `data` Leaflet passes to fire(), missing type/target/sourceTarget that Leaflet's own fire() merges in before notifying real .on() listeners. Typing `detail` against Leaflet's actual event interfaces would have been dishonest otherwise, so the merge now matches Leaflet's own Evented#fire. Also added 'line-updated' to the existing internal-event HTMLElementEventMap augmentation in register.ts (needed once addEventListener got overridden on polygon/polyline, which use it internally) and cleaned up ~35 now- redundant `as HTMLElement & {...}` casts across the test suite that the tag name map makes unnecessary. --- src/components/leaflet-circle-marker.ts | 9 +- src/components/leaflet-circle.ts | 9 +- src/components/leaflet-feature-group.ts | 9 +- src/components/leaflet-geojson.ts | 9 +- src/components/leaflet-image-overlay.ts | 9 +- src/components/leaflet-layer-group.ts | 9 +- src/components/leaflet-map.ts | 9 +- src/components/leaflet-marker.ts | 9 +- src/components/leaflet-polygon.ts | 9 +- src/components/leaflet-polyline.ts | 9 +- src/components/leaflet-popup.ts | 9 +- src/components/leaflet-rectangle.ts | 9 +- src/components/leaflet-svg-overlay.ts | 9 +- src/components/leaflet-tile-layer-wms.ts | 9 +- src/components/leaflet-tile-layer.ts | 9 +- src/components/leaflet-tooltip.ts | 9 +- src/components/leaflet-video-overlay.ts | 9 +- src/core/event-types.ts | 125 ++++++++++++++++++++ src/core/register.ts | 5 + src/core/with-props.ts | 41 ++++++- src/index.ts | 56 +++++++++ test/components/controls-and-groups.test.ts | 24 +--- test/components/geojson.test.ts | 8 +- test/components/icons.test.ts | 12 +- test/components/latlng-bindables.test.ts | 34 ++---- test/components/overlays.test.ts | 20 +--- test/components/shapes.test.ts | 28 +---- test/components/tile-layers.test.ts | 17 +-- test/core/event-types.test.ts | 53 +++++++++ test/core/with-props.test.ts | 9 +- test/integration.test.ts | 27 ++--- 31 files changed, 464 insertions(+), 148 deletions(-) create mode 100644 src/core/event-types.ts create mode 100644 test/core/event-types.test.ts diff --git a/src/components/leaflet-circle-marker.ts b/src/components/leaflet-circle-marker.ts index 618ebe8..3753435 100644 --- a/src/components/leaflet-circle-marker.ts +++ b/src/components/leaflet-circle-marker.ts @@ -1,7 +1,12 @@ import { CircleMarker, type CircleMarkerOptions } from 'leaflet'; import { num } from '../core/props.ts'; import { latLngProps, pathProps } from '../core/shared-props.ts'; -import { WithProps } from '../core/with-props.ts'; +import { + WithProps, + type LeafletAddEventListener, + type LeafletRemoveEventListener, +} from '../core/with-props.ts'; +import type { PathEvents } from '../core/event-types.ts'; export class LeafletCircleMarker extends WithProps({ ...latLngProps, @@ -9,6 +14,8 @@ export class LeafletCircleMarker extends WithProps({ ...pathProps, }) { declare readonly leafletObject?: CircleMarker; + declare addEventListener: LeafletAddEventListener; + declare removeEventListener: LeafletRemoveEventListener; createLeafletObject(options: CircleMarkerOptions): CircleMarker { return new CircleMarker([this.lat, this.lng], options); diff --git a/src/components/leaflet-circle.ts b/src/components/leaflet-circle.ts index e1c51cd..15cf814 100644 --- a/src/components/leaflet-circle.ts +++ b/src/components/leaflet-circle.ts @@ -1,7 +1,12 @@ import { Circle, type CircleOptions } from 'leaflet'; import { num, positional } from '../core/props.ts'; import { latLngProps, pathProps } from '../core/shared-props.ts'; -import { WithProps } from '../core/with-props.ts'; +import { + WithProps, + type LeafletAddEventListener, + type LeafletRemoveEventListener, +} from '../core/with-props.ts'; +import type { PathEvents } from '../core/event-types.ts'; export class LeafletCircle extends WithProps({ ...latLngProps, @@ -11,6 +16,8 @@ export class LeafletCircle extends WithProps({ radius: positional(num(1000, { get: (obj) => obj.getRadius() })), }) { declare readonly leafletObject?: Circle; + declare addEventListener: LeafletAddEventListener; + declare removeEventListener: LeafletRemoveEventListener; createLeafletObject(options: CircleOptions): Circle { return new Circle([this.lat, this.lng], { ...options, radius: this.radius }); diff --git a/src/components/leaflet-feature-group.ts b/src/components/leaflet-feature-group.ts index 4d87f58..79e064b 100644 --- a/src/components/leaflet-feature-group.ts +++ b/src/components/leaflet-feature-group.ts @@ -1,9 +1,16 @@ import { FeatureGroup } from 'leaflet'; -import { WithProps } from '../core/with-props.ts'; +import { + WithProps, + type LeafletAddEventListener, + type LeafletRemoveEventListener, +} from '../core/with-props.ts'; +import type { GroupEvents } from '../core/event-types.ts'; // Like leaflet-layer-group, but its children share events and a bounding box. export class LeafletFeatureGroup extends WithProps({}) { declare readonly leafletObject?: FeatureGroup; + declare addEventListener: LeafletAddEventListener; + declare removeEventListener: LeafletRemoveEventListener; createLeafletObject(): FeatureGroup { return new FeatureGroup([]); diff --git a/src/components/leaflet-geojson.ts b/src/components/leaflet-geojson.ts index 2d9b2aa..8f459fa 100644 --- a/src/components/leaflet-geojson.ts +++ b/src/components/leaflet-geojson.ts @@ -2,7 +2,12 @@ import { GeoJSON, type PathOptions } from 'leaflet'; import type { GeoJsonObject } from 'geojson'; import { json, positional } from '../core/props.ts'; import { pathProps } from '../core/shared-props.ts'; -import { WithProps } from '../core/with-props.ts'; +import { + WithProps, + type LeafletAddEventListener, + type LeafletRemoveEventListener, +} from '../core/with-props.ts'; +import type { GroupEvents } from '../core/event-types.ts'; export class LeafletGeoJSON extends WithProps({ data: positional( @@ -16,6 +21,8 @@ export class LeafletGeoJSON extends WithProps({ ...pathProps, }) { declare readonly leafletObject?: GeoJSON; + declare addEventListener: LeafletAddEventListener; + declare removeEventListener: LeafletRemoveEventListener; // Every prop but `data` is a style option, and GeoJSON takes those nested // under `style` so they apply to each feature it builds. diff --git a/src/components/leaflet-image-overlay.ts b/src/components/leaflet-image-overlay.ts index 4651648..31ebde6 100644 --- a/src/components/leaflet-image-overlay.ts +++ b/src/components/leaflet-image-overlay.ts @@ -6,7 +6,12 @@ import { } from 'leaflet'; import { bool, choice, json, num, positional, str } from '../core/props.ts'; import { getBounds, urlProp } from '../core/shared-props.ts'; -import { WithProps } from '../core/with-props.ts'; +import { + WithProps, + type LeafletAddEventListener, + type LeafletRemoveEventListener, +} from '../core/with-props.ts'; +import type { PathEvents } from '../core/event-types.ts'; export class LeafletImageOverlay extends WithProps({ url: urlProp, @@ -25,6 +30,8 @@ export class LeafletImageOverlay extends WithProps({ className: str(), }) { declare readonly leafletObject?: ImageOverlay; + declare addEventListener: LeafletAddEventListener; + declare removeEventListener: LeafletRemoveEventListener; createLeafletObject(options: ImageOverlayOptions): ImageOverlay { return new ImageOverlay(this.url, this.bounds, options); diff --git a/src/components/leaflet-layer-group.ts b/src/components/leaflet-layer-group.ts index c7adc92..5980e62 100644 --- a/src/components/leaflet-layer-group.ts +++ b/src/components/leaflet-layer-group.ts @@ -1,10 +1,17 @@ import { LayerGroup } from 'leaflet'; -import { WithProps } from '../core/with-props.ts'; +import { + WithProps, + type LeafletAddEventListener, + type LeafletRemoveEventListener, +} from '../core/with-props.ts'; +import type { GroupEvents } from '../core/event-types.ts'; // A passthrough container: it has no options of its own, and children add // themselves to it through the standard registration bubble. export class LeafletLayerGroup extends WithProps({}) { declare readonly leafletObject?: LayerGroup; + declare addEventListener: LeafletAddEventListener; + declare removeEventListener: LeafletRemoveEventListener; createLeafletObject(): LayerGroup { return new LayerGroup([]); diff --git a/src/components/leaflet-map.ts b/src/components/leaflet-map.ts index f6735d8..602a160 100644 --- a/src/components/leaflet-map.ts +++ b/src/components/leaflet-map.ts @@ -1,7 +1,12 @@ import { Icon, Map as LMap, type MapOptions } from 'leaflet'; import { bool, disabled, num, positional, str } from '../core/props.ts'; -import { WithProps } from '../core/with-props.ts'; +import { + WithProps, + type LeafletAddEventListener, + type LeafletRemoveEventListener, +} from '../core/with-props.ts'; import type { LeafletLayerEvent, LeafletRegisterEvent } from '../core/register.ts'; +import type { MapEvents } from '../core/event-types.ts'; const DEFAULT_CSS_URL = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'; const DEFAULT_CSS_INTEGRITY = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY='; @@ -121,6 +126,8 @@ export class LeafletMap extends WithProps( { attach: 'none' }, ) { declare readonly leafletObject?: LMap; + declare addEventListener: LeafletAddEventListener; + declare removeEventListener: LeafletRemoveEventListener; #container?: HTMLDivElement; #cssLink?: HTMLLinkElement; diff --git a/src/components/leaflet-marker.ts b/src/components/leaflet-marker.ts index a86c508..9e9ac58 100644 --- a/src/components/leaflet-marker.ts +++ b/src/components/leaflet-marker.ts @@ -1,8 +1,13 @@ import { Icon, Marker, type MarkerOptions } from 'leaflet'; import { bool, num, str } from '../core/props.ts'; import { latLngProps } from '../core/shared-props.ts'; -import { WithProps } from '../core/with-props.ts'; +import { + WithProps, + type LeafletAddEventListener, + type LeafletRemoveEventListener, +} from '../core/with-props.ts'; import type { LeafletIconChangedEvent } from '../core/register.ts'; +import type { MarkerEvents } from '../core/event-types.ts'; const PROPS = { ...latLngProps, @@ -31,6 +36,8 @@ const PROPS = { export class LeafletMarker extends WithProps(PROPS) { declare readonly leafletObject?: Marker; + declare addEventListener: LeafletAddEventListener; + declare removeEventListener: LeafletRemoveEventListener; createLeafletObject(options: MarkerOptions): Marker { return new Marker([this.lat, this.lng], options); diff --git a/src/components/leaflet-polygon.ts b/src/components/leaflet-polygon.ts index 03ad657..5c3bae4 100644 --- a/src/components/leaflet-polygon.ts +++ b/src/components/leaflet-polygon.ts @@ -1,10 +1,17 @@ import { Polygon, type PolylineOptions } from 'leaflet'; import { pathProps } from '../core/shared-props.ts'; -import { WithProps } from '../core/with-props.ts'; +import { + WithProps, + type LeafletAddEventListener, + type LeafletRemoveEventListener, +} from '../core/with-props.ts'; import type { LeafletLine } from './leaflet-line.ts'; +import type { PathEvents } from '../core/event-types.ts'; export class LeafletPolygon extends WithProps({ ...pathProps }) { declare readonly leafletObject?: Polygon; + declare addEventListener: LeafletAddEventListener; + declare removeEventListener: LeafletRemoveEventListener; #observer?: MutationObserver; diff --git a/src/components/leaflet-polyline.ts b/src/components/leaflet-polyline.ts index ebd2834..4ff905f 100644 --- a/src/components/leaflet-polyline.ts +++ b/src/components/leaflet-polyline.ts @@ -1,8 +1,13 @@ import { Polyline, type PolylineOptions } from 'leaflet'; import { bool, num } from '../core/props.ts'; import { pathProps, style } from '../core/shared-props.ts'; -import { WithProps } from '../core/with-props.ts'; +import { + WithProps, + type LeafletAddEventListener, + type LeafletRemoveEventListener, +} from '../core/with-props.ts'; import type { LeafletLine } from './leaflet-line.ts'; +import type { PathEvents } from '../core/event-types.ts'; export class LeafletPolyline extends WithProps({ ...pathProps, @@ -12,6 +17,8 @@ export class LeafletPolyline extends WithProps({ noClip: bool(), }) { declare readonly leafletObject?: Polyline; + declare addEventListener: LeafletAddEventListener; + declare removeEventListener: LeafletRemoveEventListener; #observer?: MutationObserver; diff --git a/src/components/leaflet-popup.ts b/src/components/leaflet-popup.ts index 3525e82..83565a2 100644 --- a/src/components/leaflet-popup.ts +++ b/src/components/leaflet-popup.ts @@ -1,7 +1,12 @@ import { Popup, type PopupOptions } from 'leaflet'; import { bool, num } from '../core/props.ts'; import { latLngProps } from '../core/shared-props.ts'; -import { WithProps } from '../core/with-props.ts'; +import { + WithProps, + type LeafletAddEventListener, + type LeafletRemoveEventListener, +} from '../core/with-props.ts'; +import type { DivOverlayLayerEvents } from '../core/event-types.ts'; export class LeafletPopup extends WithProps( { @@ -16,6 +21,8 @@ export class LeafletPopup extends WithProps( { attach: 'self' }, ) { declare readonly leafletObject?: Popup; + declare addEventListener: LeafletAddEventListener; + declare removeEventListener: LeafletRemoveEventListener; #observer?: MutationObserver; diff --git a/src/components/leaflet-rectangle.ts b/src/components/leaflet-rectangle.ts index f1d7e37..5d8fefc 100644 --- a/src/components/leaflet-rectangle.ts +++ b/src/components/leaflet-rectangle.ts @@ -1,13 +1,20 @@ import { Rectangle, type LatLngBoundsExpression, type PolylineOptions } from 'leaflet'; import { json, positional } from '../core/props.ts'; import { pathProps, getBounds } from '../core/shared-props.ts'; -import { WithProps } from '../core/with-props.ts'; +import { + WithProps, + type LeafletAddEventListener, + type LeafletRemoveEventListener, +} from '../core/with-props.ts'; +import type { PathEvents } from '../core/event-types.ts'; export class LeafletRectangle extends WithProps({ bounds: positional(json([], { get: getBounds })), ...pathProps, }) { declare readonly leafletObject?: Rectangle; + declare addEventListener: LeafletAddEventListener; + declare removeEventListener: LeafletRemoveEventListener; createLeafletObject(options: PolylineOptions): Rectangle { return new Rectangle(this.bounds, options); diff --git a/src/components/leaflet-svg-overlay.ts b/src/components/leaflet-svg-overlay.ts index ece04f6..0dd1f1e 100644 --- a/src/components/leaflet-svg-overlay.ts +++ b/src/components/leaflet-svg-overlay.ts @@ -6,7 +6,12 @@ import { } from 'leaflet'; import { bool, choice, json, num, positional, str } from '../core/props.ts'; import { getBounds } from '../core/shared-props.ts'; -import { WithProps } from '../core/with-props.ts'; +import { + WithProps, + type LeafletAddEventListener, + type LeafletRemoveEventListener, +} from '../core/with-props.ts'; +import type { PathEvents } from '../core/event-types.ts'; export class LeafletSVGOverlay extends WithProps({ bounds: positional(json([], { get: getBounds })), @@ -17,6 +22,8 @@ export class LeafletSVGOverlay extends WithProps({ className: str(), }) { declare readonly leafletObject?: SVGOverlay; + declare addEventListener: LeafletAddEventListener; + declare removeEventListener: LeafletRemoveEventListener; createLeafletObject(options: ImageOverlayOptions): SVGOverlay { const svg = diff --git a/src/components/leaflet-tile-layer-wms.ts b/src/components/leaflet-tile-layer-wms.ts index 2cc016c..1e2f3da 100644 --- a/src/components/leaflet-tile-layer-wms.ts +++ b/src/components/leaflet-tile-layer-wms.ts @@ -1,7 +1,12 @@ import { CRS, TileLayer, type WMSOptions, type WMSParams } from 'leaflet'; import { bool, str, type PropDef } from '../core/props.ts'; import { tileLayerProps, urlProp } from '../core/shared-props.ts'; -import { WithProps } from '../core/with-props.ts'; +import { + WithProps, + type LeafletAddEventListener, + type LeafletRemoveEventListener, +} from '../core/with-props.ts'; +import type { TileLayerEvents } from '../core/event-types.ts'; // WMS request parameters have no individual setters -- they're merged into the // query string through setParams(). @@ -43,6 +48,8 @@ export class LeafletTileLayerWMS extends WithProps({ crs: crsProp, }) { declare readonly leafletObject?: TileLayer.WMS; + declare addEventListener: LeafletAddEventListener; + declare removeEventListener: LeafletRemoveEventListener; createLeafletObject(options: WMSOptions): TileLayer.WMS { return new TileLayer.WMS(this.url, options); diff --git a/src/components/leaflet-tile-layer.ts b/src/components/leaflet-tile-layer.ts index 08632e7..ef0471f 100644 --- a/src/components/leaflet-tile-layer.ts +++ b/src/components/leaflet-tile-layer.ts @@ -1,12 +1,19 @@ import { TileLayer, type TileLayerOptions } from 'leaflet'; import { tileLayerProps, urlProp } from '../core/shared-props.ts'; -import { WithProps } from '../core/with-props.ts'; +import { + WithProps, + type LeafletAddEventListener, + type LeafletRemoveEventListener, +} from '../core/with-props.ts'; +import type { TileLayerEvents } from '../core/event-types.ts'; export class LeafletTileLayer extends WithProps({ url: urlProp, ...tileLayerProps, }) { declare readonly leafletObject?: TileLayer; + declare addEventListener: LeafletAddEventListener; + declare removeEventListener: LeafletRemoveEventListener; createLeafletObject(options: TileLayerOptions): TileLayer { return new TileLayer(this.url, options); diff --git a/src/components/leaflet-tooltip.ts b/src/components/leaflet-tooltip.ts index da39c46..4f9bb88 100644 --- a/src/components/leaflet-tooltip.ts +++ b/src/components/leaflet-tooltip.ts @@ -1,7 +1,12 @@ import { Tooltip, type Direction, type PointExpression, type TooltipOptions } from 'leaflet'; import { bool, choice, json, num, str } from '../core/props.ts'; import { latLngProps } from '../core/shared-props.ts'; -import { WithProps } from '../core/with-props.ts'; +import { + WithProps, + type LeafletAddEventListener, + type LeafletRemoveEventListener, +} from '../core/with-props.ts'; +import type { DivOverlayLayerEvents } from '../core/event-types.ts'; export class LeafletTooltip extends WithProps( { @@ -16,6 +21,8 @@ export class LeafletTooltip extends WithProps( { attach: 'self' }, ) { declare readonly leafletObject?: Tooltip; + declare addEventListener: LeafletAddEventListener; + declare removeEventListener: LeafletRemoveEventListener; #observer?: MutationObserver; diff --git a/src/components/leaflet-video-overlay.ts b/src/components/leaflet-video-overlay.ts index 91d9e17..647af97 100644 --- a/src/components/leaflet-video-overlay.ts +++ b/src/components/leaflet-video-overlay.ts @@ -6,7 +6,12 @@ import { } from 'leaflet'; import { bool, choice, json, num, positional, str } from '../core/props.ts'; import { getBounds, urlProp } from '../core/shared-props.ts'; -import { WithProps } from '../core/with-props.ts'; +import { + WithProps, + type LeafletAddEventListener, + type LeafletRemoveEventListener, +} from '../core/with-props.ts'; +import type { PathEvents } from '../core/event-types.ts'; // Playback options live on the