feat: add Geolocation API#23527
Conversation
|
2fa466a to
e39787c
Compare
Provides access to the browser's Geolocation API with two usage modes: - get(): one-shot position request with success/error callbacks - track(): continuous tracking via a reactive Signal tied to a component's lifecycle, using a GeolocationState sealed type (Pending | GeolocationPosition | GeolocationError) that enables exhaustive pattern matching Includes records for coordinates, position, error, and options, a JS module that bridges navigator.geolocation to Vaadin's event system, and unit + integration tests.
Convert the plain JS geolocation module to TypeScript with typed interfaces matching the Java records, and move it from flow-server to flow-client where other core client infrastructure lives. The module is loaded via flow-client's index.ts entry point instead of a @jsmodule annotation, following the same pattern as Flow.ts.
Signal.get() requires a reactive context, which unit tests don't have. Use peek() to read signal values without dependency tracking, matching the pattern used by other signal tests in the codebase.
Import Geolocation from Flow.ts instead of index.ts, since Flow.ts is what Vite actually bundles at runtime. Add empty export to make TypeScript emit it as an ES module. Fix track IT test race condition by listening for the DOM event directly instead of polling the signal after a single JS round-trip. Use peek() in view code since it runs outside a reactive context. Add debug div to GeolocationView to report geolocation API status.
Extends the Geolocation API with stop(), isSupported(), queryPermission(), the GeolocationPermission and GeolocationErrorCode enums, and a Duration- accepting builder on GeolocationOptions. Javadoc across the package is rewritten for Java developers who do not read the underlying JS API, and the TypeScript helper drops fallbacks that are only relevant to browsers outside the supported set (Safari < 17 Apple-epoch timestamps, defensive feature checks).
The Vaadin serialization tests scan all inner classes and require them to be Serializable. Builder was missing the marker interface.
Replaces the static Geolocation.get/.track entry points with an instance-based facade reached via ui.getGeolocation(). The one-shot get() now takes a single SerializableConsumer<GeolocationResult> instead of separate success and error callbacks, letting applications pattern-match on the result. Continuous tracking returns a new GeolocationTracker handle whose value() exposes Signal<GeolocationResult> and whose stop() cancels the watch. GeolocationState is renamed to GeolocationResult. isSupported and queryPermission move to instance methods on the same facade; the static API is removed.
Collapses the two capability checks into a single queryAvailability() call returning a GeolocationAvailability enum (GRANTED / DENIED / PROMPT / UNKNOWN / UNSUPPORTED). One JS round-trip covers both the "is the API usable here" and "what permission state has the user granted" questions, and applications pick the branch that matches the combined answer. GeolocationPermission is removed; isSupported() and queryPermission() disappear. Also tightens a few Javadoc paragraphs that over-promised about UI-thread / ui.access() guarantees.
Moves GeolocationAvailability off an async callback and into a synchronous getGeolocation().getAvailability() backed by ExtendedClientDetails. The initial value ships with the browser-details init handshake (v-ga); get()/track() responses refresh it inline via their JSON payload; a vaadin-geolocation-availability-change DOM event, dispatched by the client's permissionchange listener and received by a DOM listener registered in Geolocation's constructor, keeps the cached value current when the user flips the site permission mid-session. The Geolocation instance is now built in UI's constructor body after internals are set, so the listener can register upfront without lazy guards. Also drops GeolocationResult.Pending: the sealed interface now permits only Position and Error. The tracker's signal starts null and callers pattern-match with case null for the waiting state, removing the dead case arm from every get() switch. Flow.ts collectBrowserDetails is now async and awaits window.Vaadin.Flow.geolocation.queryAvailability() so the value is ready in the very first request to the server.
Flow.ts: hoists the await for collectBrowserDetails() out of the Promise
body (flowInitUi is already async); drops the defensive
typeof-check/try-catch around window.Vaadin.Flow.geolocation.queryAvailability
since Geolocation.ts is imported as a side-effect module.
Geolocation.ts: inlines deriveSupported into its only caller; replaces
the `(window as any).Vaadin = (window as any).Vaadin || {}` bootstrap
with ES2021 ??= assignment behind a local $wnd alias; renames
applyAvailabilityFromResult to getAndCacheAvailabilityFromResult to
reflect that it both caches and returns the value; get() callers now
use the function's return value directly instead of reading the
module-level cache after a side-effectful call.
A position event already in-flight when the Stop click reaches the server can still produce a new trackResult div after stopResult has been appended to the DOM. Pause 100 ms before snapshotting the count so that trailing in-flight event has landed first; without this the "no new divs after stop()" assertion is flaky.
`asReadonly()` allocates a fresh lambda-backed wrapper on every call, so callers got a different instance each time — behaviorally equivalent (all delegate to the same underlying ValueSignal) but identity-unstable and wasteful. Cache one wrapper per signal on the tracker and the Geolocation facade and return that.
|
I get an unexpected behavior if I start tracking and then revoke and grant again the permission after a couple of positions are received: in this case, the browser stops sending position updates. I don't know if this is the default browser behavior, if my testing is incorrect, or if it is a bug somewhere in the implementation. EDIT: the same behavior is also with plain JavaScript. @Artur- I guess this needs to be documented somehow. |
maximumAge cannot be negative anymore, use 9999 to trigger the error
…pattern VaadinSession.localeSignal(), windowSizeSignal, and validationStatusSignal all use the *Signal() naming when exposing a Signal<T> accessor. Align the three geolocation accessors: - GeolocationTracker.value() -> valueSignal() - GeolocationTracker.active() -> activeSignal() - Geolocation.availability() -> availabilitySignal() Javadoc examples, cross-references and callers in tests and the IT view are updated to match.
UIInternals#getGeolocationAvailabilitySignal still linked to Geolocation#availability(), which was renamed to availabilitySignal() in the *Signal() naming pass. Unit-tests job 3 failed because the javadoc goal is fail-on-error.
|
Something here we need to deal with still for an initial version? |
Browsers silently stop delivering position updates to an active watchPosition() if the user revokes permission and then grants it again — the existing watch is effectively dead and a fresh watchPosition() call is required. This is W3C-spec behavior across browsers, not Flow-specific, but applications that don't know to stop/resume the tracker see updates quietly stop. Document the caveat on Geolocation.track(Component) with the recommended recovery pattern (stop() + resume()) and a pointer to availabilitySignal() for apps that want to automate it.
|
Added javadoc about revoking access. Hopefully the workaround also works |
|
|
Tested the latest changes and the permission revoke/grant workaround, and it works well. |
|
Related-to vaadin/platform#8758 |
|
This ticket/PR has been released with Vaadin 25.2.0-alpha4. |
Update the article to match the final API shipped in vaadin/flow#23527: the per-UI facade obtained via UI.getGeolocation(), sealed GeolocationOutcome / GeolocationResult types for pattern matching, GeolocationTracker with valueSignal/activeSignal/stop/resume, the GeolocationOptions builder with Duration overloads, the availabilitySignal with its reliability caveats, and the GeolocationErrorCode enum.



