From 680f5d7465e6328ed6c238bd70071ec277fa9a59 Mon Sep 17 00:00:00 2001 From: Akshar Patel <123344143+AksharP5@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:29:08 +0000 Subject: [PATCH] fix(player): keep Next available for the final slide's reveals (#6233) --- docs/guides/slideshow.mdx | 3 ++ .../src/slideshow/SlideshowController.ts | 3 +- .../slideshow/hyperframes-slideshow.test.ts | 40 +++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/docs/guides/slideshow.mdx b/docs/guides/slideshow.mdx index b8655bb6e5..70675b933a 100644 --- a/docs/guides/slideshow.mdx +++ b/docs/guides/slideshow.mdx @@ -63,6 +63,9 @@ Attach the source notes, document, page, or existing deck. Name the audience and The main story should still make sense when no optional branch is opened. +Use **Next** to step through fragment reveals, including those on the final slide. +The Next button disappears once the final slide's last reveal is shown. + ## Review it as a presenter - Each slide makes one clear point. diff --git a/packages/player/src/slideshow/SlideshowController.ts b/packages/player/src/slideshow/SlideshowController.ts index 006be722d8..84146a405a 100644 --- a/packages/player/src/slideshow/SlideshowController.ts +++ b/packages/player/src/slideshow/SlideshowController.ts @@ -71,7 +71,8 @@ export class SlideshowController { } get canNext(): boolean { - // next has a destination: a later slide in this sequence, OR (in a branch) the parent. + const slide = this.currentSlide; + if (slide && this.frame.fragmentIndex + 1 < slide.fragments.length) return true; const slides = this.slidesOf(this.frame.sequenceId); return this.frame.slideIndex + 1 < slides.length || this.stack.length > 1; } diff --git a/packages/player/src/slideshow/hyperframes-slideshow.test.ts b/packages/player/src/slideshow/hyperframes-slideshow.test.ts index 0ed739a699..19d3192462 100644 --- a/packages/player/src/slideshow/hyperframes-slideshow.test.ts +++ b/packages/player/src/slideshow/hyperframes-slideshow.test.ts @@ -4,8 +4,10 @@ import { handleRuntimeMessage } from "../runtime-message-handler.js"; import { childrenMayStillArrive, dropInvalidSlides, + HyperframesSlideshow, locateSlideshowParts, } from "./hyperframes-slideshow.js"; +import { SlideshowController } from "./SlideshowController.js"; import { slideshowChannelName } from "./slideshowPresenter.js"; // Dynamic import defers custom-element registration until happy-dom is active. @@ -131,6 +133,44 @@ describe("", () => { el.remove(); }); + it("keeps Next available until the final slide's fragments are revealed", () => { + const player = { + currentTime: 0, + seek: (time: number) => { + player.currentTime = time; + }, + play: vi.fn(), + pause: vi.fn(), + onTimeUpdate: () => () => {}, + }; + const controller = new SlideshowController(player, { + slides: [ + { sceneId: "intro", start: 0, end: 5, fragments: [], hotspots: [] }, + { sceneId: "closing", start: 5, end: 10, fragments: [6, 8, 9], hotspots: [] }, + ], + sequences: {}, + }); + const el = new HyperframesSlideshow(); + document.body.appendChild(el); + el.__setControllerForTest(controller); + + try { + controller.goToSlide(1); + expect(player.currentTime).toBe(6); + expect(el.querySelector("[data-hf-next]")).not.toBeNull(); + + el.querySelector("[data-hf-next]")?.click(); + expect(player.currentTime).toBe(8); + expect(el.querySelector("[data-hf-next]")).not.toBeNull(); + + el.querySelector("[data-hf-next]")?.click(); + expect(player.currentTime).toBe(9); + expect(el.querySelector("[data-hf-next]")).toBeNull(); + } finally { + el.remove(); + } + }); + it("renders an icon-only present button inside the nav cluster in normal mode", () => { const el = makeEl({ index: 1, total: 3 }); const cluster = el.querySelector("[data-hf-nav-cluster]");