diff --git a/src/app/components/message/MsgTypeRenderers.tsx b/src/app/components/message/MsgTypeRenderers.tsx index 659fcf2f3..191d1895a 100644 --- a/src/app/components/message/MsgTypeRenderers.tsx +++ b/src/app/components/message/MsgTypeRenderers.tsx @@ -1,4 +1,4 @@ -import React, { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react'; +import React, { CSSProperties, ReactNode, useEffect, useId, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Box, Button, config, Icon, Icons, Text, color, toRem } from 'folds'; import { IContent, MatrixEvent } from 'matrix-js-sdk'; @@ -41,7 +41,10 @@ import { formatAccuracy } from '../../utils/geoShare'; import { Attachment, AttachmentBox, AttachmentContent, AttachmentHeader } from './attachment'; import { FileHeader, FileDownloadButton } from './FileHeader'; -const COLLAPSE_MAX_HEIGHT = 320; // px ≈ 20 lines +// ≈ 20 lines; in rem so a larger font setting doesn't show fewer lines (Gitea #343). +const COLLAPSE_MAX_HEIGHT_REM = 20; +const collapseMaxHeightPx = (): number => + COLLAPSE_MAX_HEIGHT_REM * (parseFloat(getComputedStyle(document.documentElement).fontSize) || 16); type CollapsibleBodyProps = { eventId?: string; @@ -79,7 +82,7 @@ function CollapsibleBody({ eventId, children }: CollapsibleBodyProps) { const el = bodyRef.current; if (!el) return undefined; const observer = new ResizeObserver(() => { - setNeedsCollapse(el.scrollHeight > COLLAPSE_MAX_HEIGHT); + setNeedsCollapse(el.scrollHeight > collapseMaxHeightPx()); }); observer.observe(el); return () => observer.disconnect(); @@ -89,16 +92,36 @@ function CollapsibleBody({ eventId, children }: CollapsibleBodyProps) { // (Gitea #85); useReducedMotion subscribes to the change event instead. const prefersReducedMotion = useReducedMotion(); + const bodyId = useId(); + const isClipped = needsCollapse && collapsed; + // [Gitea #343] The clipped part stays in the DOM, so Tab can reach a link in + // it; the browser would then scroll the overflow:hidden box itself (top cut + // off, and it stayed that way). Expand instead when focus lands inside. + const handleFocus = () => { + if (isClipped) setCollapsed(false); + }; + useEffect(() => { + if (isClipped && bodyRef.current) bodyRef.current.scrollTop = 0; + }, [isClipped]); + + // Fade the content itself rather than painting a fixed-colour gradient over + // it, so it blends on any background (bubbles, highlights, chat backgrounds). + const fadeMask = 'linear-gradient(#000 calc(100% - 3rem), transparent)'; + return (