diff --git a/.changeset/fix-sequence-messagealign-rtl-arrows.md b/.changeset/fix-sequence-messagealign-rtl-arrows.md new file mode 100644 index 00000000000..b540d3d72f0 --- /dev/null +++ b/.changeset/fix-sequence-messagealign-rtl-arrows.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: correct messageAlign label position for right-to-left arrows in sequence diagrams diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.spec.js b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.spec.js new file mode 100644 index 00000000000..99faf6fc252 --- /dev/null +++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.spec.js @@ -0,0 +1,96 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { SequenceDB } from './sequenceDb.js'; + +vi.mock('./svgDraw.js', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + drawText: vi.fn(), + }; +}); + +vi.mock('../../utils.js', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + default: { + ...actual.default, + calculateTextDimensions: vi.fn(() => ({ width: 40, height: 14 })), + }, + }; +}); + +import * as svgDraw from './svgDraw.js'; +import { drawMessage, setConf } from './sequenceRenderer.js'; + +function mockDiagram(name = 'svg') { + const children = []; + const elem = { + get __children() { + return children; + }, + __name: name, + append(n) { + const child = mockDiagram(n); + children.push(child); + return child; + }, + lower: vi.fn(() => elem), + attr: vi.fn(() => elem), + style: vi.fn(() => elem), + text: vi.fn(() => elem), + }; + return elem; +} + +describe('drawMessage (#3594)', () => { + beforeEach(() => { + setConf({ + messageFontFamily: 'sans-serif', + messageFontSize: 14, + messageFontWeight: '400', + messageAlign: 'left', + wrapPadding: 10, + arrowMarkerAbsolute: false, + showSequenceNumbers: false, + }); + vi.mocked(svgDraw.drawText).mockClear(); + }); + + it('passes min(startx, stopx) and abs(stopx - startx) to drawText when startx > stopx', async () => { + const diagram = mockDiagram(); + const startx = 320; + const stopx = 80; + const sequenceDb = new SequenceDB(); + const diagObj = { db: sequenceDb }; + + const msgModel = { + startx, + stopx, + starty: 40, + stopy: 90, + message: 'RTL label', + type: sequenceDb.LINETYPE.SOLID, + sequenceIndex: 1, + sequenceVisible: false, + id: '0', + from: 'Bob', + to: 'Alice', + fromBounds: startx - 20, + toBounds: stopx + 20, + }; + + const msg = { type: sequenceDb.LINETYPE.SOLID, centralConnection: 0 }; + + await drawMessage(diagram, msgModel, 100, diagObj, msg, 'test-id'); + + expect(svgDraw.drawText).toHaveBeenCalled(); + const messageTextCalls = vi + .mocked(svgDraw.drawText) + .mock.calls.filter((call) => call[1]?.class === 'messageText'); + expect(messageTextCalls).toHaveLength(1); + const textObj = messageTextCalls[0][1]; + expect(textObj.x).toBe(Math.min(startx, stopx)); + expect(textObj.width).toBe(Math.abs(stopx - startx)); + }); +}); diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts index bdd650c4015..eaad1244553 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts @@ -457,7 +457,7 @@ async function boundMessage(_diagram, msgModel): Promise { * @param lineStartY - The Y coordinate at which the message line starts * @param diagObj - The diagram object. */ -const drawMessage = async function ( +export const drawMessage = async function ( diagram, msgModel, lineStartY: number, @@ -468,9 +468,9 @@ const drawMessage = async function ( const { startx, stopx, starty, message, type, sequenceIndex, sequenceVisible } = msgModel; const textDims = utils.calculateTextDimensions(message, messageFont(conf)); const textObj = svgDrawCommon.getTextObj(); - textObj.x = startx; + textObj.x = Math.min(startx, stopx); textObj.y = starty + 10; - textObj.width = stopx - startx; + textObj.width = Math.abs(stopx - startx); textObj.class = 'messageText'; textObj.dy = '1em'; textObj.text = message; diff --git a/packages/mermaid/src/diagrams/sequence/svgDraw.spec.js b/packages/mermaid/src/diagrams/sequence/svgDraw.spec.js index 490852b5c15..3b39e2a2c01 100644 --- a/packages/mermaid/src/diagrams/sequence/svgDraw.spec.js +++ b/packages/mermaid/src/diagrams/sequence/svgDraw.spec.js @@ -164,6 +164,57 @@ describe('svgDraw', function () { expect(text3.attr).toHaveBeenCalledWith('y', 10); expect(text3.text).toHaveBeenCalledWith('fine lines'); }); + describe('messageAlign anchor positioning', function () { + const x = 100; + const width = 200; + const textMargin = 10; + + it('anchor "left" positions text at x + textMargin', function () { + const svg = MockD3('svg'); + svgDraw.drawText(svg, { + x, + y: 20, + text: 'hello', + width, + anchor: 'left', + textMargin, + }); + const text = svg.__children[0]; + expect(text.attr).toHaveBeenCalledWith('x', x + textMargin); + expect(text.attr).toHaveBeenCalledWith('text-anchor', 'start'); + }); + + it('anchor "center" positions text at x + width/2', function () { + const svg = MockD3('svg'); + svgDraw.drawText(svg, { + x, + y: 20, + text: 'hello', + width, + anchor: 'center', + textMargin, + }); + const text = svg.__children[0]; + expect(text.attr).toHaveBeenCalledWith('x', x + width / 2); + expect(text.attr).toHaveBeenCalledWith('text-anchor', 'middle'); + }); + + it('anchor "right" positions text at x + width - textMargin', function () { + const svg = MockD3('svg'); + svgDraw.drawText(svg, { + x, + y: 20, + text: 'hello', + width, + anchor: 'right', + textMargin, + }); + const text = svg.__children[0]; + expect(text.attr).toHaveBeenCalledWith('x', x + width - textMargin); + expect(text.attr).toHaveBeenCalledWith('text-anchor', 'end'); + }); + }); + it('should work with numeral font sizes', function () { const svg = MockD3('svg'); svgDraw.drawText(svg, {