@bearmenu/ui 0.8.21 → 0.8.23

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.
Files changed (50) hide show
  1. package/dist/{chunk-DJD3EMPK.js → chunk-YE4ZQUTI.js} +1 -1
  2. package/dist/components/alert.d.ts +2 -2
  3. package/dist/components/badge.d.ts +1 -1
  4. package/dist/components/button.d.ts +1 -1
  5. package/dist/components/coach-marks.d.ts +43 -6
  6. package/dist/components/coach-marks.js +116 -30
  7. package/dist/components/command.d.ts +7 -7
  8. package/dist/components/concept-compare.d.ts +1 -1
  9. package/dist/components/doorway-tiles.d.ts +1 -1
  10. package/dist/components/grade-atom.d.ts +1 -1
  11. package/dist/components/grade-atom.js +1 -1
  12. package/dist/components/kitchen-dossier.d.ts +1 -1
  13. package/dist/components/menu-dead-end.d.ts +1 -1
  14. package/dist/components/menu-family-block.d.ts +1 -1
  15. package/dist/components/menu-family-card.d.ts +1 -1
  16. package/dist/components/menu-filter-chips.d.ts +1 -1
  17. package/dist/components/menu-group-rows.d.ts +1 -1
  18. package/dist/components/menu-index-header.d.ts +1 -1
  19. package/dist/components/menu-index-list.d.ts +1 -1
  20. package/dist/components/menu-intent-hero.d.ts +1 -1
  21. package/dist/components/menu-item.d.ts +1 -1
  22. package/dist/components/menu-kitchen-list.d.ts +1 -1
  23. package/dist/components/menu-mocks.d.ts +1 -1
  24. package/dist/components/menu-types.d.ts +1 -1
  25. package/dist/components/open-now-list.d.ts +1 -1
  26. package/dist/components/place-about-tab.d.ts +1 -1
  27. package/dist/components/place-card.d.ts +1 -1
  28. package/dist/components/place-card.js +1 -1
  29. package/dist/components/place-details-mobile.d.ts +1 -1
  30. package/dist/components/place-schedule-week.d.ts +1 -1
  31. package/dist/components/place-types.d.ts +1 -1
  32. package/dist/components/qa-block.d.ts +1 -1
  33. package/dist/components/spinner.d.ts +2 -2
  34. package/dist/components/tag.d.ts +2 -2
  35. package/dist/components/text.d.ts +3 -3
  36. package/dist/components/top-comparison.d.ts +1 -1
  37. package/dist/components/top-cross-link.d.ts +1 -1
  38. package/dist/components/top-ranking-entry.d.ts +1 -1
  39. package/dist/components/top-ranking-hero.d.ts +1 -1
  40. package/dist/components/top-types.d.ts +1 -1
  41. package/dist/components/venue-grade-dimensions.d.ts +1 -1
  42. package/dist/components/venue-quotes.d.ts +1 -1
  43. package/dist/components/venue-wall.d.ts +1 -1
  44. package/dist/{place-types-BHdeMMiG.d.ts → place-types-C4rR3LTs.d.ts} +7 -0
  45. package/package.json +1 -1
  46. package/src/components/coach-marks.stories.tsx +20 -4
  47. package/src/components/coach-marks.test.tsx +181 -6
  48. package/src/components/coach-marks.tsx +204 -45
  49. package/src/components/grade-atom.test.tsx +32 -0
  50. package/src/components/grade-atom.tsx +8 -1
@@ -1,7 +1,7 @@
1
1
  import { OVERLAY_PILL } from '../chunk-EZG2NELX.js';
2
2
  import { HOVER_IMAGE_MOTION, HOVER_IMAGE_WRAPPER, HOVER_MOTION, HOVER_LIFT_GRID_GROUP } from '../chunk-XCB3F6SF.js';
3
3
  import { NEUTRAL_ICON_FILTER } from '../chunk-LQWGZIYA.js';
4
- import { GradeAtom } from '../chunk-DJD3EMPK.js';
4
+ import { GradeAtom } from '../chunk-YE4ZQUTI.js';
5
5
  import { showsGradeAtom } from '../chunk-I5V446BX.js';
6
6
  import { Separator } from '../chunk-NWZGSLPU.js';
