Two parent-looks-for-child patterns found and removed, per the
child-emits-events-parent-never-reaches-in principle:
- leaflet-control-layers: #childLayers() queried children directly via
querySelectorAll + reading their leafletObject/attributes, in two call
sites. Verified empirically that a parent's connectedCallback always
completes before a freshly-connected child's does (even for an
already-built subtree attached in one shot), which means both call
sites always ran against unpopulated children -- dead code. Deleted;
#onChildRegister (already event-driven) was doing all the real work.
- leaflet-polygon/leaflet-polyline: #coords() queried <leaflet-line>
children via querySelectorAll + read their .latlng property directly,
using a MutationObserver + a payload-less event only as a "something
changed, rescan everyone" signal. Rewritten to be purely event-driven:
<leaflet-line> now fires leaflet-line-sync (connect + every lat/lng
change, carrying its own position) and leaflet-line-remove (disconnect)
on itself; a new shared VertexTracker (src/core/vertex-tracker.ts)
turns that event stream into an ordered coordinate list, using
compareDocumentPosition only to place a newly-registered vertex at its
real document position rather than assuming registration order matches
DOM order. No querySelectorAll, no MutationObserver, no reading a
child's property.
Testing the polygon rewrite in an actual browser (not jsdom) surfaced two
real bugs, both fixed:
1. disconnectedCallback fires *after* a node is already detached from its
parent, so leaflet-line's removal event had nowhere to bubble to.
Fixed by caching parentNode while still connected and dispatching from
that cached reference instead of from the (by-then-detached) node.
2. A load-order hazard affecting every "parent listens for a specific
child tag's announcement" relationship in this codebase:
customElements.define() upgrades every matching element already
parsed into the page immediately and synchronously, so whichever tag
gets defined first wins a race -- a child tag defined before its
listening parent fires its one-shot connect-time announcement into a
parent that doesn't exist yet, and it's lost for good. This broke
polygon/polyline (introduced by this rewrite, since leaflet-line was
exported before leaflet-polygon) and, independently, was already
silently broken for leaflet-control-layers (a base/overlay layer
leaked directly onto the map, bypassing the control entirely) and
latently for leaflet-layer-group/leaflet-feature-group. src/index.ts's
export order now encodes and documents the real dependency hierarchy
(map -> containers that listen for child announcements -> concrete
layer types -> their own children).
Added test/load-order.test.ts, which is structurally different from
every other test file: it never statically imports a component
module, building the DOM with plain undefined elements first and only
dynamically import()ing src/index.ts afterward -- reproducing a real
page's actual load order, which every other test's "define everything
first" pattern cannot catch. Confirmed it fails against the old
ordering and passes against the fix.
Finishes the WithProps(PROPS, options) mixin (src/core/with-props.ts): every
component gets reactive attributes, live-value property getters that read
through to the Leaflet object where possible, and every Leaflet event
re-emitted on the element as `leaflet:<type>` via a generic fire() patch (no
per-component or per-event registration needed).
Prop coverage:
- Centralized pathProps in shared-props.ts (stroke, lineCap, lineJoin,
dashArray, dashOffset, fillRule, interactive, className,
bubblingMouseEvents, pane), deduped leaflet-geojson against it.
- Added live getters backed by Leaflet's own accessors: radius (circle,
circle-marker), bounds (rectangle, image/video/svg-overlay), url
(image/video-overlay).
- Filled gaps: smoothFactor/noClip on polyline; a shared tileLayerProps
fragment now used by both tile-layer and tile-layer-wms (WMS previously
exposed none of its base tile options); crs on WMS; zIndex/className/
keepAspectRatio/errorOverlayUrl on video-overlay; fixed tile-layer's
zIndex default and div-icon's className default to match Leaflet.
Tooling:
- Removed dead src/core/events.ts (broken, unused, superseded by the
generic event forwarding above) and src/core/attributes.ts (emptied by
an earlier rename, nothing imported it).
- Swapped ESLint + @typescript-eslint for oxlint: no released
@typescript-eslint version supports the pinned typescript@7, even for
parsing alone. oxlint has its own parser and lints clean.
- Dropped the Rollup CJS/UMD bundle step; dist/ is ESM-only from tsc now.
Updated package.json's main/module/exports/unpkg accordingly.
- Updated CLAUDE.md to match: build/lint commands, ESM-only output, the
event-forwarding mechanism, and layer-group/feature-group now going
through WithProps({}) instead of raw HTMLElement.
Removed the utils.ts file. The functions in there either should have
been a part of the register.ts or props.ts files. The only functions
left related to attributes so utils.ts was renamed to attributes.ts
Upgrade ESLint config to use flat config with @typescript-eslint/strict-type-checked and stylistic-type-checked presets.
- Use defineConfig helper, scope to .ts files only
- Replace || with ??, convert type to interface, remove redundant type assertions and non-null assertions
- Annotate intentional violations (unbound-method, non-null-assertion) with eslint-disable
- Use querySelectorAll<T> for typed selection, Map over globalThis.Map
- Add @eslint/js dev dependency
WithProps is tightly coupled to PropDef types, so it belongs in
props.ts alongside defineProps and the type definitions.
- Move WithProps, definePropAccessors, and Ctor to props.ts
- Import PropTypesFromTable in utils.ts (used by buildOptions)
- Update all 18 component imports to source WithProps from props.ts
Replace the #map getter (which used closest("leaflet-map") + leafletObject)
with two new custom events: leaflet-add-layer and leaflet-remove-layer.
The map listens for them and delegates to map.addLayer/removeLayer.
This removes the coupling between leaflet-control-layers and the map
element, aligning with the existing leaflet-register event protocol.
slot is a reserved HTML global attribute with shadow-DOM semantics;
using it was misleading even though it happened to work. Switch to
type="base" / type="overlay", which is conventional and carries no
side effects.
New <leaflet-control-layers> wraps Control.Layers. Children with
slot="base" or slot="overlay" and a name attribute are automatically
registered as base layers or overlays. The control intercepts child
leaflet-register events and stops propagation so layers are managed
by the layers control rather than added directly to the map.
Unchecked layers that connected before the control are removed from
the map before the control initialises, so Control.Layers._addItem
correctly reflects the unchecked state on first render.