mirror of
https://github.com/kamranahmedse/driver.js.git
synced 2026-10-31 03:52:13 +08:00
Clean up the hints module
show() now mounts hints whose elements have appeared since the last call, which the docs already promised but only a full hide()/show() delivered; mountHints() skips what is already on the page, making show() naturally idempotent and simpler. The closePopover()/close() pair collapses into one function, the scroll and resize listeners use requireRefresh directly instead of wrapper closures, and config/dismissed become consts. Comments get a pass: the HintsConfig overlay comment still described the pre-cutout semantics (beacons above the overlay) and is corrected; obvious comments are dropped, the load-bearing ones tightened, and em dashes across source comments replaced per Kamran's preference.
This commit is contained in:
@@ -214,7 +214,7 @@ Each hint in the `hints` array:
|
||||
```js
|
||||
const hint = {
|
||||
// Selector, element, or a function returning one. A hint whose element
|
||||
// is missing is skipped and picked up again on the next show()/refresh().
|
||||
// is missing is skipped and picked up again on the next show().
|
||||
element: "#export-btn",
|
||||
|
||||
// Stable identity, used by open/dismiss/restore and in the hooks.
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.driver-no-interaction, .driver-no-interaction * {
|
||||
.driver-no-interaction,
|
||||
.driver-no-interaction * {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/* The hint popover shares its styling with the tour, so this entry pulls in
|
||||
the same popover stylesheet the tour uses. Importing hints.css is therefore
|
||||
all a hints-only consumer needs — no separate popover styles to remember.
|
||||
Everything below is beacon-specific. */
|
||||
/* Popover styles are shared with the tour; importing hints.css alone is
|
||||
enough for hints-only consumers. Everything below is hint-specific. */
|
||||
@import "./popover.css";
|
||||
|
||||
.driver-hint {
|
||||
@@ -12,10 +10,9 @@
|
||||
width: var(--driver-hint-size, 24px);
|
||||
height: var(--driver-hint-size, 24px);
|
||||
cursor: pointer;
|
||||
/* Above the page but below the overlay, so when a hint is open with the
|
||||
overlay enabled, the other beacons dim with the rest of the page. */
|
||||
/* Below the overlay, so other beacons dim with the page while a hint is open. */
|
||||
z-index: 999999996;
|
||||
/* The beacon is centered on the anchor point set inline by the script. */
|
||||
/* Centered on the anchor point that the script sets inline. */
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@@ -32,13 +29,10 @@
|
||||
.driver-hint-pulse {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
/* A light indigo by default — noticeable without shouting, and it reads on
|
||||
both light and dark pages; override with --driver-hint-color. */
|
||||
background-color: var(--driver-hint-color, #818cf8);
|
||||
}
|
||||
|
||||
.driver-hint-dot {
|
||||
/* The solid core, half the size of the beacon box. */
|
||||
inset: 25%;
|
||||
}
|
||||
|
||||
@@ -75,28 +69,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* A larger arrow than the tour's 5px triangle, proportionate to the beacon.
|
||||
The placement code measures the arrow, so the size only lives here — if you
|
||||
restyle it, alignment follows. Kept in sync with HINT_ARROW_SIZE in
|
||||
hints.ts, which shifts the popover so the tip lands on the beacon. */
|
||||
/* A larger arrow than the tour's, proportionate to the beacon. Placement
|
||||
measures the arrow, so restyling it keeps the tip on target; only the
|
||||
beacon shift assumes the size (HINT_ARROW_SIZE in hints.ts). */
|
||||
.driver-hint-popover .driver-popover-arrow {
|
||||
border-width: 7px;
|
||||
}
|
||||
|
||||
/* box-shadow stops at the popover's box, which leaves the protruding arrow
|
||||
flat and shadow-less. A drop-shadow filter follows the rendered silhouette
|
||||
instead, so the popover and its arrow cast one seamless shadow, whatever
|
||||
the theme's colors are. */
|
||||
/* box-shadow stops at the popover's box, leaving the protruding arrow flat.
|
||||
A drop-shadow filter follows the rendered silhouette instead, so the box
|
||||
and the arrow cast one seamless shadow in any theme. */
|
||||
.driver-hint-popover.driver-popover {
|
||||
box-shadow: none;
|
||||
filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.25)) drop-shadow(0 4px 12px rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
|
||||
/* The opt-in overlay shown while a hint is open: a full-screen dim with the
|
||||
hint's element cut out. Only the open hint's beacon and popover sit above
|
||||
it. The svg itself lets pointer events through (the element in the cutout
|
||||
stays interactive); the dimmed path catches them, so clicking the dim
|
||||
closes the hint. */
|
||||
/* The opt-in dim with the hint's element cut out. The svg lets pointer events
|
||||
through (the cutout stays interactive); the dimmed path catches them, and
|
||||
clicking it closes the hint. */
|
||||
.driver-hint-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -119,8 +109,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* A tour takes over the screen, so the beacons step aside until it ends. The
|
||||
tour marks the body while it runs, which keeps the two decoupled. */
|
||||
/* The tour marks the body while it runs; beacons step aside until it ends. */
|
||||
.driver-active .driver-hint {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -16,13 +16,11 @@ import "./hints.css";
|
||||
const OVERLAY_PADDING = 10;
|
||||
const OVERLAY_RADIUS = 5;
|
||||
|
||||
// The hint arrow is rendered larger than the tour's (7px borders — see
|
||||
// hints.css); the popover shift that puts the arrow tip on the beacon has to
|
||||
// assume the same size, since it is computed before the popover exists.
|
||||
// Must match the arrow's border-width in hints.css; the popover shift that
|
||||
// puts the arrow tip on the beacon runs before the popover exists, so the
|
||||
// size cannot be measured.
|
||||
const HINT_ARROW_SIZE = 14;
|
||||
|
||||
// Part of the hints API surface: beacon/popover placement and the DOM handed
|
||||
// to onPopoverRender are described with the popover primitive's types.
|
||||
export type { Alignment, PopoverDOM, Side } from "./popover";
|
||||
|
||||
export type HintBeacon = {
|
||||
@@ -76,9 +74,10 @@ export type HintsConfig = {
|
||||
popoverClass?: string;
|
||||
popoverOffset?: number;
|
||||
|
||||
// Dim the page while a hint is open. The beacons and the popover stay above
|
||||
// the overlay so other hints remain clickable; clicking the dimmed page
|
||||
// closes the open hint like any outside click.
|
||||
// Dim the page while a hint is open, with the hint's element cut out like a
|
||||
// tour step. The popover then anchors to the element, the open hint's beacon
|
||||
// steps aside, and the other beacons wait under the dim; clicking the dimmed
|
||||
// page closes the hint.
|
||||
overlay?: boolean;
|
||||
overlayColor?: string;
|
||||
overlayOpacity?: number;
|
||||
@@ -110,9 +109,9 @@ type MountedHint = {
|
||||
};
|
||||
|
||||
export function hints(config: HintsConfig = {}): Hints {
|
||||
let currentConfig: HintsConfig = { ...config };
|
||||
const currentConfig: HintsConfig = { ...config };
|
||||
const dismissed = new Set<string>();
|
||||
let mounted: MountedHint[] = [];
|
||||
let dismissed = new Set<string>();
|
||||
let teardown: (() => void)[] = [];
|
||||
|
||||
let activeId: string | undefined;
|
||||
@@ -205,10 +204,10 @@ export function hints(config: HintsConfig = {}): Hints {
|
||||
}
|
||||
|
||||
function mountHint(hint: DriverHint, id: string) {
|
||||
const element = resolveElement(hint.element);
|
||||
// A hint without an anchor has nothing to point at. It is skipped rather
|
||||
// than centered like a tour's element-less popover, and picked up on the
|
||||
// next show()/refresh() if the element appears later.
|
||||
// next show() if the element appears later.
|
||||
const element = resolveElement(hint.element);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
@@ -226,7 +225,7 @@ export function hints(config: HintsConfig = {}): Hints {
|
||||
function mountHints() {
|
||||
(currentConfig.hints || []).forEach((hint, index) => {
|
||||
const id = hintId(hint, index);
|
||||
if (dismissed.has(id)) {
|
||||
if (dismissed.has(id) || find(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -252,9 +251,6 @@ export function hints(config: HintsConfig = {}): Hints {
|
||||
}
|
||||
|
||||
function bindListeners() {
|
||||
const onScroll = () => requireRefresh();
|
||||
const onResize = () => requireRefresh();
|
||||
|
||||
const onDocumentClick = (event: MouseEvent) => {
|
||||
if (!activeId) {
|
||||
return;
|
||||
@@ -275,22 +271,22 @@ export function hints(config: HintsConfig = {}): Hints {
|
||||
return;
|
||||
}
|
||||
|
||||
// Escape came from the keyboard, so send focus back where it started.
|
||||
const beacon = find(activeId)?.beacon;
|
||||
close();
|
||||
// Escape came from the keyboard, so send focus back where it started.
|
||||
beacon?.focus();
|
||||
};
|
||||
|
||||
// Capture, so scrolling inside a nested container repositions the beacons
|
||||
// too — those events never reach the window during the bubble phase.
|
||||
window.addEventListener("scroll", onScroll, true);
|
||||
window.addEventListener("resize", onResize);
|
||||
// too; those events never reach the window during the bubble phase.
|
||||
window.addEventListener("scroll", requireRefresh, true);
|
||||
window.addEventListener("resize", requireRefresh);
|
||||
document.addEventListener("click", onDocumentClick);
|
||||
window.addEventListener("keyup", onKeyup);
|
||||
|
||||
teardown = [
|
||||
() => window.removeEventListener("scroll", onScroll, true),
|
||||
() => window.removeEventListener("resize", onResize),
|
||||
() => window.removeEventListener("scroll", requireRefresh, true),
|
||||
() => window.removeEventListener("resize", requireRefresh),
|
||||
() => document.removeEventListener("click", onDocumentClick),
|
||||
() => window.removeEventListener("keyup", onKeyup),
|
||||
];
|
||||
@@ -424,15 +420,15 @@ export function hints(config: HintsConfig = {}): Hints {
|
||||
};
|
||||
}
|
||||
|
||||
function closePopover() {
|
||||
function close() {
|
||||
if (!popover) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = find(activeId!);
|
||||
entry?.beacon.setAttribute("aria-expanded", "false");
|
||||
// Bring back a beacon that overlay mode tucked away.
|
||||
if (entry) {
|
||||
entry.beacon.setAttribute("aria-expanded", "false");
|
||||
// Bring back a beacon that overlay mode tucked away.
|
||||
entry.beacon.style.display = "";
|
||||
}
|
||||
|
||||
@@ -450,7 +446,7 @@ export function hints(config: HintsConfig = {}): Hints {
|
||||
}
|
||||
|
||||
// Only one hint is open at a time; opening another swaps it out.
|
||||
closePopover();
|
||||
close();
|
||||
|
||||
activeId = entry.id;
|
||||
entry.beacon.setAttribute("aria-expanded", "true");
|
||||
@@ -468,10 +464,6 @@ export function hints(config: HintsConfig = {}): Hints {
|
||||
onOpen?.(entry.element, entry.hint, { config: currentConfig, hints: api });
|
||||
}
|
||||
|
||||
function close() {
|
||||
closePopover();
|
||||
}
|
||||
|
||||
function toggle(id: string | number) {
|
||||
if (activeId === `${id}`) {
|
||||
close();
|
||||
@@ -488,7 +480,7 @@ export function hints(config: HintsConfig = {}): Hints {
|
||||
}
|
||||
|
||||
if (activeId === entry.id) {
|
||||
closePopover();
|
||||
close();
|
||||
}
|
||||
|
||||
dismissed.add(entry.id);
|
||||
@@ -535,15 +527,15 @@ export function hints(config: HintsConfig = {}): Hints {
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (isVisible) {
|
||||
// Idempotent: re-resolve elements that have appeared since.
|
||||
refresh();
|
||||
return;
|
||||
if (!isVisible) {
|
||||
isVisible = true;
|
||||
bindListeners();
|
||||
}
|
||||
|
||||
isVisible = true;
|
||||
// mountHints() skips what is already on the page, so calling show() again
|
||||
// picks up hints whose elements have appeared since.
|
||||
mountHints();
|
||||
bindListeners();
|
||||
refresh();
|
||||
}
|
||||
|
||||
function hide() {
|
||||
@@ -552,7 +544,7 @@ export function hints(config: HintsConfig = {}): Hints {
|
||||
}
|
||||
|
||||
isVisible = false;
|
||||
closePopover();
|
||||
close();
|
||||
unmountHints();
|
||||
|
||||
teardown.forEach(off => off());
|
||||
@@ -574,7 +566,7 @@ export function hints(config: HintsConfig = {}): Hints {
|
||||
return;
|
||||
}
|
||||
|
||||
closePopover();
|
||||
close();
|
||||
unmountHints();
|
||||
mountHints();
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ function createOverlaySvg(ctx: Context, stage: StageDefinition): SVGSVGElement {
|
||||
return svg;
|
||||
}
|
||||
|
||||
// The full-screen dim with a rounded cutout, as a single evenodd path. Pure —
|
||||
// The full-screen dim with a rounded cutout, as a single evenodd path. Pure:
|
||||
// the tour passes its stagePadding/stageRadius, the hints overlay its own.
|
||||
export function generateStageSvgPathString(stage: StageDefinition, options: { padding: number; radius: number }) {
|
||||
const windowX = window.innerWidth;
|
||||
|
||||
@@ -53,7 +53,7 @@ export type PopoverDOM = {
|
||||
};
|
||||
|
||||
// Everything the primitive needs to render a popover, fully resolved by the
|
||||
// caller — no config or state fallbacks happen at this level.
|
||||
// caller; no config or state fallbacks happen at this level.
|
||||
export type PopoverRenderOptions = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
|
||||
@@ -87,7 +87,7 @@ function resolveStepPopover(ctx: Context, element: Element, step: DriveStep): Po
|
||||
},
|
||||
|
||||
onRender: popoverDom => {
|
||||
// Commit the popover to state before the user hook runs — the hook's
|
||||
// Commit the popover to state before the user hook runs; the hook's
|
||||
// opts.state.popover has always pointed at the freshly rendered popover.
|
||||
ctx.setState("popover", popoverDom);
|
||||
|
||||
|
||||
@@ -107,17 +107,16 @@ describe("mounting beacons", () => {
|
||||
});
|
||||
|
||||
it("picks up an element that appears later on show()", () => {
|
||||
const productHints = createHints({ hints: [{ element: "#late" }] });
|
||||
const productHints = createHints({ hints: [{ element: "#late" }, ...SAMPLE_HINTS] });
|
||||
productHints.show();
|
||||
expect(beacons()).toHaveLength(0);
|
||||
expect(beacons()).toHaveLength(2);
|
||||
|
||||
const late = document.createElement("div");
|
||||
late.id = "late";
|
||||
document.body.appendChild(late);
|
||||
productHints.hide();
|
||||
productHints.show();
|
||||
|
||||
expect(beacons()).toHaveLength(1);
|
||||
expect(beacons()).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("resolves an element node and a function element", () => {
|
||||
|
||||
Reference in New Issue
Block a user