@docentjs/dom 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -114,7 +114,12 @@ export declare class DomRenderer implements Renderer {
114
114
  private isSheet;
115
115
  /** In sheet mode, scroll once so the target is not hidden behind the sheet. */
116
116
  private keepClearOfSheet;
117
- /** Run after a smooth scroll settles (scrollend, or a short fallback), or right away. */
117
+ /**
118
+ * Run once a smooth scroll has settled, or right away for instant scrolls.
119
+ * Settled means the target stopped moving for two frames' worth of samples,
120
+ * not a fixed delay: smooth scrolls take longer on slow or busy devices.
121
+ * `scrollend` finishes early where supported; a cap keeps it bounded.
122
+ */
118
123
  private afterScroll;
119
124
  private template;
120
125
  private buildDefault;
@@ -145,8 +150,11 @@ interface CreateTourOptions extends Omit<ControllerOptions, 'tour' | 'renderer'>
145
150
  */
146
151
  export declare class DomTourController extends TourController {
147
152
  private readonly cleanups;
153
+ private readonly followRoutes;
148
154
  constructor(tour: Tour, options?: CreateTourOptions);
155
+ start(at?: number | string): Promise<void>;
149
156
  destroy(): Promise<void>;
157
+ private listenToRoutes;
150
158
  }
151
159
  /** Create a browser-ready tour. Call `.start()` or `.resume()` on the result. */
152
160
  export declare function createTour(tour: Tour, options?: CreateTourOptions): DomTourController;
package/dist/index.d.ts CHANGED
@@ -114,7 +114,12 @@ export declare class DomRenderer implements Renderer {
114
114
  private isSheet;
115
115
  /** In sheet mode, scroll once so the target is not hidden behind the sheet. */
116
116
  private keepClearOfSheet;
117
- /** Run after a smooth scroll settles (scrollend, or a short fallback), or right away. */
117
+ /**
118
+ * Run once a smooth scroll has settled, or right away for instant scrolls.
119
+ * Settled means the target stopped moving for two frames' worth of samples,
120
+ * not a fixed delay: smooth scrolls take longer on slow or busy devices.
121
+ * `scrollend` finishes early where supported; a cap keeps it bounded.
122
+ */
118
123
  private afterScroll;
119
124
  private template;
120
125
  private buildDefault;
@@ -145,8 +150,11 @@ interface CreateTourOptions extends Omit<ControllerOptions, 'tour' | 'renderer'>
145
150
  */
146
151
  export declare class DomTourController extends TourController {
147
152
  private readonly cleanups;
153
+ private readonly followRoutes;
148
154
  constructor(tour: Tour, options?: CreateTourOptions);
155
+ start(at?: number | string): Promise<void>;
149
156
  destroy(): Promise<void>;
157
+ private listenToRoutes;
150
158
  }
151
159
  /** Create a browser-ready tour. Call `.start()` or `.resume()` on the result. */
152
160
  export declare function createTour(tour: Tour, options?: CreateTourOptions): DomTourController;
package/dist/index.js CHANGED
@@ -480,7 +480,9 @@ const STYLES = `
480
480
  --docent-width: 320px;
481
481
  --docent-overlay: #000;
482
482
  --docent-overlay-opacity: 0.55;
483
- --docent-duration: 250ms;
483
+ --docent-duration: 220ms;
484
+ /* Fast start, gentle stop: movement begins the moment you click. */
485
+ --docent-easing: cubic-bezier(0.2, 0.8, 0.2, 1);
484
486
  position: fixed;
485
487
  inset: 0;
486
488
  z-index: var(--docent-z, 2147483000);
@@ -504,7 +506,7 @@ const STYLES = `
504
506
  background: var(--docent-overlay);
505
507
  opacity: var(--docent-overlay-opacity);
506
508
  pointer-events: auto;
507
- transition: clip-path var(--docent-duration) ease;
509
+ transition: clip-path var(--docent-duration) var(--docent-easing);
508
510
  }
509
511
  .blocker {
510
512
  position: absolute;
@@ -524,9 +526,12 @@ const STYLES = `
524
526
  padding: 16px;
525
527
  pointer-events: auto;
526
528
  outline: none;
527
- transition: transform var(--docent-duration) ease, opacity var(--docent-duration) ease;
529
+ transition: transform var(--docent-duration) var(--docent-easing), opacity var(--docent-duration) var(--docent-easing);
528
530
  }
529
531
  .popover[data-entering] { opacity: 0; transition: none; }
532
+ /* Between steps the popover slides; only its content cross-fades, briefly. */
533
+ .popover[data-moving] > * { animation: docent-swap 160ms ease-out; }
534
+ @keyframes docent-swap { from { opacity: 0; } to { opacity: 1; } }
530
535
  .popover.headless {
531
536
  width: auto;
532
537
  max-width: none;
@@ -598,6 +603,7 @@ const STYLES = `
598
603
  .button:focus-visible, .close:focus-visible { outline: 2px solid var(--docent-accent); outline-offset: 2px; }
599
604
  @media (prefers-reduced-motion: reduce) {
600
605
  .overlay, .popover { transition: none; }
606
+ .popover[data-moving] > * { animation: none; }
601
607
  }
602
608
  `;
603
609
  //#endregion
@@ -754,6 +760,7 @@ var DomRenderer = class {
754
760
  show(ctx) {
755
761
  const firstStep = !this.host;
756
762
  const host = this.mount();
763
+ const from = this.popover?.style.transform || null;
757
764
  this.teardownStep();
758
765
  this.ctx = ctx;
759
766
  this.target = ctx.step.target === void 0 ? null : resolveTarget(ctx.step.target, this.doc);
@@ -761,12 +768,15 @@ var DomRenderer = class {
761
768
  applyTheme(host, mergeThemes(this.options.theme, template?.theme, ctx.tour.options?.theme));
762
769
  this.setTemplateCss(template?.css);
763
770
  const initialFocus = this.options.headless ? this.buildHeadless(ctx, host, this.options.headless) : this.buildDefault(ctx, host, template);
764
- this.popover?.setAttribute("data-entering", "");
771
+ if (this.popover && from) {
772
+ this.popover.style.transform = from;
773
+ this.popover.setAttribute("data-moving", "");
774
+ } else this.popover?.setAttribute("data-entering", "");
765
775
  if (this.target) {
766
776
  const smooth = this.scrollIntoView(this.target, ctx.step);
767
777
  if (this.options.avoidOcclusion !== false) {
768
778
  const target = this.target;
769
- this.afterScroll(smooth, () => {
779
+ this.afterScroll(smooth, target, () => {
770
780
  if (this.target === target && uncover(target, host, this.viewport())) this.update();
771
781
  });
772
782
  }
@@ -909,28 +919,41 @@ var DomRenderer = class {
909
919
  behavior: "auto"
910
920
  });
911
921
  }
912
- /** Run after a smooth scroll settles (scrollend, or a short fallback), or right away. */
913
- afterScroll(smooth, fn) {
922
+ /**
923
+ * Run once a smooth scroll has settled, or right away for instant scrolls.
924
+ * Settled means the target stopped moving for two frames' worth of samples,
925
+ * not a fixed delay: smooth scrolls take longer on slow or busy devices.
926
+ * `scrollend` finishes early where supported; a cap keeps it bounded.
927
+ */
928
+ afterScroll(smooth, target, fn) {
914
929
  const win = this.doc.defaultView;
915
930
  if (!smooth || !win) {
916
931
  fn();
917
932
  return;
918
933
  }
919
934
  let done = false;
920
- const finish = () => {
921
- if (done) return;
935
+ let lastTop = NaN;
936
+ let stableSamples = 0;
937
+ const stop = () => {
922
938
  done = true;
923
939
  win.removeEventListener("scrollend", finish);
924
- clearTimeout(timer);
940
+ clearInterval(poll);
941
+ clearTimeout(cap);
942
+ };
943
+ const finish = () => {
944
+ if (done) return;
945
+ stop();
925
946
  fn();
926
947
  };
927
- const timer = setTimeout(finish, 600);
948
+ const poll = setInterval(() => {
949
+ const top = target.getBoundingClientRect().top;
950
+ stableSamples = Math.abs(top - lastTop) < .5 ? stableSamples + 1 : 0;
951
+ lastTop = top;
952
+ if (stableSamples >= 2) finish();
953
+ }, 80);
954
+ const cap = setTimeout(finish, 3e3);
928
955
  win.addEventListener("scrollend", finish, { once: true });
929
- this.cleanups.push(() => {
930
- done = true;
931
- win.removeEventListener("scrollend", finish);
932
- clearTimeout(timer);
933
- });
956
+ this.cleanups.push(stop);
934
957
  }
935
958
  template(ctx) {
936
959
  const name = ctx.tour.options?.template ?? this.options.template;
@@ -1095,6 +1118,9 @@ var DomRenderer = class {
1095
1118
  }
1096
1119
  onKeydown(e, ctx) {
1097
1120
  const options = ctx.tour.options ?? {};
1121
+ const path = typeof e.composedPath === "function" ? e.composedPath() : [];
1122
+ const origin = path[0] ?? e.target;
1123
+ if (path.some((n) => n instanceof Element && n.hasAttribute("data-docent-ignore-keys"))) return;
1098
1124
  if (e.key === "Escape" && options.allowClose !== false) {
1099
1125
  e.preventDefault();
1100
1126
  ctx.actions.skip();
@@ -1105,7 +1131,7 @@ var DomRenderer = class {
1105
1131
  return;
1106
1132
  }
1107
1133
  if (options.keyboard === false) return;
1108
- if (e.target instanceof HTMLElement && /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) return;
1134
+ if (origin instanceof HTMLElement && (/^(INPUT|TEXTAREA|SELECT)$/.test(origin.tagName) || origin.isContentEditable)) return;
1109
1135
  if (e.key === "ArrowRight" && ctx.step.buttons?.next !== false) {
1110
1136
  e.preventDefault();
1111
1137
  ctx.actions.next();
@@ -1200,6 +1226,7 @@ function createLocalStorage(storage) {
1200
1226
  */
1201
1227
  var DomTourController = class extends TourController {
1202
1228
  cleanups = [];
1229
+ followRoutes;
1203
1230
  constructor(tour, options = {}) {
1204
1231
  const { renderer: rendererOptions, followRoutes, ...rest } = options;
1205
1232
  const renderer = new DomRenderer(rendererOptions);
@@ -1209,24 +1236,30 @@ var DomTourController = class extends TourController {
1209
1236
  renderer,
1210
1237
  storage: rest.storage ?? createLocalStorage()
1211
1238
  });
1212
- if (followRoutes !== false && typeof window !== "undefined") {
1213
- const onChange = () => void this.routeChanged();
1214
- for (const type of ["popstate", "hashchange"]) {
1215
- window.addEventListener(type, onChange);
1216
- this.cleanups.push(() => window.removeEventListener(type, onChange));
1217
- }
1218
- const nav = window.navigation;
1219
- if (nav) {
1220
- nav.addEventListener("navigatesuccess", onChange);
1221
- this.cleanups.push(() => nav.removeEventListener("navigatesuccess", onChange));
1222
- }
1223
- }
1239
+ this.followRoutes = followRoutes !== false;
1240
+ }
1241
+ async start(at) {
1242
+ this.listenToRoutes();
1243
+ return super.start(at);
1224
1244
  }
1225
1245
  async destroy() {
1226
1246
  for (const c of this.cleanups) c();
1227
1247
  this.cleanups.length = 0;
1228
1248
  await super.destroy();
1229
1249
  }
1250
+ listenToRoutes() {
1251
+ if (!this.followRoutes || this.cleanups.length > 0 || typeof window === "undefined") return;
1252
+ const onChange = () => void this.routeChanged();
1253
+ for (const type of ["popstate", "hashchange"]) {
1254
+ window.addEventListener(type, onChange);
1255
+ this.cleanups.push(() => window.removeEventListener(type, onChange));
1256
+ }
1257
+ const nav = window.navigation;
1258
+ if (nav) {
1259
+ nav.addEventListener("navigatesuccess", onChange);
1260
+ this.cleanups.push(() => nav.removeEventListener("navigatesuccess", onChange));
1261
+ }
1262
+ }
1230
1263
  };
1231
1264
  /** Create a browser-ready tour. Call `.start()` or `.resume()` on the result. */
1232
1265
  function createTour(tour, options) {