@nstudio/nativescript-driver
v1.0.1
Published
Use the driver.js walkthrough/coachmark library as-is in NativeScript (dominative + MasonKit DOM), rendered natively on iOS and Android.
Readme
@nstudio/nativescript-driver
Use the popular web walkthrough library driver.js as-is in a NativeScript app. The driver.js package runs unmodified; this plugin provides the small NativeScript DOM compatibility layer that lets it render natively on iOS and Android on top of dominative + @triniwiz/nativescript-masonkit.
How it works
driver.js draws a full-screen <svg> with a single <path fill-rule="evenodd"> cutout (animated each frame via requestAnimationFrame) and a positioned popover. None of that runs on a logical DOM out of the box, so the plugin fills the gaps:
getBoundingClientRect→view.getLocationInWindow()+getActualSize()(DIPs).window.innerWidth/innerHeight→Screen.mainScreen.*DIPs;requestAnimationFrameis the native global;getComputedStyleis a minimal shim.classList/contains/closest/matches/querySelector(All)/getElementById/innerHTMLare patched onto the element/document prototypes.svg/pathelements are registered with dominative and backed by a self-contained dim overlay (no extra native dependency); the dim extends through the top safe area. On iOS the cutout is a rounded-corner hole punched with an even-oddCAShapeLayermask (matching driver'sstageRadius); on Android it's a real rectangular hole left by four dim rectangles.- The overlay + popover are hosted on top of the app in a full-screen layer (
RootLayoutpreferred), re-routing driver'sdocument.bodyappends. - Native taps are translated into the capturing DOM
clickpipeline driver.js listens for, so the backdrop and popover buttons work unchanged.
Install
npm install @nstudio/nativescript-driver driver.js dominative @triniwiz/nativescript-masonkitUsage (React)
Import the plugin once, after your dominative element registration, so the DOM layer installs and svg/path are registered:
// src/native/index.tsx
import './dom'; // your dominative element registration
import '@nstudio/nativescript-driver'; // installs the DOM compat layerRequired: wrap your app root in a NativeScript RootLayout. The overlay mounts into it as a true full-window layer; without it the cutout/popover are offset by the page's safe-area inset.
<rootlayout>
<gridlayout>{/* your app */}</gridlayout>
</rootlayout>Then use driver.js exactly like on the web:
import { driver } from '@nstudio/nativescript-driver';
// or the React hook:
import { useDriver } from '@nstudio/nativescript-driver/react';
function HelpButton({ saveRef }) {
const tour = useDriver({
showProgress: true,
steps: [
{ element: saveRef.current, popover: { title: 'Save', description: 'Tap to save.', side: 'bottom' } },
{ element: '#profile-tab', popover: { title: 'Profile', description: 'Your profile lives here.' } },
],
});
return <button text="Help" onTap={() => tour.drive()} />;
}step.element accepts a dominative element reference (ref.current) or a simple
#id selector (matched against the NativeScript view id).
Implementation notes
A few NativeScript/MasonKit specifics the adapter handles internally:
- The overlay is hosted in a RootLayout (full window) as two stacked sibling layers - a dim layer and a popover layer above it - so the popover always renders on top of the dim (a single layer doesn't reliably z-order the MasonKit popover over the dim). A single hit-testing tap handler on the top layer routes taps to popover buttons / popover body / backdrop.
- Deterministic popover positioning. driver's own side selection depends on async MasonKit text measurement and is non-deterministic on mobile (it flips the popover to different sides run-to-run). The adapter instead positions the popover from the cutout (target) rect it parses from the SVG path - honoring driver's preferred side first, then falling back to whichever side fits - so placement is stable and always on-screen. The popover stays hidden until its position settles, so it never visibly jumps.
- Dim through the safe area. The host layers and dim view disable iOS view clipping so the dim extends up into the status/notch area, dimming the full window.
- Rounded cutout (iOS). The dim is one full-window view whose
layer.maskis an even-oddCAShapeLayer(outer rect + rounded-rect hole). NativeScript can resetlayer.maskduring its layout pass, so the mask is re-applied on the view'slayoutChanged(andloaded) — otherwise the corners render square. - The popover is a MasonKit element that ignores
AbsoluteLayoutattached props and lays its own frame at an unstable origin (translating it relative to the layer origin intermittently parked it in the top-left corner). So the adapter wraps the popover in a coreGridLayoutholder and positions the holder viaAbsoluteLayout(core layouts honor it reliably) — stable, with no transform feedback. The popover is revealed on its first valid placement (the placement is reliable, so there's no slow wait-for-stability). Its footer (progress + Prev/Next) is laid out as a column so the buttons stay within the narrow mobile popover width and never run off-screen (MasonKit doesn't honorflex-wrap). - Footer spacing/clipping. Three MasonKit text quirks are handled in the footer: (1) a
MasonText<span>ignoresmargin, so the vertical gap between the progress text and the nav buttons is a fixed-height container spacer (containers honor height); (2) the nav row is left-aligned and content-sized (alignSelf: flex-start) rather than stretched, so the buttons pack together and the last button never sits flush against — and clip on — the popover's right edge; (3)MasonTextunder-measures its own text width, so the buttons get aminWidthto leave room for a wide final glyph (e.g. the "e" in "Done"). - Footer button alignment & popover padding. The leading button ("Previous") is given
padding-left: 0(via the injected CSS, which is what actually wins for padding here — inline padding on these MasonKit nodes is dropped) so its text starts flush with the title/description/progress left edge. The popover's own content padding (the inset of all text/buttons from the white card edges) is set with the CSSpaddingshorthand — the wrapper is a MasonKitDiv/Scrollthat honors thepaddingshorthand but not thepadding-top/right/bottom/leftlonghands (the longhands barely inset the content), so always pad it via the shorthand. - Type hierarchy via MasonText slots. driver builds the popover title as a
<header>and the description as a<div>— both container elements that lay out their text as a childMasonTextNode. AMasonTextNode's attributed glyph run is fixed at creation (the framework constructs it withnullattributes), so a font set on the container never re-renders the text — neither re-measuring nor swapping the node redraws it. The adapter therefore renders the title/description text through a child<span>(aMasonTextview, which renders its own live font), routes the container's text writes into that span, and styles the span — yielding a bold title and a smaller description (matching the web hierarchy). Progress (<span>) and the buttons (<button>) are alreadyMasonText, so they're styled directly. The adapter uses a deep selector walk to reach these nested nodes (MasonKit's own-instancequerySelectorfinds only direct children). - Footer buttons render as text links, not boxed pills. MasonKit's
Button, when nested in the footer's text-like container, clips the top of a bordered/filled box (the box can't be un-clipped via padding,clipsToBounds, ordisplay:flex), and settingborder/background/coloron a MasonKitButtoninline crashes (Mason bridges them as ObjC strings). So Prev/Next/Done are styled (via injected CSS) as colored text actions — which can't be clipped and read cleanly on mobile. - Button hit-testing is region-based. The nested MasonKit footer buttons don't report a window location and don't receive native taps, so Next/Prev/Done are located by the navigation row's frame plus accumulated button widths (DOM order Prev→Next).
- Close button removed. driver's
×close button renders as a stray bordered box in MasonKit (which honors neitherdisplay:nonenorvisibility:collapsehere), so the adapter removes it from the popover. Dismiss via a backdrop tap or the Done button on the last step. - Robust teardown. When the tour ends (overlay + popover both removed), the host layers are immediately made non-interactive + collapsed and then removed on the next tick. The immediate step matters because teardown often runs inside the popover layer's own tap-gesture callback (a native backdrop dismiss), where removing a view from the tree is silently dropped by iOS - leaving an invisible touch-blocking layer that would freeze the page.
- Popover text is set via the element's native
textproperty + a child text node (driver'sinnerHTML/innerTextare routed through this atdocument.createElementtime). - Android footer buttons. A MasonKit
Buttonnested in the footer's nav-row<span>(aText) doesn't lay out or paint on Android (it collapses to zero size). So on Android the adapter renders the Prev/Next/Done labels as<span>s (aTextmeasures + paints reliably) inside a real<view>row, routes each real button's text into its label span (via the same text-slot mechanism used for the title/description), and routes a tap on a label back to the real (hidden) button so driver's click handler fires. iOS keeps laying the buttons out inside the nav-row span directly. - Android popover padding is
24(vs12on iOS) —12reads too tight on Android. The nav-row label gap is a fixed-width spacer<view>(aTextspan ignoresmargin).
Fidelity notes as of v1
- Rounded cutout corners (
stageRadius) are rendered on both platforms: iOS punches the hole with an even-oddCAShapeLayermask; Android paints the dim and punches a rounded-rect hole with aPorterDuff.CLEARforegroundDrawable(asaveLayer+Canvas.drawRoundRect). - Popover arrow is not drawn (collapsed to avoid corrupting popover positioning).
- Footer buttons are colored text actions, not boxed pills (MasonKit clips the button box and crashes on inline button border/background — see implementation notes).
- Hole tap-through: tapping the spotlighted element counts as a backdrop tap (true pass-through needs a native hit-test). Matches driver's default close-on-overlay behavior.
- Auto-scroll to a target (
scrollIntoView) is supported on iOS, both down and up: the adapter scrolls the page's MasonKitScrollcontainer to center the target (skipping when it's already comfortably visible), and the cutout + popover follow. The scroll is animated (a ~340 msrequestAnimationFrameease-in-out that drives the container's modelcontentOffseteach frame — not the nativesetContentOffset(animated:), whose presentation-only animation would desync the cutout from the page sincegetLocationInWindowjumps straight to the final offset). To also scroll targets that sit near the very top under a sticky/floating header (which driver's own in-view check would skip), set driver'sonHighlightStarted: (el) => el?.scrollIntoView({ block: 'center' }). On Android the same works via the native scroll container'ssmoothScrollTo(whose animation moves the real scroll position, so the cutout/popover track it natively). - Keyboard navigation (Esc/arrows/Tab) is a desktop concern and is a no-op on mobile.
- Complex CSS selectors for
step.elementare best-effort; prefer element refs.
License
Apache-2.0
