From 1c62fb4722602095b890989b084de0e0b34fc420 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Sat, 17 Oct 2026 18:26:16 -0400 Subject: [PATCH] fix(reactions): the reactions row no longer flickers or loses its spacing (#305) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs in the one-row "+N" collapse: - measure() used the border box, which grows by 4 px while collapsed (margin -2 / padding 2), so a row within 4 px of fitting flipped collapsed↔expanded every frame (flicker, layout shifts, ~11 % of a core per looping message). It now measures the content box. - The collapsed style used the `margin` shorthand, which overrode the timeline's marginTop and, when removed, cleared it; the row then stuck to the message text. Longhand margins keep marginTop (minus 2 px). Verified headless (12 reactions, viewport sweep 1300–1400 px): before, 90 style flips in 1.5 s at 1344 px and margin-top 0px after a cycle; now 0 flips at every width and margin-top stays 8px. Fixes #305 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5 --- src/app/features/room/message/Reactions.tsx | 28 ++++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/app/features/room/message/Reactions.tsx b/src/app/features/room/message/Reactions.tsx index b03ab2d6d..89f59bc2b 100644 --- a/src/app/features/room/message/Reactions.tsx +++ b/src/app/features/room/message/Reactions.tsx @@ -69,8 +69,15 @@ export const Reactions = as<'div', ReactionsProps>( const measure = useCallback(() => { const container = containerRef.current; if (!container) return; - const rowWidth = container.getBoundingClientRect().width; - const gap = parseFloat(getComputedStyle(container).columnGap) || 8; + // [Gitea #305] The width the chips actually get (content box). The border + // box grows by 4 px while collapsed (margin -2 / padding 2), and measuring + // it made a row within 4 px of fitting flip collapsed↔expanded every frame. + const cs = getComputedStyle(container); + const rowWidth = + container.clientWidth - + (parseFloat(cs.paddingLeft) || 0) - + (parseFloat(cs.paddingRight) || 0); + const gap = parseFloat(cs.columnGap) || 8; const chips = keys ? keys.split('\u0000').map((k) => chipRefs.current.get(k)) : []; const widths = chips.map((el) => el?.getBoundingClientRect().width ?? 0); const moreWidth = moreRef.current?.getBoundingClientRect().width ?? 48; @@ -110,8 +117,21 @@ export const Reactions = as<'div', ReactionsProps>( style={{ ...props.style, ...(collapsed && rowHeight - ? // 2px breathing room so row-1 focus outlines are not clipped. - { maxHeight: rowHeight + 4, overflow: 'hidden', padding: 2, margin: -2 } + ? // 2px breathing room so row-1 focus outlines are not clipped. Longhand + // margins so the timeline's marginTop survives (Gitea #305: the + // `margin` shorthand overrode it and, once removed, cleared it). + { + maxHeight: rowHeight + 4, + overflow: 'hidden', + padding: 2, + marginLeft: -2, + marginRight: -2, + marginBottom: -2, + marginTop: + props.style?.marginTop !== undefined + ? `calc(${typeof props.style.marginTop === 'number' ? `${props.style.marginTop}px` : props.style.marginTop} - 2px)` + : -2, + } : {}), }} ref={(el) => {