fix(quick-261005-blw): Links in Notiz-Kacheln oeffnen in neuem Tab

NoteLink ersetzt <a> der Markdown-Vorschau: target=_blank,
rel=noopener noreferrer; #-Sprungmarken bleiben im Fenster.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-05 08:22:43 +02:00
parent f3e816fafa
commit 781bc9f9ee
8 changed files with 89 additions and 99 deletions
@@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import MDEditor from '@uiw/react-md-editor';
import rehypeSanitize from 'rehype-sanitize';
import { NoteCheckbox, isTaskLine, toggleTaskLine } from './note-task-list';
import { NoteCheckbox, NoteLink, isTaskLine, toggleTaskLine } from './note-task-list';
describe('note-task-list', () => {
describe('isTaskLine', () => {
@@ -143,5 +143,30 @@ describe('note-task-list', () => {
expect((boxes[2] as HTMLInputElement).checked).toBe(true);
expect((boxes[3] as HTMLInputElement).checked).toBe(false);
});
it('oeffnet Links in einem neuen Tab, Sprungmarken im selben Fenster', () => {
const SRC =
'[Doku](https://example.com/doku) und https://example.org\n\n[oben](#oben)';
const { container } = render(
<MDEditor.Markdown
source={SRC}
rehypePlugins={[[rehypeSanitize]]}
components={{ a: NoteLink }}
/>,
);
const doku = container.querySelector('a[href="https://example.com/doku"]');
const auto = container.querySelector('a[href="https://example.org"]');
const hash = container.querySelector('a[href="#oben"]');
for (const link of [doku, auto]) {
expect(link?.getAttribute('target')).toBe('_blank');
expect(link?.getAttribute('rel')).toBe('noopener noreferrer');
expect(link?.hasAttribute('node')).toBe(false);
}
expect(hash).not.toBeNull();
expect(hash?.hasAttribute('target')).toBe(false);
});
});
});
@@ -115,3 +115,31 @@ export function NoteCheckbox({
/>
);
}
/**
* Ersetzt das von react-markdown gerenderte `<a>`: Links aus Notizen oeffnen
* immer in einem neuen Tab, damit das Dashboard nicht verlassen wird.
* Sprungmarken innerhalb der Notiz (`#...`) bleiben im selben Fenster.
* `node` wird wie bei NoteCheckbox nicht ins DOM durchgereicht. In der
* Desktop-App faengt das Opener-Link-Skript `target="_blank"` ab und
* oeffnet den Link im System-Browser.
*/
export function NoteLink({
node: _node,
href,
children,
...rest
}: React.AnchorHTMLAttributes<HTMLAnchorElement> & { node?: unknown }) {
if (href?.startsWith('#')) {
return (
<a href={href} {...rest}>
{children}
</a>
);
}
return (
<a {...rest} href={href} target="_blank" rel="noopener noreferrer">
{children}
</a>
);
}
@@ -8,7 +8,7 @@ import MDEditor, { commands } from '@uiw/react-md-editor';
import rehypeSanitize from 'rehype-sanitize';
import type { WidgetProps } from '../widget-registry';
import { updateWidgetConfig } from '@/lib/dashboard-api';
import { NoteCheckbox, toggleTaskLine } from './note-task-list';
import { NoteCheckbox, NoteLink, toggleTaskLine } from './note-task-list';
const DEBOUNCE_MS = 1500;
@@ -156,6 +156,7 @@ export function NoteWidget({ instanceId, config }: WidgetProps) {
onToggle={(element) => handleCheckboxToggleRef.current(element)}
/>
),
a: NoteLink,
},
}),
[],