Summary
Geolocationfacade (UI.getGeolocation()) that wraps the browser's Geolocation API with a one-shotget(...)request, a reactivetrack(Component)session, and anavailabilitySignal()reporting capability/permission state.GeolocationOutcome(GeolocationPosition|GeolocationError) for one-shot reads, andGeolocationResult(addsGeolocationPending) for the tracker's signal before the first fix arrives.vaadin-geolocation-availability-changeDOM event — so application code can read it (or bind to it) without an extra round-trip.Details
Geolocation.get(callback)andGeolocation.get(options, callback)enqueue a single position request; the callback receives aGeolocationOutcomeon the UI thread.Geolocation.track(Component)/track(Component, GeolocationOptions)returns aGeolocationTracker:valueSignal()—Signal<GeolocationResult>carryingGeolocationPendinguntil the first fix, then successiveGeolocationPosition/GeolocationErrorvalues.activeSignal()—Signal<Boolean>for binding a start/stop button without tracking a separate flag.stop()/resume()— cancel or resume the browser watch; the tracker handle is reusable and bound effects keep working.stop()+resume()(or driving it offavailabilitySignal()) as recovery.GeolocationOptionsis a record with a serializable builder,Durationoverloads, and validation that rejects negativetimeout/maximumAge.GeolocationError.errorCode()returns aGeolocationErrorCodeenum (PERMISSION_DENIED,POSITION_UNAVAILABLE,TIMEOUT,UNKNOWN) so switches are exhaustive without anullarm.availabilitySignal()yields aGeolocationAvailabilityenum (GRANTED/DENIED/PROMPT/UNKNOWN/UNSUPPORTED). Javadoc spells out the reliability caveats (Safari always reportsUNKNOWN, Firefox does not always propagate settings changes, a small propagation delay on Chromium) and recommendsget(...)in the callback forcritical paths.
The client bridge lives in
flow-client/src/main/frontend/Geolocation.ts, loaded as a side-effect import fromFlow.ts(the entry point Vite actually bundles).collectBrowserDetails()awaitsqueryAvailability()so the initial value is present in the first server request. kThe package is
@NullMarked(JSpecify) with@Nullableon the genuinely optional spots (options, boxed coordinate fields, internal wire records). Read-only signal wrappers are cached so callers observe stable identities. Accompanied by unit tests inGeolocationTestand an integration test (GeolocationIT+GeolocationView)covering granted/denied/error paths and the "no updates after stop()" invariant.