feat: decouple person-mention display text from person name (#372) #373

Merged
marcel merged 17 commits from feat/person-mentions-issue-362-frontend-b2 into main 2026-04-29 16:55:53 +02:00
2 changed files with 27 additions and 48 deletions
Showing only changes of commit 7a25feb04e - Show all commits

View File

@@ -79,17 +79,6 @@ let leftBorderClass = $derived(
saveState === 'error' ? 'border-l-2 border-error' : active ? 'border-l-2 border-turquoise' : ''
);
// Single source of truth for the editor's textarea — stored on attach so
// we can read selection bounds for quote selection without re-querying the DOM.
let textareaEl: HTMLTextAreaElement | null = null;
function captureTextarea(node: HTMLTextAreaElement) {
textareaEl = node;
return () => {
textareaEl = null;
};
}
function emitChange() {
onTextChange(localText, localMentions);
}
@@ -101,17 +90,6 @@ async function handleDelete() {
});
if (confirmed) onDeleteClick();
}
function handleTextareaMouseUp() {
if (!textareaEl) return;
const start = textareaEl.selectionStart;
const end = textareaEl.selectionEnd;
if (start !== end) {
selectedQuote = localText.substring(start, end);
} else {
selectedQuote = null;
}
}
</script>
<div
@@ -176,24 +154,22 @@ function handleTextareaMouseUp() {
{/if}
</div>
<!-- Textarea (now powered by PersonMentionEditor for @-mention typeahead) -->
<div onmouseup={handleTextareaMouseUp} role="presentation">
<PersonMentionEditor
bind:value={() => localText,
(v) => {
localText = v;
emitChange();
}}
bind:mentionedPersons={() => localMentions,
(next) => {
localMentions = next;
emitChange();
}}
placeholder={m.transcription_block_placeholder()}
onfocus={onFocus}
captureTextarea={captureTextarea}
/>
</div>
<!-- Editor powered by PersonMentionEditor (Tiptap) for @-mention typeahead -->
<PersonMentionEditor
bind:value={() => localText,
(v) => {
localText = v;
emitChange();
}}
bind:mentionedPersons={() => localMentions,
(next) => {
localMentions = next;
emitChange();
}}
placeholder={m.transcription_block_placeholder()}
onfocus={onFocus}
onSelectionChange={(text) => (selectedQuote = text)}
/>
{#if selectedQuote}
<p class="mt-1 text-xs text-ink-3">{m.transcription_block_quote_hint()}</p>

View File

@@ -41,8 +41,7 @@ describe('TranscriptionBlock — rendering', () => {
it('renders text in textarea', async () => {
renderBlock();
const textarea = page.getByRole('textbox');
await expect.element(textarea).toHaveValue('Liebe Mutter,');
await expect.element(page.getByText('Liebe Mutter,')).toBeInTheDocument();
});
it('renders optional label when provided', async () => {
@@ -226,14 +225,18 @@ describe('TranscriptionBlock — delete confirmation', () => {
// ─── Quote selection ─────────────────────────────────────────────────────────
describe('TranscriptionBlock — quote selection', () => {
it('shows quote hint after text is selected in textarea', async () => {
it('shows quote hint after text is selected in the editor', async () => {
renderBlock({ text: 'Breslau, den 12. August' });
await page.getByRole('textbox').click();
// Select text and fire mouseup via native DOM — locator.selectText/dispatchEvent not available
const el = document.querySelector('textarea') as HTMLTextAreaElement;
el.focus();
el.setSelectionRange(0, el.value.length);
el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
// Select all text in the contenteditable via the native Selection API.
// Tiptap fires selectionUpdate which the block forwards as onSelectionChange.
const editorEl = document.querySelector('[role="textbox"]') as HTMLElement;
const range = document.createRange();
range.selectNodeContents(editorEl);
const selection = window.getSelection()!;
selection.removeAllRanges();
selection.addRange(range);
editorEl.dispatchEvent(new Event('input', { bubbles: true }));
await expect.element(page.getByText(/Zitat/)).toBeInTheDocument();
});
});