fix(markdown-editor): give the link edit-form label/aria-describedby parity

The link edit-form relied on a placeholder + aria-label with no visible <Label>,
and its invalid-URL alert had no aria-describedby back to the input — both
present on the sibling image edit-form. A screen-reader user who tabbed onto the
field after the error fired never heard why it was invalid. Add a visible Label
(via useId) and wire aria-describedby to the error id, matching ImageEditForm.

Also trim two comments per the zero-comment rule: the normalizeLinkUrl-return
restatement (documented at the callee) and the "(matches Docs/Notion)"
competitor-justification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-07-07 20:33:31 +07:00
co-authored by Claude Opus 4.8
parent 215c1cd8c9
commit fde073e393
@@ -1,9 +1,10 @@
import type { Editor } from '@tiptap/react';
import { ArrowUpRight, Check, Trash2 } from 'lucide-react';
import { useState } from 'react';
import { useId, useState } from 'react';
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '@/components/ui/input-group';
import { Label } from '@/components/ui/label';
import { normalizeLinkUrl } from './markdown-editor-toolbar-url';
@@ -22,8 +23,9 @@ interface LinkEditFormProps {
// mount, so consumers give it a fresh `key` per editing session.
export function LinkEditForm({ autoFocus = true, editor, initialUrl, isActive, onDone }: LinkEditFormProps) {
const [url, setUrl] = useState(initialUrl);
const urlId = useId();
const errorId = useId();
// Normalized absolute href (scheme prepended, protocol validated) or null when the input is unsafe/empty.
const href = normalizeLinkUrl(url);
const isInvalid = url !== '' && href === null;
@@ -35,8 +37,8 @@ export function LinkEditForm({ autoFocus = true, editor, initialUrl, isActive, o
const { empty } = editor.state.selection;
if (empty && !isActive) {
// No selection to wrap → insert the URL as its own linked text (matches Docs/Notion). Shows what the
// user typed but links to the normalized href.
// No selection to wrap → insert the URL as its own linked text: the visible text is what the user
// typed, but the href is the normalized value.
editor
.chain()
.focus()
@@ -62,11 +64,13 @@ export function LinkEditForm({ autoFocus = true, editor, initialUrl, isActive, o
return (
<div className="flex flex-col gap-2">
<Label htmlFor={urlId}>Link URL</Label>
<InputGroup>
<InputGroupInput
aria-describedby={isInvalid ? errorId : undefined}
aria-invalid={isInvalid}
aria-label="Link URL"
autoFocus={autoFocus}
id={urlId}
onChange={(event) => setUrl(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') {
@@ -112,6 +116,7 @@ export function LinkEditForm({ autoFocus = true, editor, initialUrl, isActive, o
{isInvalid ? (
<p
className="text-destructive text-xs"
id={errorId}
role="alert"
>
Only http, https, mailto and tel links are allowed.