From 8a138a035070b2b213308f34b3fc7275710637cc Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Sat, 22 Aug 2026 10:28:35 -0700 Subject: [PATCH 01/10] fix(composer): wrap Buzz chip labels with their icons Co-authored-by: Carl Signed-off-by: Taylor Ho --- .../lib/composerMessageLinkNode.test.mjs | 16 +++++-- .../messages/lib/composerMessageLinkNode.ts | 36 +++++++++++++- .../src/shared/styles/globals/composer.css | 16 +++++-- desktop/tests/e2e/navigation.spec.ts | 48 +++++++++++++++++++ 4 files changed, 104 insertions(+), 12 deletions(-) diff --git a/desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs b/desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs index 3695275762d..20a6784c01b 100644 --- a/desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs +++ b/desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs @@ -188,6 +188,10 @@ test("markdown parsing stops message links before emphasis delimiters", () => { assert.deepEqual(token.meta, { channelName: "general", href: HREF }); }); +function renderedChipLabel(rendered) { + return `${rendered[2][2]}${rendered[3]}`; +} + test("composer node uses the sent-message chip presentation", () => { const node = { attrs: { channelName: "general", href: HREF }, @@ -209,7 +213,9 @@ test("composer node uses the sent-message chip presentation", () => { assert.equal(rendered[1]["data-buzz-link"], ""); // Channel label only — no event hash, so the chip does not change width when // the draft is sent and the rendered chip resolves its metadata. - assert.equal(rendered[2], "general"); + assert.match(rendered[1].class, /wrapping-inline-chip/); + assert.match(rendered[2][1].class, /inline-chip-leading-fragment/); + assert.equal(renderedChipLabel(rendered), "general"); }); test("composer node renders channel and entity chip presentations", () => { @@ -227,24 +233,24 @@ test("composer node renders channel and entity chip presentations", () => { const channel = render(CHANNEL_HREF); assert.equal(channel[1]["data-channel-deep-link"], ""); assert.match(channel[1].class, /inline-chip-icon-channel/); - assert.equal(channel[2], "general"); + assert.equal(renderedChipLabel(channel), "general"); const repo = render(REPO_HREF); assert.equal(repo[1]["data-buzz-link-kind"], "repo"); assert.match(repo[1].class, /inline-chip-icon-repo/); - assert.equal(repo[2], "buzz-world"); + assert.equal(renderedChipLabel(repo), "buzz-world"); const issue = render(ISSUE_HREF); assert.equal(issue[1]["data-buzz-link-kind"], "issue"); assert.match(issue[1].class, /inline-chip-icon-issue/); // Repository name only — the rendered chip never widens into the issue // title, so the composer must not widen into the event hash either. - assert.equal(issue[2], "buzz-world"); + assert.equal(renderedChipLabel(issue), "buzz-world"); const pullRequest = render(PR_HREF); assert.equal(pullRequest[1]["data-buzz-link-kind"], "pr"); assert.match(pullRequest[1].class, /inline-chip-icon-pr/); - assert.equal(pullRequest[2], "buzz-world"); + assert.equal(renderedChipLabel(pullRequest), "buzz-world"); }); test("markdown rendering stores identity in attributes, not visible id text", () => { diff --git a/desktop/src/features/messages/lib/composerMessageLinkNode.ts b/desktop/src/features/messages/lib/composerMessageLinkNode.ts index 9587c312cab..4f25a6bc871 100644 --- a/desktop/src/features/messages/lib/composerMessageLinkNode.ts +++ b/desktop/src/features/messages/lib/composerMessageLinkNode.ts @@ -14,6 +14,7 @@ import { inlineChipIconClasses, type InlineChipIconKind, MENTION_CHIP_BASE_CLASSES, + WRAPPING_INLINE_CHIP_CLASSES, } from "@/shared/ui/mentionChip"; import { buildChannelLink, parseChannelLink } from "./channelLink"; import { getMessageLinkLabel } from "./messageLinkLabel"; @@ -285,6 +286,32 @@ function composerLinkPresentation( }; } +function wrappingComposerChipContent( + label: string, + icon: InlineChipIconKind, +): { leading: [string, Record, string]; remainder: string } { + const separatorIndex = label.search(/[-\s]/u); + const leadingEnd = + separatorIndex < 0 + ? Array.from(label)[0]?.length + : separatorIndex + (label[separatorIndex] === "-" ? 1 : 0); + if (!leadingEnd) { + return { leading: ["span", {}, label], remainder: "" }; + } + + return { + leading: [ + "span", + { + "aria-hidden": "true", + class: `inline-chip-leading-fragment ${inlineChipIconClasses(icon)}`, + }, + label.slice(0, leadingEnd), + ], + remainder: label.slice(leadingEnd), + }; +} + export const ComposerMessageLinkNode = Node.create({ name: COMPOSER_MESSAGE_LINK_NODE_NAME, @@ -328,11 +355,15 @@ export const ComposerMessageLinkNode = String(node.attrs.channelName ?? ""), this.options.resolveChannelName, ); + const content = wrappingComposerChipContent( + presentation.label, + presentation.icon, + ); return [ "span", mergeAttributes(HTMLAttributes, { "aria-label": presentation.ariaLabel, - class: `${MENTION_CHIP_BASE_CLASSES} ${inlineChipIconClasses(presentation.icon)} cursor-text`, + class: `${MENTION_CHIP_BASE_CLASSES} ${WRAPPING_INLINE_CHIP_CLASSES} ${inlineChipIconClasses(presentation.icon)} cursor-text`, "data-buzz-link": "", "data-channel-name": presentation.channelName, "data-composer-buzz-link": "", @@ -340,7 +371,8 @@ export const ComposerMessageLinkNode = ...presentation.dataAttributes, title: presentation.ariaLabel, }), - presentation.label, + content.leading, + content.remainder, ]; }, diff --git a/desktop/src/shared/styles/globals/composer.css b/desktop/src/shared/styles/globals/composer.css index fbbcdf026c9..2b71b4483a2 100644 --- a/desktop/src/shared/styles/globals/composer.css +++ b/desktop/src/shared/styles/globals/composer.css @@ -256,12 +256,18 @@ Body copy is text-sm; inline code labels sit one step down (text-xs). All chip variants share one box height so mono labels stay balanced. - Rendered-message chips use inline-flex. The same decoration in the - composer pulls the caret into the chip and swallows the trailing - space after an @mention, so typing "hello" after @quinn becomes - "@quinnhello". Keep composer chips inline; leave ::before icons in - place. */ + Plain-text mention decorations stay inline because inline-flex pulls the + caret into their decorated range and swallows the trailing space after an + @mention. Composer Buzz links are atom nodes, but their labels may still + fragment between characters; only the icon and leading label fragment stay + together. */ .rich-text-composer .tiptap .mention-chip { display: inline; min-height: 0; } + +.rich-text-composer .tiptap [data-composer-buzz-link] { + display: inline; + overflow-wrap: anywhere; + white-space: normal; +} diff --git a/desktop/tests/e2e/navigation.spec.ts b/desktop/tests/e2e/navigation.spec.ts index 189db09e0f6..808df6439e1 100644 --- a/desktop/tests/e2e/navigation.spec.ts +++ b/desktop/tests/e2e/navigation.spec.ts @@ -445,6 +445,54 @@ test("mixed Buzz permalinks render as chips in the composer", async ({ await expect(composerInput).not.toContainText("buzz://"); }); +test("composer Buzz chip labels wrap without orphaning their icons", async ({ + page, +}) => { + await page.goto("/"); + await page.getByTestId("channel-general").click(); + + const composerInput = page.getByTestId("message-input"); + const repoLink = `buzz://repo?owner=${"a".repeat(64)}&d=buzz-inline-chip-wrapping-fix`; + await composerInput.evaluate((element) => { + element.style.width = "220px"; + }); + await composerInput.evaluate((element, text) => { + const clipboardData = new DataTransfer(); + clipboardData.setData("text/plain", text); + element.dispatchEvent( + new ClipboardEvent("paste", { + bubbles: true, + cancelable: true, + clipboardData, + }), + ); + }, `A deliberately long lead-in ${repoLink}`); + + const chip = composerInput.locator('[data-composer-buzz-link=""]'); + const leadingFragment = chip.locator(".inline-chip-leading-fragment"); + await expect(chip).toHaveText("buzz-inline-chip-wrapping-fix"); + await expect(chip).toHaveCSS("display", "inline"); + await expect(chip).toHaveCSS("overflow-wrap", "anywhere"); + await expect(leadingFragment).toHaveText("buzz-"); + await expect + .poll(() => + chip.evaluate((element) => { + const chipRange = document.createRange(); + chipRange.selectNodeContents(element); + const leading = element.querySelector(".inline-chip-leading-fragment"); + return { + chipLineBoxes: new Set( + Array.from(chipRange.getClientRects(), (rect) => + Math.round(rect.y), + ), + ).size, + leadingLineBoxes: leading?.getClientRects().length ?? 0, + }; + }), + ) + .toEqual({ chipLineBoxes: 3, leadingLineBoxes: 1 }); +}); + test("message links to visible root messages open the thread panel", async ({ page, }) => { From 03320984c96ebc6c0185cd5d588616cc2fb5fe93 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Sat, 22 Aug 2026 11:42:04 -0700 Subject: [PATCH 02/10] test(composer): cover inline chip wrapping transitions Co-authored-by: Carl Signed-off-by: Taylor Ho --- desktop/tests/e2e/navigation.spec.ts | 124 +++++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 8 deletions(-) diff --git a/desktop/tests/e2e/navigation.spec.ts b/desktop/tests/e2e/navigation.spec.ts index 808df6439e1..435e48a8a0e 100644 --- a/desktop/tests/e2e/navigation.spec.ts +++ b/desktop/tests/e2e/navigation.spec.ts @@ -448,14 +448,34 @@ test("mixed Buzz permalinks render as chips in the composer", async ({ test("composer Buzz chip labels wrap without orphaning their icons", async ({ page, }) => { + await page.addInitScript(() => { + window.__BUZZ_E2E_EXTRA_PROJECT_EVENTS__ = [ + { + id: "mock-project-relay-tools-observability-console", + kind: 30617, + pubkey: + "953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f", + created_at: Math.floor(Date.now() / 1000) - 60, + content: + "Operator tooling and observability console for relay deployments.", + tags: [ + ["d", "relay-tools-observability-console"], + ["name", "relay-tools-observability-console"], + [ + "description", + "Operator tooling and observability console for relay deployments.", + ], + ["clone", "https://github.com/block/relay-tools.git"], + ], + }, + ]; + }); await page.goto("/"); await page.getByTestId("channel-general").click(); const composerInput = page.getByTestId("message-input"); - const repoLink = `buzz://repo?owner=${"a".repeat(64)}&d=buzz-inline-chip-wrapping-fix`; - await composerInput.evaluate((element) => { - element.style.width = "220px"; - }); + const repoLink = + "buzz://repo?owner=953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f&d=relay-tools-observability-console"; await composerInput.evaluate((element, text) => { const clipboardData = new DataTransfer(); clipboardData.setData("text/plain", text); @@ -466,14 +486,35 @@ test("composer Buzz chip labels wrap without orphaning their icons", async ({ clipboardData, }), ); - }, `A deliberately long lead-in ${repoLink}`); + }, repoLink); const chip = composerInput.locator('[data-composer-buzz-link=""]'); const leadingFragment = chip.locator(".inline-chip-leading-fragment"); - await expect(chip).toHaveText("buzz-inline-chip-wrapping-fix"); + await expect(chip).toHaveText("relay-tools-observability-console"); + const chipHeightAtWidth = async (width: number) => { + await composerInput.evaluate((element, nextWidth) => { + element.style.width = `${nextWidth}px`; + }, width); + return chip.evaluate((element) => { + const range = document.createRange(); + range.selectNodeContents(element); + const rects = Array.from(range.getClientRects()).filter( + (rect) => rect.width > 0 && rect.height > 0, + ); + return ( + Math.max(...rects.map((rect) => rect.bottom)) - + Math.min(...rects.map((rect) => rect.top)) + ); + }); + }; + const wideHeight = await chipHeightAtWidth(420); + const mediumHeight = await chipHeightAtWidth(210); + const narrowHeight = await chipHeightAtWidth(150); + expect(mediumHeight).toBeGreaterThan(wideHeight); + expect(narrowHeight).toBeGreaterThan(mediumHeight); await expect(chip).toHaveCSS("display", "inline"); await expect(chip).toHaveCSS("overflow-wrap", "anywhere"); - await expect(leadingFragment).toHaveText("buzz-"); + await expect(leadingFragment).toHaveText("relay-"); await expect .poll(() => chip.evaluate((element) => { @@ -490,7 +531,74 @@ test("composer Buzz chip labels wrap without orphaning their icons", async ({ }; }), ) - .toEqual({ chipLineBoxes: 3, leadingLineBoxes: 1 }); + .toEqual({ chipLineBoxes: 4, leadingLineBoxes: 1 }); + + await expect(chip).toHaveAttribute( + "title", + "Open repository relay-tools-observability-console", + ); + await page.getByTestId("send-message").click(); + + const sentChip = page.getByTestId("message-row").last().getByRole("button", { + name: "Open repository relay-tools-observability-console", + }); + await expect(sentChip).toBeVisible(); + await sentChip.evaluate((element) => { + const container = element.parentElement; + if (container) container.style.width = "220px"; + }); + const fragmentRects = await sentChip.evaluate((element) => + Array.from(element.getClientRects(), (rect) => ({ + bottom: rect.bottom, + height: rect.height, + left: rect.left, + right: rect.right, + top: rect.top, + width: rect.width, + })).filter((rect) => rect.width > 0 && rect.height > 0), + ); + expect(fragmentRects.length).toBeGreaterThanOrEqual(2); + + const hoverFragment = async (fragment: (typeof fragmentRects)[number]) => { + await expect + .poll(async () => { + await page.getByTestId("chat-title").hover(); + await sentChip.hover(); + return page.getByRole("tooltip").count(); + }) + .toBeGreaterThan(0); + await page.mouse.move( + fragment.left + fragment.width / 2, + fragment.top + fragment.height / 2, + ); + await sentChip.dispatchEvent("pointermove", { + clientX: fragment.left + fragment.width / 2, + clientY: fragment.top + fragment.height / 2, + pointerType: "mouse", + }); + const tooltip = page.getByRole("tooltip"); + await expect(tooltip).toBeVisible(); + const tooltipBox = await tooltip.boundingBox(); + if (!tooltipBox) throw new Error("Expected repository tooltip bounds"); + return { + cursorX: fragment.left + fragment.width / 2, + tooltipCenter: tooltipBox.x + tooltipBox.width / 2, + }; + }; + + const firstTooltip = await hoverFragment(fragmentRects[0]); + const lastTooltip = await hoverFragment( + fragmentRects.at(-1) ?? fragmentRects[0], + ); + expect( + Math.abs(firstTooltip.tooltipCenter - firstTooltip.cursorX), + ).toBeLessThan(1); + expect( + Math.abs(lastTooltip.tooltipCenter - lastTooltip.cursorX), + ).toBeLessThan(1); + expect( + Math.abs(firstTooltip.tooltipCenter - lastTooltip.tooltipCenter), + ).toBeGreaterThan(1); }); test("message links to visible root messages open the thread panel", async ({ From 5aa5c771fdbe09e3ed85244809e0e036033ab918 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Sat, 22 Aug 2026 14:29:36 -0700 Subject: [PATCH 03/10] fix(chips): preserve label continuity across wrapping Co-authored-by: Carl Signed-off-by: Taylor Ho --- .../lib/composerMessageLinkNode.test.mjs | 33 +++++++++++++++++++ .../messages/lib/composerMessageLinkNode.ts | 11 +++---- .../src/shared/ui/markdown/BuzzLinkChip.tsx | 7 ++-- desktop/src/shared/ui/mentionChip.ts | 31 ++++++++++++++--- 4 files changed, 66 insertions(+), 16 deletions(-) diff --git a/desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs b/desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs index 20a6784c01b..2cf1f92b515 100644 --- a/desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs +++ b/desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs @@ -218,6 +218,39 @@ test("composer node uses the sent-message chip presentation", () => { assert.equal(renderedChipLabel(rendered), "general"); }); +test("composer node truncates and preserves grapheme-safe leading fragments", () => { + const render = ComposerMessageLinkNode.configure({ + resolveChannelName: () => undefined, + }).config.renderHTML; + assert.ok(render); + + const longName = `relay-${"observability".repeat(5)}`; + const longRendered = render.call( + { options: { resolveChannelName: () => undefined } }, + { + node: { attrs: { channelName: longName, href: HREF } }, + HTMLAttributes: {}, + }, + ); + assert.equal(renderedChipLabel(longRendered), `${longName.slice(0, 47)}…`); + + for (const label of ["🇺🇸channel", "e\u0301quipe"]) { + const rendered = render.call( + { options: { resolveChannelName: () => undefined } }, + { + node: { attrs: { channelName: label, href: HREF } }, + HTMLAttributes: {}, + }, + ); + const firstGrapheme = Array.from( + new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(label), + ({ segment }) => segment, + )[0]; + assert.equal(rendered[2][2], firstGrapheme); + assert.equal(renderedChipLabel(rendered), label); + } +}); + test("composer node renders channel and entity chip presentations", () => { const render = (href) => globalThis.structuredClone( diff --git a/desktop/src/features/messages/lib/composerMessageLinkNode.ts b/desktop/src/features/messages/lib/composerMessageLinkNode.ts index 4f25a6bc871..f25a679ffb2 100644 --- a/desktop/src/features/messages/lib/composerMessageLinkNode.ts +++ b/desktop/src/features/messages/lib/composerMessageLinkNode.ts @@ -12,8 +12,10 @@ import { } from "@/shared/lib/entityLink"; import { inlineChipIconClasses, + inlineChipLeadingEnd, type InlineChipIconKind, MENTION_CHIP_BASE_CLASSES, + truncateInlineChipLabel, WRAPPING_INLINE_CHIP_CLASSES, } from "@/shared/ui/mentionChip"; import { buildChannelLink, parseChannelLink } from "./channelLink"; @@ -290,11 +292,7 @@ function wrappingComposerChipContent( label: string, icon: InlineChipIconKind, ): { leading: [string, Record, string]; remainder: string } { - const separatorIndex = label.search(/[-\s]/u); - const leadingEnd = - separatorIndex < 0 - ? Array.from(label)[0]?.length - : separatorIndex + (label[separatorIndex] === "-" ? 1 : 0); + const leadingEnd = inlineChipLeadingEnd(label); if (!leadingEnd) { return { leading: ["span", {}, label], remainder: "" }; } @@ -355,8 +353,9 @@ export const ComposerMessageLinkNode = String(node.attrs.channelName ?? ""), this.options.resolveChannelName, ); + const visibleLabel = truncateInlineChipLabel(presentation.label); const content = wrappingComposerChipContent( - presentation.label, + visibleLabel, presentation.icon, ); return [ diff --git a/desktop/src/shared/ui/markdown/BuzzLinkChip.tsx b/desktop/src/shared/ui/markdown/BuzzLinkChip.tsx index c31aa9b5191..76d01d29a96 100644 --- a/desktop/src/shared/ui/markdown/BuzzLinkChip.tsx +++ b/desktop/src/shared/ui/markdown/BuzzLinkChip.tsx @@ -5,6 +5,7 @@ import { cn } from "@/shared/lib/cn"; import { InlineChip } from "@/shared/ui/InlineChip"; import { inlineChipIconClasses, + inlineChipLeadingEnd, type InlineChipIconKind, truncateInlineChipLabel, WRAPPING_INLINE_CHIP_CLASSES, @@ -72,11 +73,7 @@ function wrappingChipContent( ): React.ReactNode { if (typeof children !== "string" || children.length === 0) return children; - const separatorIndex = children.search(/[-\s]/u); - const leadingEnd = - separatorIndex < 0 - ? Array.from(children)[0]?.length - : separatorIndex + (children[separatorIndex] === "-" ? 1 : 0); + const leadingEnd = inlineChipLeadingEnd(children); if (!leadingEnd) return children; return ( diff --git a/desktop/src/shared/ui/mentionChip.ts b/desktop/src/shared/ui/mentionChip.ts index 5c922728585..414df669cc2 100644 --- a/desktop/src/shared/ui/mentionChip.ts +++ b/desktop/src/shared/ui/mentionChip.ts @@ -4,13 +4,34 @@ export const MENTION_CHIP_HOVER_CLASSES = "mention-chip-hover"; const INLINE_CHIP_LABEL_MAX_CHARACTERS = 48; +const inlineChipGraphemeSegmenter = + typeof Intl.Segmenter === "function" + ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) + : null; + +function inlineChipGraphemes(label: string): string[] { + return inlineChipGraphemeSegmenter + ? Array.from( + inlineChipGraphemeSegmenter.segment(label), + ({ segment }) => segment, + ) + : Array.from(label); +} + /** Caps a fragmentable chip label without changing its underlying metadata. */ export function truncateInlineChipLabel(label: string): string { - const characters = Array.from(label); - if (characters.length <= INLINE_CHIP_LABEL_MAX_CHARACTERS) return label; - return `${characters - .slice(0, INLINE_CHIP_LABEL_MAX_CHARACTERS - 1) - .join("")}…`; + const graphemes = inlineChipGraphemes(label); + if (graphemes.length <= INLINE_CHIP_LABEL_MAX_CHARACTERS) return label; + return `${graphemes.slice(0, INLINE_CHIP_LABEL_MAX_CHARACTERS - 1).join("")}…`; +} + +/** Returns the boundary after the icon-bearing prefix of a wrapping chip. */ +export function inlineChipLeadingEnd(label: string): number { + const separatorIndex = label.search(/[-\s]/u); + if (separatorIndex >= 0) { + return separatorIndex + (label[separatorIndex] === "-" ? 1 : 0); + } + return inlineChipGraphemes(label)[0]?.length ?? 0; } /** Allows a long chip to fragment into separately decorated line boxes. */ From 91baa452413e264fd0b51cfb50714fb4861e4be2 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Sat, 22 Aug 2026 14:53:11 -0700 Subject: [PATCH 04/10] fix(chips): bound icon-bearing wrap fragments Co-authored-by: Carl Signed-off-by: Taylor Ho --- .../lib/composerMessageLinkNode.test.mjs | 14 +++--- .../messages/lib/composerMessageLinkNode.ts | 12 ++++- .../src/shared/styles/globals/composer.css | 6 --- desktop/src/shared/ui/markdown.test.mjs | 6 +-- .../src/shared/ui/markdown/BuzzLinkChip.tsx | 15 +++++- desktop/src/shared/ui/mentionChip.ts | 20 ++++++-- desktop/tests/e2e/navigation.spec.ts | 47 +++++++++++++++---- 7 files changed, 88 insertions(+), 32 deletions(-) diff --git a/desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs b/desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs index 2cf1f92b515..f5d47871f00 100644 --- a/desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs +++ b/desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs @@ -234,7 +234,12 @@ test("composer node truncates and preserves grapheme-safe leading fragments", () ); assert.equal(renderedChipLabel(longRendered), `${longName.slice(0, 47)}…`); - for (const label of ["🇺🇸channel", "e\u0301quipe"]) { + for (const [label, expectedLeading] of [ + ["🇺🇸channel", "🇺🇸chan"], + ["e\u0301quipe", "e\u0301quip"], + ["relaytoolsobservabilityconsole-main", "relay"], + [" leading-space", ""], + ]) { const rendered = render.call( { options: { resolveChannelName: () => undefined } }, { @@ -242,11 +247,8 @@ test("composer node truncates and preserves grapheme-safe leading fragments", () HTMLAttributes: {}, }, ); - const firstGrapheme = Array.from( - new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(label), - ({ segment }) => segment, - )[0]; - assert.equal(rendered[2][2], firstGrapheme); + assert.equal(rendered[2][2], expectedLeading); + assert.match(rendered[2][1].class, /inline-chip-with-icon/); assert.equal(renderedChipLabel(rendered), label); } }); diff --git a/desktop/src/features/messages/lib/composerMessageLinkNode.ts b/desktop/src/features/messages/lib/composerMessageLinkNode.ts index f25a679ffb2..59b15c78ba6 100644 --- a/desktop/src/features/messages/lib/composerMessageLinkNode.ts +++ b/desktop/src/features/messages/lib/composerMessageLinkNode.ts @@ -294,7 +294,17 @@ function wrappingComposerChipContent( ): { leading: [string, Record, string]; remainder: string } { const leadingEnd = inlineChipLeadingEnd(label); if (!leadingEnd) { - return { leading: ["span", {}, label], remainder: "" }; + return { + leading: [ + "span", + { + "aria-hidden": "true", + class: `inline-chip-leading-fragment ${inlineChipIconClasses(icon)}`, + }, + "", + ], + remainder: label, + }; } return { diff --git a/desktop/src/shared/styles/globals/composer.css b/desktop/src/shared/styles/globals/composer.css index 2b71b4483a2..781ac8a1968 100644 --- a/desktop/src/shared/styles/globals/composer.css +++ b/desktop/src/shared/styles/globals/composer.css @@ -265,9 +265,3 @@ display: inline; min-height: 0; } - -.rich-text-composer .tiptap [data-composer-buzz-link] { - display: inline; - overflow-wrap: anywhere; - white-space: normal; -} diff --git a/desktop/src/shared/ui/markdown.test.mjs b/desktop/src/shared/ui/markdown.test.mjs index 8136ff9f08e..6cd15c69b4c 100644 --- a/desktop/src/shared/ui/markdown.test.mjs +++ b/desktop/src/shared/ui/markdown.test.mjs @@ -1121,7 +1121,7 @@ test("bare Buzz permalinks render cohesive icon-prefixed chips", () => { assert.equal((html.match(/data-channel-deep-link=""/g) ?? []).length, 1); assert.match(html, /inline-chip-icon-channel/); assert.match(html, /wrapping-inline-chip/); - assert.match(html, /inline-chip-leading-fragment[^>]*>e]*>engin { ); assert.equal( - (html.match(/inline-chip-leading-fragment[^>]*>5<\/span>80ca78b/g) ?? []) + (html.match(/inline-chip-leading-fragment[^>]*>580ca<\/span>78b/g) ?? []) .length, 2, ); @@ -1332,7 +1332,7 @@ test("channel references replace the authored hash with the channel icon", () => assert.match(html, /inline-chip-icon-channel/); assert.match(html, /wrapping-inline-chip/); - assert.match(html, /inline-chip-leading-fragment[^>]*>e]*>engin]+>/g, ""), /engineering/); assert.doesNotMatch(html, />#engineering +