diff --git a/src/core/ui/composites/TransitionPanel.ts b/src/core/ui/composites/TransitionPanel.ts index be0821b8..75825489 100644 --- a/src/core/ui/composites/TransitionPanel.ts +++ b/src/core/ui/composites/TransitionPanel.ts @@ -198,6 +198,11 @@ export class TransitionPanel extends UIComponent { } private stepSpeed(direction: number): void { + const effect = this.state.tab === "in" ? this.state.inEffect : this.state.outEffect; + if (!this.supportsSpeedVariants(effect)) { + return; + } + const speeds = TransitionPanel.SPEEDS; const currentSpeed = this.state.tab === "in" ? this.state.inSpeed : this.state.outSpeed; @@ -224,6 +229,14 @@ export class TransitionPanel extends UIComponent { return ["slide", "wipe", "carousel"].includes(effect); } + /** + * Whether the Shotstack transition API exposes Slow/Fast variants for this effect. + * `zoom` is fixed-speed (only `zoom` is valid — not `zoomSlow`/`zoomFast`). + */ + private supportsSpeedVariants(effect: string): boolean { + return Boolean(effect) && effect !== "zoom"; + } + private updateUI(): void { const { tab } = this.state; const effect = tab === "in" ? this.state.inEffect : this.state.outEffect; @@ -257,10 +270,11 @@ export class TransitionPanel extends UIComponent { this.speedLabel.textContent = `${speed.toFixed(2)}s`; } - // Update stepper states + // Update stepper states — disable when the effect has no Slow/Fast API variants + const canAdjustSpeed = this.supportsSpeedVariants(effect); const speedIdx = TransitionPanel.SPEEDS.indexOf(speed); - if (this.speedDecreaseBtn) this.speedDecreaseBtn.disabled = speedIdx <= 0; - if (this.speedIncreaseBtn) this.speedIncreaseBtn.disabled = speedIdx >= TransitionPanel.SPEEDS.length - 1; + if (this.speedDecreaseBtn) this.speedDecreaseBtn.disabled = !canAdjustSpeed || speedIdx <= 0; + if (this.speedIncreaseBtn) this.speedIncreaseBtn.disabled = !canAdjustSpeed || speedIdx >= TransitionPanel.SPEEDS.length - 1; } // ─── Transition Value Parsing/Building ─────────────────────────────────── @@ -304,6 +318,10 @@ export class TransitionPanel extends UIComponent { } private speedToSuffix(speed: number, effect: string): string { + if (!this.supportsSpeedVariants(effect)) { + return ""; + } + const isSlideOrCarousel = effect === "slide" || effect === "carousel"; if (isSlideOrCarousel) { diff --git a/tests/transition-panel.test.ts b/tests/transition-panel.test.ts new file mode 100644 index 00000000..7b9a2054 --- /dev/null +++ b/tests/transition-panel.test.ts @@ -0,0 +1,69 @@ +/** + * @jest-environment jsdom + */ +import { TransitionPanel } from "../src/core/ui/composites/TransitionPanel"; + +function createTestContainer(): HTMLDivElement { + const container = document.createElement("div"); + document.body.appendChild(container); + return container; +} + +function cleanupTestContainer(container: HTMLDivElement): void { + container.remove(); +} + +function simulateClick(element: Element | null): void { + if (element) { + element.dispatchEvent(new MouseEvent("click", { bubbles: true })); + } +} + +describe("TransitionPanel", () => { + let panel: TransitionPanel; + let container: HTMLDivElement; + + beforeEach(() => { + panel = new TransitionPanel(); + container = createTestContainer(); + panel.mount(container); + }); + + afterEach(() => { + panel.dispose(); + cleanupTestContainer(container); + }); + + describe("zoom transition speed", () => { + it("does not emit zoomSlow or zoomFast when the speed stepper is used", () => { + simulateClick(container.querySelector('[data-effect="zoom"]')); + expect(panel.getClipValue()?.in).toBe("zoom"); + + const increaseBtn = container.querySelector("[data-speed-increase]") as HTMLButtonElement; + const decreaseBtn = container.querySelector("[data-speed-decrease]") as HTMLButtonElement; + expect(increaseBtn.disabled).toBe(true); + expect(decreaseBtn.disabled).toBe(true); + + simulateClick(increaseBtn); + simulateClick(decreaseBtn); + + expect(panel.getClipValue()?.in).toBe("zoom"); + }); + + it("still allows Slow/Fast variants for fade", () => { + simulateClick(container.querySelector('[data-effect="fade"]')); + expect(panel.getClipValue()?.in).toBe("fade"); + + const increaseBtn = container.querySelector("[data-speed-increase]") as HTMLButtonElement; + expect(increaseBtn.disabled).toBe(false); + + simulateClick(increaseBtn); + expect(panel.getClipValue()?.in).toBe("fadeSlow"); + }); + + it("strips invalid Slow/Fast suffixes when rebuilding zoom from clip state", () => { + panel.setFromClip({ in: "zoomSlow" }); + expect(panel.getClipValue()?.in).toBe("zoom"); + }); + }); +});