Files
indiekit-blog/_includes/components/fediverse-modal.njk
Ricardo 1026d728af a11y: fix all remaining WCAG 2.1 AA issues from audit round 2
- Focus traps for fediverse modal and lightbox dialogs (C3, C4)
- Search widget input label (C5)
- Blogroll widget tab ARIA semantics (C6)
- Footer social links "opens in new tab" warning (S5)
- Reply context aria-label on aside (S8)
- Photo alt text fallback includes post title (S10)
- Post categories use list markup (M3)
- Funkwhale now-playing bars aria-hidden (M7)
- TOC uses static Tailwind classes instead of dynamic (M9)
- Footer headings use proper aria heading roles (M15)
- Header anchor opacity increased to 1 for contrast (M18)
- Custom HTML widgets labeled as regions (M19)
- Empty collection placeholder role=status (M22)
- GitHub widget loading state announced (N5)
- Subscribe icon contrast improved (m1)
- All Permalink links have aria-label with post context (m3)
- Podroll audio element aria-label (m4)
- Obfuscated email link aria-label (m6)
- Fediverse follow button uses aria-label (M10)

Score: 53.6% → 92.9% (26/28 WCAG criteria passing)

Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596
2026-03-07 19:34:25 +01:00

95 lines
5.5 KiB
Plaintext

{# Shared fediverse instance picker modal #}
{# Used by post.njk (interact), fediverse-follow.njk (follow), share.njk (share) #}
{# Requires: modalTitle, modalDescription variables set before include #}
<template x-if="showModal">
<div class="fixed inset-0 z-50 flex items-center justify-center p-4"
@keydown.escape.window="showModal = false"
x-init="$nextTick(() => { const input = $el.querySelector('input'); if (input) input.focus(); })"
@keydown.tab="
const focusable = [...$el.querySelectorAll('button, input, a, [tabindex]:not([tabindex=\"-1\"])')].filter(el => !el.closest('[x-show]') || el.closest('[x-show]').style.display !== 'none');
if (!focusable.length) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if ($event.shiftKey && document.activeElement === first) { $event.preventDefault(); last.focus(); }
else if (!$event.shiftKey && document.activeElement === last) { $event.preventDefault(); first.focus(); }
">
{# Backdrop #}
<div class="fixed inset-0 bg-black/40"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="showModal = false"></div>
{# Panel #}
<div class="relative bg-surface-50 dark:bg-surface-800 rounded-xl shadow-xl w-full max-w-sm p-6"
role="dialog" aria-modal="true" aria-labelledby="fediverse-modal-title"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
@click.stop>
<h3 id="fediverse-modal-title" class="text-lg font-semibold text-surface-900 dark:text-surface-100 mb-1">{{ modalTitle }}</h3>
<p class="text-sm text-surface-600 dark:text-surface-400 mb-4">{{ modalDescription }}</p>
{# Saved domains list #}
<template x-if="savedDomains.length > 0 && !showInput">
<div>
<div class="flex flex-col gap-2 mb-3">
<template x-for="item in savedDomains" :key="item.domain">
<div class="flex items-center gap-2 rounded-lg bg-surface-50 dark:bg-surface-800 hover:bg-surface-100 dark:hover:bg-surface-600 transition-colors">
<button class="flex-1 px-3 py-2.5 text-left text-sm font-medium text-surface-900 dark:text-surface-100 cursor-pointer"
@click="useSaved(item.domain)"
x-text="item.domain"></button>
<button class="px-2 py-2.5 text-surface-400 hover:text-red-500 transition-colors cursor-pointer"
@click="deleteSaved(item.domain)"
:aria-label="'Remove ' + item.domain">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
</template>
</div>
<button class="w-full text-sm text-[#a730b8] hover:text-[#a730b8]/80 cursor-pointer font-medium"
@click="showAddNew()">Use a different instance</button>
<div class="flex mt-3">
<button @click="showModal = false"
class="w-full px-4 py-2 text-sm font-medium text-surface-600 dark:text-surface-300 bg-surface-100 dark:bg-surface-800 hover:bg-surface-200 dark:hover:bg-surface-600 rounded-lg transition-colors">
Cancel
</button>
</div>
</div>
</template>
{# New domain input #}
<template x-if="savedDomains.length === 0 || showInput">
<div>
<label for="fediverse-instance-input" class="sr-only">Fediverse instance domain</label>
<input id="fediverse-instance-input"
x-ref="instanceInput"
x-model="instance"
@keydown.enter.prevent="confirm()"
type="text"
placeholder="mastodon.social"
class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-surface-50 dark:bg-surface-800 text-surface-900 dark:text-surface-100 placeholder-surface-400 text-sm">
<template x-if="error">
<p class="text-xs text-red-500 mt-1" x-text="error" role="alert"></p>
</template>
<div class="flex gap-3 mt-4">
<button @click="showInput ? (showInput = false) : (showModal = false)"
class="flex-1 px-4 py-2 text-sm font-medium text-surface-600 dark:text-surface-300 bg-surface-100 dark:bg-surface-800 hover:bg-surface-200 dark:hover:bg-surface-600 rounded-lg transition-colors"
x-text="showInput && savedDomains.length > 0 ? 'Back' : 'Cancel'">
</button>
<button @click="confirm()"
class="flex-1 px-4 py-2 text-sm font-medium text-white bg-[#a730b8] hover:bg-[#a730b8]/80 rounded-lg transition-colors">
Go
</button>
</div>
</div>
</template>
</div>
</div>
</template>