mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:01:08 +02:00
fix(auth-ui): add focus traps to modals + aria-disabled on all buttons
- Add focus trap (Tab/Shift+Tab cycling) to AuthGateModal and GuestWelcomeModal with auto-focus on primary action - Add aria-disabled to all disabled buttons across 8 components for proper screen reader announcements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
201819280e
commit
e5c63f65fb
8 changed files with 98 additions and 0 deletions
|
|
@ -159,6 +159,45 @@
|
|||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
function trapFocus(node: HTMLElement) {
|
||||
const focusableSelectors =
|
||||
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key !== 'Tab') return;
|
||||
|
||||
const focusable = Array.from(node.querySelectorAll(focusableSelectors)) as HTMLElement[];
|
||||
if (focusable.length === 0) return;
|
||||
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
|
||||
if (e.shiftKey && document.activeElement === first) {
|
||||
e.preventDefault();
|
||||
last.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === last) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
}
|
||||
|
||||
node.addEventListener('keydown', handleKeydown);
|
||||
// Auto-focus the primary (login) button
|
||||
const focusable = node.querySelectorAll(focusableSelectors) as NodeListOf<HTMLElement>;
|
||||
// Skip the close button (index 0), focus the login button (index 1)
|
||||
if (focusable.length > 1) {
|
||||
focusable[1].focus();
|
||||
} else if (focusable.length > 0) {
|
||||
focusable[0].focus();
|
||||
}
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener('keydown', handleKeydown);
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
|
@ -175,6 +214,7 @@
|
|||
aria-modal="true"
|
||||
aria-labelledby="auth-gate-title"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
use:trapFocus
|
||||
>
|
||||
<!-- Close button -->
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@
|
|||
<button
|
||||
type="submit"
|
||||
disabled={!canSubmit}
|
||||
aria-disabled={!canSubmit}
|
||||
class="submit-button"
|
||||
style:background-color={primaryColor + '60'}
|
||||
style:border-color={primaryColor}
|
||||
|
|
|
|||
|
|
@ -164,6 +164,44 @@
|
|||
handleContinueAsGuest();
|
||||
}
|
||||
}
|
||||
|
||||
function trapFocus(node: HTMLElement) {
|
||||
const focusableSelectors =
|
||||
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key !== 'Tab') return;
|
||||
|
||||
const focusable = Array.from(node.querySelectorAll(focusableSelectors)) as HTMLElement[];
|
||||
if (focusable.length === 0) return;
|
||||
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
|
||||
if (e.shiftKey && document.activeElement === first) {
|
||||
e.preventDefault();
|
||||
last.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === last) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
}
|
||||
|
||||
node.addEventListener('keydown', handleKeydown);
|
||||
// Auto-focus the primary (login) button - skip close button (index 0)
|
||||
const focusable = node.querySelectorAll(focusableSelectors) as NodeListOf<HTMLElement>;
|
||||
if (focusable.length > 1) {
|
||||
focusable[1].focus();
|
||||
} else if (focusable.length > 0) {
|
||||
focusable[0].focus();
|
||||
}
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener('keydown', handleKeydown);
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
|
@ -186,6 +224,7 @@
|
|||
class="modal-content"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
use:trapFocus
|
||||
>
|
||||
<!-- Close Button -->
|
||||
<button type="button" class="close-button" onclick={handleContinueAsGuest} aria-label="Close">
|
||||
|
|
|
|||
|
|
@ -242,6 +242,7 @@
|
|||
class="pm-btn pm-btn-cancel"
|
||||
onclick={cancelDelete}
|
||||
disabled={loading}
|
||||
aria-disabled={loading}
|
||||
>
|
||||
{t.cancelButton}
|
||||
</button>
|
||||
|
|
@ -250,6 +251,7 @@
|
|||
class="pm-btn pm-btn-danger"
|
||||
onclick={executeDelete}
|
||||
disabled={loading}
|
||||
aria-disabled={loading}
|
||||
>
|
||||
{#if loading}
|
||||
<svg
|
||||
|
|
@ -287,6 +289,7 @@
|
|||
class="pm-btn pm-btn-cancel"
|
||||
onclick={cancelRename}
|
||||
disabled={loading}
|
||||
aria-disabled={loading}
|
||||
>
|
||||
{t.cancelButton}
|
||||
</button>
|
||||
|
|
@ -295,6 +298,7 @@
|
|||
class="pm-btn pm-btn-primary"
|
||||
onclick={() => saveRename(passkey.id)}
|
||||
disabled={loading || !editName.trim()}
|
||||
aria-disabled={loading || !editName.trim()}
|
||||
>
|
||||
{t.saveButton}
|
||||
</button>
|
||||
|
|
@ -363,6 +367,7 @@
|
|||
class="pm-btn pm-btn-cancel"
|
||||
onclick={cancelRegister}
|
||||
disabled={loading}
|
||||
aria-disabled={loading}
|
||||
>
|
||||
{t.cancelButton}
|
||||
</button>
|
||||
|
|
@ -371,6 +376,7 @@
|
|||
class="pm-btn pm-btn-primary"
|
||||
onclick={handleRegister}
|
||||
disabled={loading}
|
||||
aria-disabled={loading}
|
||||
>
|
||||
{#if loading}
|
||||
<svg
|
||||
|
|
@ -394,6 +400,7 @@
|
|||
class="pm-btn pm-btn-register"
|
||||
onclick={openRegisterForm}
|
||||
disabled={loading}
|
||||
aria-disabled={loading}
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
|
|
|
|||
|
|
@ -202,6 +202,7 @@
|
|||
class="refresh-button"
|
||||
onclick={onRefresh}
|
||||
disabled={loading}
|
||||
aria-disabled={loading}
|
||||
aria-label={t.refresh}
|
||||
>
|
||||
<svg
|
||||
|
|
@ -289,6 +290,7 @@
|
|||
class="revoke-button"
|
||||
onclick={() => handleRevoke(session.id)}
|
||||
disabled={revoking === session.id || revokingAll}
|
||||
aria-disabled={revoking === session.id || revokingAll}
|
||||
>
|
||||
{#if revoking === session.id}
|
||||
<span class="revoke-spinner"></span>
|
||||
|
|
@ -307,6 +309,7 @@
|
|||
class="revoke-all-button"
|
||||
onclick={handleRevokeAll}
|
||||
disabled={revokingAll}
|
||||
aria-disabled={revokingAll}
|
||||
>
|
||||
{#if revokingAll}
|
||||
<span class="revoke-spinner"></span>
|
||||
|
|
|
|||
|
|
@ -250,6 +250,7 @@
|
|||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
aria-disabled={loading}
|
||||
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style:background-color={primaryColor + '60'}
|
||||
style:border-color={primaryColor}
|
||||
|
|
|
|||
|
|
@ -561,6 +561,7 @@
|
|||
<button
|
||||
type="submit"
|
||||
disabled={loading || !twoFactorCode}
|
||||
aria-disabled={loading || !twoFactorCode}
|
||||
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style:background-color={primaryColor + '60'}
|
||||
style:border-color={primaryColor}
|
||||
|
|
@ -636,6 +637,7 @@
|
|||
type="button"
|
||||
onclick={handlePasskeySignIn}
|
||||
disabled={loading || showSuccess}
|
||||
aria-disabled={loading || showSuccess}
|
||||
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity bg-transparent hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style:border-color={primaryColor}
|
||||
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
|
||||
|
|
@ -726,6 +728,7 @@
|
|||
class="bg-transparent border-none cursor-pointer font-medium text-sm p-0 text-left underline hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onclick={handleResendVerification}
|
||||
disabled={resendingVerification}
|
||||
aria-disabled={resendingVerification}
|
||||
style:color={primaryColor}
|
||||
>
|
||||
{resendingVerification ? t.resendingVerification : t.resendVerification}
|
||||
|
|
@ -842,6 +845,7 @@
|
|||
<button
|
||||
type="submit"
|
||||
disabled={loading || showSuccess || rateLimitCountdown > 0}
|
||||
aria-disabled={loading || showSuccess || rateLimitCountdown > 0}
|
||||
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style:background-color={showSuccess ? '#22c55e' : primaryColor + '60'}
|
||||
style:border-color={showSuccess ? '#22c55e' : primaryColor}
|
||||
|
|
@ -892,6 +896,7 @@
|
|||
type="button"
|
||||
onclick={handleSendMagicLink}
|
||||
disabled={sendingMagicLink || !email}
|
||||
aria-disabled={sendingMagicLink || !email}
|
||||
class="w-full bg-transparent border-none cursor-pointer font-medium text-sm p-3 text-center transition-opacity hover:opacity-70 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
style:color={primaryColor}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -374,6 +374,7 @@
|
|||
type="button"
|
||||
onclick={handleResendVerification}
|
||||
disabled={resendingVerification}
|
||||
aria-disabled={resendingVerification}
|
||||
class="w-full flex items-center justify-center gap-2 h-11 rounded-lg font-medium transition-opacity hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed border-[1.5px]"
|
||||
style:background-color="{primaryColor}40"
|
||||
style:border-color={primaryColor}
|
||||
|
|
@ -504,6 +505,7 @@
|
|||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
aria-disabled={loading}
|
||||
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style:background-color="{primaryColor}60"
|
||||
style:border-color={primaryColor}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue