Skip to content

Commit 8583ba8

Browse files
nulfrostdanielroe
andauthored
refactor(a11y): use <dialog> tag for modal components (#522)
Co-authored-by: Daniel Roe <daniel@roe.dev>
1 parent 4f08f0a commit 8583ba8

15 files changed

Lines changed: 790 additions & 872 deletions

app/app.vue

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,20 @@ function handleGlobalKeyup() {
6868
showKbdHints.value = false
6969
}
7070
71+
/* A hack to get light dismiss to work in safari because it does not support closedby="any" yet */
72+
// https://codepen.io/paramagicdev/pen/gbYompq
73+
// see: https://github.com/npmx-dev/npmx.dev/pull/522#discussion_r2749978022
74+
function handleModalLightDismiss(e: MouseEvent) {
75+
const target = e.target as HTMLElement
76+
if (target.tagName === 'DIALOG' && target.hasAttribute('open')) {
77+
;(target as HTMLDialogElement).close()
78+
}
79+
}
80+
7181
if (import.meta.client) {
7282
useEventListener(document, 'keydown', handleGlobalKeydown)
7383
useEventListener(document, 'keyup', handleGlobalKeyup)
84+
useEventListener(document, 'click', handleModalLightDismiss)
7485
}
7586
</script>
7687

app/assets/main.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,9 @@ input[type='search']::-webkit-search-results-decoration {
233233
animation-duration: 0.3s;
234234
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
235235
}
236+
237+
/* Locking the scroll whenever any of the modals are open */
238+
html:has(dialog:modal) {
239+
overflow: hidden;
240+
scrollbar-gutter: stable;
241+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
<script setup lang="ts">
2+
const handleInput = shallowRef('')
3+
4+
const { user, logout } = useAtproto()
5+
6+
async function handleBlueskySignIn() {
7+
await navigateTo(
8+
{
9+
path: '/api/auth/atproto',
10+
query: { handle: 'https://bsky.social' },
11+
},
12+
{ external: true },
13+
)
14+
}
15+
16+
async function handleCreateAccount() {
17+
await navigateTo(
18+
{
19+
path: '/api/auth/atproto',
20+
query: { handle: 'https://npmx.social', create: 'true' },
21+
},
22+
{ external: true },
23+
)
24+
}
25+
26+
async function handleLogin() {
27+
if (handleInput.value) {
28+
await navigateTo(
29+
{
30+
path: '/api/auth/atproto',
31+
query: { handle: handleInput.value },
32+
},
33+
{ external: true },
34+
)
35+
}
36+
}
37+
</script>
38+
39+
<template>
40+
<!-- Modal -->
41+
<Modal :modalTitle="$t('auth.modal.title')" class="max-w-lg" id="auth-modal">
42+
<div v-if="user?.handle" class="space-y-4">
43+
<div class="flex items-center gap-3 p-4 bg-bg-subtle border border-border rounded-lg">
44+
<span class="w-3 h-3 rounded-full bg-green-500" aria-hidden="true" />
45+
<div>
46+
<p class="font-mono text-xs text-fg-muted">
47+
{{ $t('auth.modal.connected_as', { handle: user.handle }) }}
48+
</p>
49+
</div>
50+
</div>
51+
<button
52+
class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
53+
@click="logout"
54+
>
55+
{{ $t('auth.modal.disconnect') }}
56+
</button>
57+
</div>
58+
59+
<!-- Disconnected state -->
60+
<form v-else class="space-y-4" @submit.prevent="handleLogin">
61+
<p class="text-sm text-fg-muted">{{ $t('auth.modal.connect_prompt') }}</p>
62+
63+
<div class="space-y-3">
64+
<div>
65+
<label
66+
for="handle-input"
67+
class="block text-xs text-fg-subtle uppercase tracking-wider mb-1.5"
68+
>
69+
{{ $t('auth.modal.handle_label') }}
70+
</label>
71+
<input
72+
id="handle-input"
73+
v-model="handleInput"
74+
type="text"
75+
name="handle"
76+
:placeholder="$t('auth.modal.handle_placeholder')"
77+
autocomplete="off"
78+
spellcheck="false"
79+
class="w-full px-3 py-2 font-mono text-sm bg-bg-subtle border border-border rounded-md text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
80+
/>
81+
</div>
82+
83+
<details class="text-sm">
84+
<summary
85+
class="text-fg-subtle cursor-pointer hover:text-fg-muted transition-colors duration-200"
86+
>
87+
{{ $t('auth.modal.what_is_atmosphere') }}
88+
</summary>
89+
<div class="mt-3">
90+
<i18n-t keypath="auth.modal.atmosphere_explanation" tag="p">
91+
<template #npmx>
92+
<span class="font-bold">npmx.dev</span>
93+
</template>
94+
<template #atproto>
95+
<a href="https://atproto.com" target="_blank" class="text-blue-400 hover:underline">
96+
AT Protocol
97+
</a>
98+
</template>
99+
<template #bluesky>
100+
<a href="https://bsky.app" target="_blank" class="text-blue-400 hover:underline">
101+
Bluesky
102+
</a>
103+
</template>
104+
<template #tangled>
105+
<a href="https://tangled.org" target="_blank" class="text-blue-400 hover:underline">
106+
Tangled
107+
</a>
108+
</template>
109+
</i18n-t>
110+
</div>
111+
</details>
112+
</div>
113+
114+
<button
115+
type="submit"
116+
:disabled="!handleInput.trim()"
117+
class="w-full px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-all duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 focus-visible:ring-offset-2 focus-visible:ring-offset-bg"
118+
>
119+
{{ $t('auth.modal.connect') }}
120+
</button>
121+
<button
122+
type="button"
123+
class="w-full px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-all duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 focus-visible:ring-offset-2 focus-visible:ring-offset-bg"
124+
@click="handleCreateAccount"
125+
>
126+
{{ $t('auth.modal.create_account') }}
127+
</button>
128+
<hr />
129+
<button
130+
type="button"
131+
class="w-full px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-all duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 focus-visible:ring-offset-2 focus-visible:ring-offset-bg flex items-center justify-center gap-2"
132+
@click="handleBlueskySignIn"
133+
>
134+
{{ $t('auth.modal.connect_bluesky') }}
135+
<svg fill="none" viewBox="0 0 64 57" width="20" style="width: 20px">
136+
<path
137+
fill="#0F73FF"
138+
d="M13.873 3.805C21.21 9.332 29.103 20.537 32 26.55v15.882c0-.338-.13.044-.41.867-1.512 4.456-7.418 21.847-20.923 7.944-7.111-7.32-3.819-14.64 9.125-16.85-7.405 1.264-15.73-.825-18.014-9.015C1.12 23.022 0 8.51 0 6.55 0-3.268 8.579-.182 13.873 3.805ZM50.127 3.805C42.79 9.332 34.897 20.537 32 26.55v15.882c0-.338.13.044.41.867 1.512 4.456 7.418 21.847 20.923 7.944 7.111-7.32 3.819-14.64-9.125-16.85 7.405 1.264 15.73-.825 18.014-9.015C62.88 23.022 64 8.51 64 6.55c0-9.818-8.578-6.732-13.873-2.745Z"
139+
></path>
140+
</svg>
141+
</button>
142+
</form>
143+
</Modal>
144+
</template>

app/components/AuthModal.vue

Lines changed: 0 additions & 198 deletions
This file was deleted.

0 commit comments

Comments
 (0)