Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/core/ui/rich-caption-toolbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,12 @@ export class RichCaptionToolbar extends RichTextToolbar {
}

protected override createSpacingPanel(): SpacingPanel {
return new SpacingPanel({ showWordSpacing: true });
// No word-spacing control: RichCaptionStyle deliberately excludes wordSpacing
return new SpacingPanel();
}

protected override supportsWordSpacing(): boolean {
return false;
}

protected override createFontColorPicker(): FontColorPicker {
Expand Down
20 changes: 17 additions & 3 deletions src/core/ui/rich-text-toolbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ export class RichTextToolbar extends BaseToolbar {
style: {
...(asset.style || {}),
letterSpacing: state.letterSpacing,
wordSpacing: state.wordSpacing,
...(this.supportsWordSpacing() ? { wordSpacing: state.wordSpacing } : {}),
lineHeight: state.lineHeight
}
};
Expand All @@ -384,7 +384,11 @@ export class RichTextToolbar extends BaseToolbar {
} else {
// Discrete update (creates command)
this.updateClipProperty({
style: { letterSpacing: state.letterSpacing, wordSpacing: state.wordSpacing, lineHeight: state.lineHeight }
style: {
letterSpacing: state.letterSpacing,
...(this.supportsWordSpacing() ? { wordSpacing: state.wordSpacing } : {}),
lineHeight: state.lineHeight
}
});
}
});
Expand All @@ -404,7 +408,9 @@ export class RichTextToolbar extends BaseToolbar {
const finalClip = structuredClone(session.initialState);
if (finalClip.asset && (finalClip.asset.type === "rich-text" || finalClip.asset.type === "rich-caption") && finalClip.asset.style) {
finalClip.asset.style.letterSpacing = finalState.letterSpacing;
(finalClip.asset.style as Record<string, unknown>)["wordSpacing"] = finalState.wordSpacing;
if (this.supportsWordSpacing()) {
(finalClip.asset.style as Record<string, unknown>)["wordSpacing"] = finalState.wordSpacing;
}
finalClip.asset.style.lineHeight = finalState.lineHeight;
}

Expand Down Expand Up @@ -943,6 +949,14 @@ export class RichTextToolbar extends BaseToolbar {
return new SpacingPanel();
}

/**
* Whether this toolbar's asset type accepts `style.wordSpacing`. Rich-caption
* does not — its schema rejects the key, so it must never be written.
*/
protected supportsWordSpacing(): boolean {
return true;
}

protected createFontColorPicker(): FontColorPicker {
return new FontColorPicker();
}
Expand Down
68 changes: 68 additions & 0 deletions tests/rich-caption-toolbar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -712,3 +712,71 @@ describe("RichCaptionToolbar", () => {
});
});
});

// ============================================================================
// Word spacing exclusion (RichCaptionStyle rejects the key — never render or write it)
// ============================================================================

describe("RichCaptionToolbar spacing controls", () => {
let toolbar: InstanceType<typeof import("../src/core/ui/rich-caption-toolbar").RichCaptionToolbar>;
let mockEdit: ReturnType<typeof createMockEdit>;
let container: HTMLDivElement;

beforeEach(async () => {
mockEdit = createMockEdit();
const { RichCaptionToolbar } = await import("../src/core/ui/rich-caption-toolbar");
toolbar = new RichCaptionToolbar(mockEdit as never);
container = createTestContainer();
setupCaptionClip(mockEdit);
toolbar.mount(container);
});

afterEach(() => {
toolbar.dispose();
cleanupTestContainer(container);
});

it("does not render a word-spacing slider", () => {
expect(container.querySelector("[data-word-spacing-slider]")).toBeNull();
expect(container.querySelector("[data-letter-spacing-slider]")).not.toBeNull();
});

it("omits wordSpacing from spacing updates", () => {
const slider = container.querySelector("[data-letter-spacing-slider]") as HTMLInputElement;
expect(slider).not.toBeNull();
simulateInput(slider, 5);

expect(mockEdit.updateClip).toHaveBeenCalled();
const lastCall = mockEdit.updateClip.mock.calls.at(-1) as unknown[];
const updates = lastCall[2] as { asset: { style: Record<string, unknown> } };
expect(updates.asset.style["letterSpacing"]).toBe(5);
expect(updates.asset.style).not.toHaveProperty("wordSpacing");
});
});

describe("RichTextToolbar spacing controls (contrast)", () => {
it("includes wordSpacing in spacing updates for rich-text", async () => {
const mockEdit = createMockEdit();
const { RichTextToolbar } = await import("../src/core/ui/rich-text-toolbar");
const toolbar = new RichTextToolbar(mockEdit as never);
const container = createTestContainer();

const asset = { type: "rich-text", text: "Hello", font: { family: "Open Sans", size: 48 } };
const clip = { asset, start: 0, length: 5 };
mockEdit.getPlayerClip.mockReturnValue({ clipConfiguration: clip, getMergeFieldBinding: jest.fn(() => null) } as never);
mockEdit.getResolvedClip.mockReturnValue(clip as never);
toolbar.mount(container);

const slider = container.querySelector("[data-letter-spacing-slider]") as HTMLInputElement;
expect(slider).not.toBeNull();
simulateInput(slider, 5);

expect(mockEdit.updateClip).toHaveBeenCalled();
const lastCall = mockEdit.updateClip.mock.calls.at(-1) as unknown[];
const updates = lastCall[2] as { asset: { style: Record<string, unknown> } };
expect(updates.asset.style).toHaveProperty("wordSpacing");

toolbar.dispose();
cleanupTestContainer(container);
});
});
Loading