diff --git a/frontend/src/lib/components/PersonMentionEditor.svelte b/frontend/src/lib/components/PersonMentionEditor.svelte index 721831df..592fdd7d 100644 --- a/frontend/src/lib/components/PersonMentionEditor.svelte +++ b/frontend/src/lib/components/PersonMentionEditor.svelte @@ -82,6 +82,11 @@ onMount(() => { editor = new Editor({ element: editorEl, + // Initial editable state honors the `disabled` prop. The reactive + // $effect below keeps it in sync if the prop flips after mount — + // without this, a keyboard user can tab into the contenteditable + // even when the wrapper has pointer-events-none (WCAG 2.1.1). + editable: !disabled, extensions: [ StarterKit.configure({ heading: false, @@ -232,11 +237,19 @@ onMount(() => { onDestroy(() => { editor?.destroy(); }); + +// Keep editor in sync with the reactive `disabled` prop. Tiptap's setEditable +// flips contenteditable on the inner DOM and stops accepting input — matches +// the textarea's old `disabled` semantics for keyboard users (WCAG 2.1.1). +$effect(() => { + editor?.setEditable(!disabled); +});
diff --git a/frontend/src/lib/components/PersonMentionEditor.svelte.spec.ts b/frontend/src/lib/components/PersonMentionEditor.svelte.spec.ts index 39a1dc15..b56f4db6 100644 --- a/frontend/src/lib/components/PersonMentionEditor.svelte.spec.ts +++ b/frontend/src/lib/components/PersonMentionEditor.svelte.spec.ts @@ -47,7 +47,9 @@ function mockFetchEmpty() { type Snapshot = { value: string; mentionedPersons: PersonMention[] }; -function renderHost(initial: { value?: string; mentionedPersons?: PersonMention[] } = {}) { +function renderHost( + initial: { value?: string; mentionedPersons?: PersonMention[]; disabled?: boolean } = {} +) { let snapshot: Snapshot = { value: initial.value ?? '', mentionedPersons: initial.mentionedPersons ?? [] @@ -55,6 +57,7 @@ function renderHost(initial: { value?: string; mentionedPersons?: PersonMention[ render(PersonMentionEditorHost, { initialValue: initial.value ?? '', initialMentions: initial.mentionedPersons ?? [], + disabled: initial.disabled ?? false, onChange: (snap: Snapshot) => { snapshot = snap; } @@ -280,6 +283,39 @@ describe('PersonMentionEditor — keyboard navigation', () => { }); }); +// ─── Disabled state (WCAG 2.1.1 — keyboard users) ──────────────────────────── + +describe('PersonMentionEditor — disabled state', () => { + it('sets contenteditable=false on the editor when disabled', async () => { + renderHost({ value: 'Bestehender Text', disabled: true }); + + await vi.waitFor(() => { + const textbox = document.querySelector('[role="textbox"]') as HTMLElement | null; + expect(textbox).not.toBeNull(); + expect(textbox!.getAttribute('contenteditable')).toBe('false'); + }); + }); + + it('exposes aria-disabled=true on the editor wrapper when disabled', async () => { + renderHost({ disabled: true }); + + await vi.waitFor(() => { + const wrapper = document.querySelector('[aria-disabled="true"]'); + expect(wrapper).not.toBeNull(); + }); + }); + + it('keeps the editor editable (contenteditable=true) when not disabled', async () => { + renderHost({ disabled: false }); + + await vi.waitFor(() => { + const textbox = document.querySelector('[role="textbox"]') as HTMLElement | null; + expect(textbox).not.toBeNull(); + expect(textbox!.getAttribute('contenteditable')).toBe('true'); + }); + }); +}); + // ─── Touch target (WCAG 2.2 AA) ────────────────────────────────────────────── describe('PersonMentionEditor — touch target', () => { diff --git a/frontend/src/lib/components/PersonMentionEditor.test-host.svelte b/frontend/src/lib/components/PersonMentionEditor.test-host.svelte index e608d694..cdc31df0 100644 --- a/frontend/src/lib/components/PersonMentionEditor.test-host.svelte +++ b/frontend/src/lib/components/PersonMentionEditor.test-host.svelte @@ -9,10 +9,17 @@ type Props = { initialValue?: string; initialMentions?: PersonMention[]; placeholder?: string; + disabled?: boolean; onChange: (snapshot: { value: string; mentionedPersons: PersonMention[] }) => void; }; -let { initialValue = '', initialMentions = [], placeholder, onChange }: Props = $props(); +let { + initialValue = '', + initialMentions = [], + placeholder, + disabled = false, + onChange +}: Props = $props(); // initial* props seed mount-time state; reading them inside untrack signals // the intentional one-shot capture and silences state_referenced_locally. @@ -28,4 +35,5 @@ $effect(() => { bind:value={value} bind:mentionedPersons={mentionedPersons} placeholder={placeholder} + disabled={disabled} />