7
7
  import { springSnappy } from '../chunk-OIV7B44M.js';
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { P as PlacePreview, e as PlaceDetailsMobileLabels } from '../place-types-BHdeMMiG.js';
2
+ import { P as PlacePreview, e as PlaceDetailsMobileLabels } from '../place-types-C4rR3LTs.js';
3
3
  import '../lib/grade.js';
4
4
 
5
5
  interface PlaceDetailsMobileProps {
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { f as PlaceSchedule, g as PlaceScheduleWeekLabels } from '../place-types-BHdeMMiG.js';
2
+ import { f as PlaceSchedule, g as PlaceScheduleWeekLabels } from '../place-types-C4rR3LTs.js';
3
3
  import '../lib/grade.js';
4
4
 
5
5
  interface PlaceScheduleWeekProps {
@@ -1,3 +1,3 @@
1
- export { C as CitedText, k as CitedValue, M as MenuSizeAndPrice, b as PlaceAboutTabLabels, d as PlaceCardLabels, e as PlaceDetailsMobileLabels, l as PlaceGrade, a as PlaceGradeDimensions, m as PlaceHourBlock, c as PlaceLinkKind, n as PlaceMustTry, P as PlacePreview, o as PlacePriceRange, p as PlaceReviewsDigest, f as PlaceSchedule, g as PlaceScheduleWeekLabels, W as WeekdayKey } from '../place-types-BHdeMMiG.js';
1
+ export { C as CitedText, k as CitedValue, M as MenuSizeAndPrice, b as PlaceAboutTabLabels, d as PlaceCardLabels, e as PlaceDetailsMobileLabels, l as PlaceGrade, a as PlaceGradeDimensions, m as PlaceHourBlock, c as PlaceLinkKind, n as PlaceMustTry, P as PlacePreview, o as PlacePriceRange, p as PlaceReviewsDigest, f as PlaceSchedule, g as PlaceScheduleWeekLabels, W as WeekdayKey } from '../place-types-C4rR3LTs.js';
2
2
  import 'react/jsx-runtime';
3
3
  import '../lib/grade.js';
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { QaItem } from './menu-types.js';
3
3
  import 'react';
4
- import '../place-types-BHdeMMiG.js';
4
+ import '../place-types-C4rR3LTs.js';
5
5
  import '../lib/grade.js';
6
6
 
7
7
  /**
@@ -4,11 +4,11 @@ import { VariantProps } from 'class-variance-authority';
4
4
 
5
5
  declare const spinnerVariants: (props?: ({
6
6
  size?: "default" | "xs" | "sm" | "lg" | "xl" | null | undefined;
7
- tone?: "inherit" | "default" | "destructive" | "primary" | null | undefined;
7
+ tone?: "default" | "destructive" | "inherit" | "primary" | null | undefined;
8
8
  } & class_variance_authority_types.ClassProp) | undefined) => string;
9
9
  declare const Spinner: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLSpanElement> & VariantProps<(props?: ({
10
10
  size?: "default" | "xs" | "sm" | "lg" | "xl" | null | undefined;
11
- tone?: "inherit" | "default" | "destructive" | "primary" | null | undefined;
11
+ tone?: "default" | "destructive" | "inherit" | "primary" | null | undefined;
12
12
  } & class_variance_authority_types.ClassProp) | undefined) => string> & {
13
13
  /** Visually-hidden label for screen readers. Defaults to "Loading". */
14
14
  label?: string;
@@ -3,11 +3,11 @@ import * as React from 'react';
3
3
  import { VariantProps } from 'class-variance-authority';
4
4
 
5
5
  declare const tagVariants: (props?: ({
6
- variant?: "default" | "destructive" | "outline" | "warning" | "success" | "primary" | "quiet" | null | undefined;
6
+ variant?: "default" | "destructive" | "outline" | "success" | "warning" | "primary" | "quiet" | null | undefined;
7
7
  size?: "default" | "sm" | "lg" | null | undefined;
8
8
  } & class_variance_authority_types.ClassProp) | undefined) => string;
9
9
  declare const Tag: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLSpanElement> & VariantProps<(props?: ({
10
- variant?: "default" | "destructive" | "outline" | "warning" | "success" | "primary" | "quiet" | null | undefined;
10
+ variant?: "default" | "destructive" | "outline" | "success" | "warning" | "primary" | "quiet" | null | undefined;
11
11
  size?: "default" | "sm" | "lg" | null | undefined;
12
12
  } & class_variance_authority_types.ClassProp) | undefined) => string> & {
13
13
  onRemove?: () => void;
@@ -22,9 +22,9 @@ import { VariantProps } from 'class-variance-authority';
22
22
  * Weights are 400 / 600 / 700. 500 is not in the system.
23
23
  */
24
24
  declare const textVariants: (props?: ({
25
- variant?: "h2" | "h3" | "label" | "body" | "caption" | "h1" | "h4" | "small" | "display" | "body-lg" | "body-sm" | "overline" | null | undefined;
26
- color?: "inherit" | "default" | "destructive" | "muted" | "primary" | "primary-foreground" | null | undefined;
27
- weight?: "bold" | "medium" | "normal" | "semibold" | null | undefined;
25
+ variant?: "small" | "display" | "h2" | "h3" | "label" | "body" | "caption" | "h1" | "h4" | "body-lg" | "body-sm" | "overline" | null | undefined;
26
+ color?: "default" | "destructive" | "inherit" | "muted" | "primary" | "primary-foreground" | null | undefined;
27
+ weight?: "medium" | "bold" | "normal" | "semibold" | null | undefined;
28
28
  leading?: "none" | "normal" | "tight" | "snug" | "relaxed" | "loose" | null | undefined;
29
29
  tracking?: "normal" | "tight" | "wide" | "wider" | null | undefined;
30
30
  wrap?: "balance" | "pretty" | "nowrap" | null | undefined;
@@ -2,7 +2,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { MenuLinkComponent } from './menu-types.js';
3
3
  import { TopComparisonRow } from './top-types.js';
4
4
  import 'react';
5
- import '../place-types-BHdeMMiG.js';
5
+ import '../place-types-C4rR3LTs.js';
6
6
  import '../lib/grade.js';
7
7
 
8
8
  type TopComparisonLabels = {
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { MenuLinkComponent } from './menu-types.js';
3
3
  import 'react';
4
- import '../place-types-BHdeMMiG.js';
4
+ import '../place-types-C4rR3LTs.js';
5
5
  import '../lib/grade.js';
6
6
 
7
7
  type TopCrossLinkProps = {
@@ -3,7 +3,7 @@ import { MenuItemLabels } from './menu-item.js';
3
3
  import { MenuItemPreview, MenuLinkComponent } from './menu-types.js';
4
4
  import { TopRankEntry } from './top-types.js';
5
5
  import 'react';
6
- import '../place-types-BHdeMMiG.js';
6
+ import '../place-types-C4rR3LTs.js';
7
7
  import '../lib/grade.js';
8
8
 
9
9
  type TopRankingEntryLabels = {
@@ -2,7 +2,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { MenuLinkComponent } from './menu-types.js';
3
3
  import { TopExit } from './top-types.js';
4
4
  import 'react';
5
- import '../place-types-BHdeMMiG.js';
5
+ import '../place-types-C4rR3LTs.js';
6
6
  import '../lib/grade.js';
7
7
 
8
8
  type TopRankingHeroProps = {
@@ -1,6 +1,6 @@
1
1
  import { ReactNode } from 'react';
2
2
  import { MenuItemPreview } from './menu-types.js';
3
- import '../place-types-BHdeMMiG.js';
3
+ import '../place-types-C4rR3LTs.js';
4
4
  import 'react/jsx-runtime';
5
5
  import '../lib/grade.js';
6
6
 
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { a as PlaceGradeDimensions } from '../place-types-BHdeMMiG.js';
2
+ import { a as PlaceGradeDimensions } from '../place-types-C4rR3LTs.js';
3
3
  import '../lib/grade.js';
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { C as CitedText } from '../place-types-BHdeMMiG.js';
2
+ import { C as CitedText } from '../place-types-C4rR3LTs.js';
3
3
  import '../lib/grade.js';
4
4
 
5
5
  /**
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { MenuLinkComponent, MenuLinkProps } from './menu-types.js';
3
- import { P as PlacePreview } from '../place-types-BHdeMMiG.js';
3
+ import { P as PlacePreview } from '../place-types-C4rR3LTs.js';
4
4
  import 'react';
5
5
  import '../lib/grade.js';
6
6
 
@@ -12,6 +12,13 @@ type GradeAtomLabels = {
12
12
  aria: (value: string) => string;
13
13
  /** Title + aria suffix when `grade.capped`, e.g. "Capped for low confidence". */
14
14
  capped: string;
15
+ /**
16
+ * The number as the locale writes it, e.g. `8,7` in Romanian. Receives the
17
+ * grade already rounded to one decimal; return the glyphs to print. Defaults
18
+ * to `formatGrade` (`8.7`, a trailing `.0` dropped). It is a label because the
19
+ * decimal mark is translation, not styling — and `aria` gets the same string.
20
+ */
21
+ value?: (overall: number) => string;
15
22
  };
16
23
  type GradeAtomProps = {
17
24
  grade: PlaceGrade;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bearmenu/ui",
3
- "version": "0.8.21",
3
+ "version": "0.8.23",
4
4
  "description": "BearMenu design system — shared UI tokens, primitives, and components",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -12,7 +12,7 @@ const meta: Meta<typeof CoachMarks> = {
12
12
  docs: {
13
13
  description: {
14
14
  component:
15
- "A guided tour drawn over the real page: the page dims, the current step's target is cut out in a spotlight with its step number on the corner, one callout says what the target is. Targets are `data-coach-mark` attributes on the page's own elements.",
15
+ "A guided tour drawn over the real page: the page dims, the current step's target is cut out in a spotlight with its step number on the corner, one callout says what the target is. Targets are `data-coach-mark` attributes on the page's own elements. The spotlight is a hole in the dimming: a tap on the ringed control reaches it and ends the tour. A `region` stop (a list, a rail) is cut out without the ring; a pinned control passes `scroll: false`; `inset` names the page's fixed chrome.",
16
16
  },
17
17
  },
18
18
  },
@@ -37,14 +37,23 @@ const STOPS: CoachMarkStop[] = [
37
37
  title: "Cards, or rows",
38
38
  body: "The same menu items either way: rows fit three times as many.",
39
39
  },
40
+ {
41
+ id: "results",
42
+ kind: "region",
43
+ title: "What matched",
44
+ body: "A region is cut out without the ring; tapping a row inside it navigates, and the tour steps out of the way.",
45
+ },
40
46
  ];
41
47
 
42
48
  function Demo() {
43
49
  const [open, setOpen] = useState(true);
50
+ const [taps, setTaps] = useState(0);
44
51
  return (
45
52
  <div className="min-h-[720px] bg-background p-8 text-foreground">
46
53
  <div className="flex items-center gap-3">
47
- <Button data-coach-mark="search">Search and filter</Button>
54
+ <Button data-coach-mark="search" onClick={() => setTaps((count) => count + 1)}>
55
+ Search and filter{taps > 0 ? ` · tapped ${taps}` : ""}
56
+ </Button>
48
57
  <Button variant="outline" data-coach-mark="view">
49
58
  Rows
50
59
  </Button>
@@ -57,9 +66,16 @@ function Demo() {
57
66
  <span>Centru ×</span>
58
67
  </div>
59
68
  <p className="mt-10 max-w-[60ch] text-sm text-muted-foreground">
60
- The page beneath the tour. Nothing here re-renders; the overlay measures the three targets
61
- and draws over them.
69
+ The page beneath the tour. Nothing here re-renders; the overlay measures the targets and
70
+ draws over them. Tap the spotlit button: the tap lands, and the tour ends.
62
71
  </p>
72
+ <ul data-coach-mark="results" className="mt-6 max-w-md divide-y divide-border rounded-2xl border border-border">
73
+ {["Papanași", "Ciorbă de burtă", "Sarmale"].map((name) => (
74
+ <li key={name} className="px-4 py-3 text-sm">
75
+ {name}
76
+ </li>
77
+ ))}
78
+ </ul>
63
79
  <CoachMarks
64
80
  open={open}
65
81
  stops={STOPS}
@@ -1,4 +1,5 @@
1
- import { describe, expect, it, vi } from "vitest";
1
+ import { useState } from "react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
3
  import { fireEvent, render, screen } from "@testing-library/react";
3
4
  import { CoachMarks, type CoachMarkStop } from "./coach-marks";
4
5
 
@@ -55,8 +56,10 @@ function pins(): string[] {
55
56
  }
56
57
 
57
58
  // jsdom has no layout: every rect is zero, which is enough to render. A
58
- // target has boxes when it is in the document, none when it is not.
59
- Element.prototype.scrollIntoView = vi.fn();
59
+ // target has boxes when it is in the document, none when it is not. The
60
+ // scroll mock sits on HTMLElement, where the test setup's stub is.
61
+ const scrollIntoView = vi.fn();
62
+ HTMLElement.prototype.scrollIntoView = scrollIntoView;
60
63
  Element.prototype.getClientRects = function () {
61
64
  return (this.isConnected ? [{}] : []) as unknown as DOMRectList;
62
65
  };
@@ -121,13 +124,185 @@ describe("CoachMarks", () => {
121
124
  expect(onClose).not.toHaveBeenCalled();
122
125
  });
123
126
 
124
- it("Skip, Escape and the dimmed page all close", () => {
127
+ it("Skip and Escape close; the dimmed page does not", () => {
125
128
  const onClose = vi.fn();
126
129
  render(<Page onClose={onClose} />);
127
130
  fireEvent.click(screen.getByText("Skip"));
128
131
  fireEvent.keyDown(window, { key: "Escape" });
129
- fireEvent.click(screen.getAllByRole("button", { name: "Skip" })[0]);
130
- expect(onClose).toHaveBeenCalledTimes(3);
132
+ expect(onClose).toHaveBeenCalledTimes(2);
133
+ expect(screen.getAllByRole("button", { name: "Skip" })).toHaveLength(1);
134
+ const dim = screen.getByRole("dialog").firstElementChild;
135
+ fireEvent.click(dim as Element);
136
+ expect(onClose).toHaveBeenCalledTimes(2);
137
+ });
138
+
139
+ it("a tap on the spotlit target reaches it and ends the tour", () => {
140
+ const onClose = vi.fn();
141
+ const onTarget = vi.fn();
142
+ render(
143
+ <>
144
+ <button type="button" data-coach-mark="a" onClick={onTarget}>
145
+ a
146
+ </button>
147
+ <button type="button" data-coach-mark="b">
148
+ b
149
+ </button>
150
+ <CoachMarks open stops={STOPS.slice(0, 2)} onClose={onClose} labels={labels} />
151
+ </>
152
+ );
153
+ fireEvent.click(screen.getByRole("button", { name: "b" }));
154
+ expect(onClose).not.toHaveBeenCalled();
155
+ fireEvent.click(screen.getByRole("button", { name: "a" }));
156
+ expect(onTarget).toHaveBeenCalledTimes(1);
157
+ expect(onClose).toHaveBeenCalledTimes(1);
158
+ });
159
+
160
+ it("hears a tap on a target that stops propagation", () => {
161
+ const onClose = vi.fn();
162
+ render(
163
+ <>
164
+ <div data-coach-mark="a">
165
+ <button type="button" onClick={(event) => event.stopPropagation()}>
166
+ inner
167
+ </button>
168
+ </div>
169
+ <CoachMarks open stops={STOPS.slice(0, 1)} onClose={onClose} labels={labels} />
170
+ </>
171
+ );
172
+ fireEvent.click(screen.getByRole("button", { name: "inner" }));
173
+ expect(onClose).toHaveBeenCalledTimes(1);
174
+ });
175
+
176
+ it("settles when the consumer rebuilds its stops and inset on every render", () => {
177
+ const onProgress = vi.fn();
178
+ function Consumer() {
179
+ const [progress, setProgress] = useState<string>("");
180
+ return (
181
+ <>
182
+ <button type="button" data-coach-mark="a">
183
+ a
184
+ </button>
185
+ <span data-testid="progress">{progress}</span>
186
+ <CoachMarks
187
+ open
188
+ stops={STOPS.slice(0, 1).map((stop) => ({ ...stop }))}
189
+ inset={{ top: 68 }}
190
+ onClose={vi.fn()}
191
+ onProgress={(step, total, stop) => {
192
+ onProgress(step, total, stop);
193
+ setProgress(`${step}/${total}`);
194
+ }}
195
+ labels={labels}
196
+ />
197
+ </>
198
+ );
199
+ }
200
+ render(<Consumer />);
201
+ expect(screen.getByTestId("progress")).toHaveTextContent("1/1");
202
+ expect(onProgress).toHaveBeenCalledTimes(1);
203
+ });
204
+
205
+ it("a region is cut out without the control's ring", () => {
206
+ const { rerender } = render(<Page onClose={vi.fn()} />);
207
+ const spotlight = () => screen.getByRole("dialog").children[1];
208
+ expect(spotlight().className).toContain("ring-2");
209
+ rerender(<Page onClose={vi.fn()} stops={STOPS.map((stop) => ({ ...stop, kind: "region" }))} />);
210
+ expect(spotlight().className).not.toContain("ring-2");
211
+ });
212
+
213
+ describe("scrolling to the target", () => {
214
+ /** A page whose targets sit at `top`, `height` 40, in an 800px viewport. */
215
+ function placeTargets(top: number) {
216
+ vi.spyOn(window, "innerHeight", "get").mockReturnValue(800);
217
+ vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockReturnValue({
218
+ top,
219
+ bottom: top + 40,
220
+ left: 0,
221
+ right: 100,
222
+ width: 100,
223
+ height: 40,
224
+ x: 0,
225
+ y: top,
226
+ toJSON: () => ({}),
227
+ });
228
+ }
229
+
230
+ beforeEach(() => scrollIntoView.mockClear());
231
+ afterEach(() => vi.restoreAllMocks());
232
+
233
+ it("centres a target out of view", () => {
234
+ placeTargets(900);
235
+ render(<Page onClose={vi.fn()} />);
236
+ expect(scrollIntoView).toHaveBeenCalledWith(expect.objectContaining({ block: "center" }));
237
+ });
238
+
239
+ it("leaves the page where it is when the target is already in view", () => {
240
+ placeTargets(300);
241
+ render(<Page onClose={vi.fn()} />);
242
+ // Still a request — it aborts the previous stop's smooth scroll — but
243
+ // one that moves nothing.
244
+ expect(scrollIntoView).toHaveBeenCalledWith(expect.objectContaining({ block: "nearest" }));
245
+ });
246
+
247
+ it("aligns a target taller than the band to its top, clear of the chrome", () => {
248
+ vi.spyOn(window, "innerHeight", "get").mockReturnValue(800);
249
+ vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockReturnValue({
250
+ top: 900,
251
+ bottom: 2400,
252
+ left: 0,
253
+ right: 100,
254
+ width: 100,
255
+ height: 1500,
256
+ x: 0,
257
+ y: 900,
258
+ toJSON: () => ({}),
259
+ });
260
+ render(
261
+ <>
262
+ <button type="button" data-coach-mark="a">
263
+ a
264
+ </button>
265
+ <CoachMarks
266
+ open
267
+ stops={STOPS.slice(0, 1)}
268
+ onClose={vi.fn()}
269
+ labels={labels}
270
+ inset={{ top: 68 }}
271
+ />
272
+ </>
273
+ );
274
+ expect(scrollIntoView).toHaveBeenCalledWith(expect.objectContaining({ block: "start" }));
275
+ expect(screen.getByRole("button", { name: "a" }).style.scrollMarginTop).toBe("76px");
276
+ });
277
+
278
+ it("counts the fixed chrome as out of view", () => {
279
+ placeTargets(20);
280
+ const { unmount } = render(<Page onClose={vi.fn()} />);
281
+ expect(scrollIntoView).toHaveBeenCalledWith(expect.objectContaining({ block: "nearest" }));
282
+ unmount();
283
+ scrollIntoView.mockClear();
284
+ render(
285
+ <>
286
+ <button type="button" data-coach-mark="a">
287
+ a
288
+ </button>
289
+ <CoachMarks
290
+ open
291
+ stops={STOPS.slice(0, 1)}
292
+ onClose={vi.fn()}
293
+ labels={labels}
294
+ inset={{ top: 68 }}
295
+ />
296
+ </>
297
+ );
298
+ expect(scrollIntoView).toHaveBeenCalledWith(expect.objectContaining({ block: "center" }));
299
+ });
300
+
301
+ it("never scrolls a stop that opted out — a pinned control", () => {
302
+ placeTargets(900);
303
+ render(<Page onClose={vi.fn()} stops={STOPS.map((stop) => ({ ...stop, scroll: false }))} />);
304
+ expect(scrollIntoView).not.toHaveBeenCalled();
305
+ });
131
306
  });
132
307
 
133
308
  it("closes at once when no stop has a target, and renders nothing while closed", () => {