fix(places): render markdown in the mobile place description

The mobile shell printed place descriptions and notes as plain text while
the desktop rows and inspector run the same field through react-markdown,
so a description written with the formatting bar read as `# headings` and
`**asterisks**` on the phone.

Put the renderer in one shared component instead of copying it next to
each mobile twin, with a clamped variant for list rows that keeps them one
line high the way the desktop sidebar does. A clamped caption sits inside
the tappable row, so its links keep their text and lose the anchor.

Fixes #2337
This commit is contained in:
Konstantinos Thermos
2026-09-16 15:42:07 +03:00
parent 2efa843306
commit 37163e9a41
8 changed files with 122 additions and 17 deletions
@@ -0,0 +1,44 @@
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import MarkdownText from './MarkdownText'
describe('MarkdownText', () => {
it('FE-MDTEXT-001: a description written in markdown reads as formatted text, not as syntax (#2337)', () => {
render(<MarkdownText>{'# Checkpoint Charlie\n\nThe **most famous** crossing.'}</MarkdownText>)
expect(screen.getByRole('heading', { name: 'Checkpoint Charlie' })).toBeInTheDocument()
expect(screen.getByText('most famous').tagName).toBe('STRONG')
expect(screen.queryByText(/#\s*Checkpoint/)).toBeNull()
})
it('FE-MDTEXT-002: a gfm table is a table', () => {
render(<MarkdownText>{'| Day | Stop |\n| --- | --- |\n| 1 | Gate |'}</MarkdownText>)
expect(screen.getByRole('table')).toBeInTheDocument()
expect(screen.getByRole('columnheader', { name: 'Day' })).toBeInTheDocument()
})
it('FE-MDTEXT-003: a single newline stays a line break, the way the editors show it', () => {
const { container } = render(<MarkdownText>{'Opening hours\n9–17'}</MarkdownText>)
expect(container.querySelector('br')).not.toBeNull()
})
it('FE-MDTEXT-004: a link in a full view opens in its own tab', () => {
render(<MarkdownText>{'[tickets](https://example.com/tickets)'}</MarkdownText>)
const link = screen.getByRole('link', { name: 'tickets' })
expect(link).toHaveAttribute('target', '_blank')
expect(link.getAttribute('rel')).toContain('noopener')
})
it('FE-MDTEXT-005: a clamped row keeps the link text but drops the anchor, so the tap opens the row', () => {
render(<MarkdownText clamp>{'[tickets](https://example.com/tickets)'}</MarkdownText>)
expect(screen.queryByRole('link')).toBeNull()
expect(screen.getByText('tickets')).toBeInTheDocument()
})
it('FE-MDTEXT-006: a clamped row is held to one line so a long description cannot swallow the list', () => {
const { container } = render(<MarkdownText clamp className="text-m-muted">{'# Berlin Wall Memorial\n\nA long second paragraph.'}</MarkdownText>)
const wrapper = container.firstElementChild as HTMLElement
expect(wrapper.style.maxHeight).toBe('1.2em')
expect(wrapper.className).toContain('truncate')
expect(wrapper.className).toContain('text-m-muted')
})
})
@@ -0,0 +1,42 @@
import Markdown from 'react-markdown'
import type { Components } from 'react-markdown'
import remarkGfm from 'remark-gfm'
import remarkBreaks from 'remark-breaks'
import { markdownLinkComponents } from './markdownLink'
/**
* A clamped caption lives inside a tappable row, so an anchor there would eat
* the tap that opens the row. The link text stays, the anchor goes.
*/
const clampedComponents: Components = {
a: ({ children }) => <>{children}</>,
}
/**
* A description or note, rendered (#2337). The field is Markdown wherever it is
* written — the editors ship a formatting bar — so printing it raw anywhere
* shows `# headings` and `**asterisks**` to the reader.
*
* `clamp` is the list-row variant: one line high, matching what the desktop
* sidebar rows do with the same field. The height cap is inline because the row
* itself sets a font size, and a Tailwind `leading-*` would race with it.
*/
export default function MarkdownText({ children, className = '', clamp = false }: {
children: string
className?: string
clamp?: boolean
}) {
return (
<div
className={`collab-note-md ${clamp ? 'truncate ' : ''}${className}`}
style={clamp ? { maxHeight: '1.2em', lineHeight: 1.2 } : undefined}
>
<Markdown
remarkPlugins={[remarkGfm, remarkBreaks]}
components={clamp ? clampedComponents : markdownLinkComponents}
>
{children}
</Markdown>
</div>
)
}