Skip to content

Commit a643e3f

Browse files
committed
feat: ✨ Add alt text editor, improve accessibility
1 parent ef4a2aa commit a643e3f

File tree

4 files changed

+83
-19
lines changed

4 files changed

+83
-19
lines changed

components/avatars/Centered.vue

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
<template>
22
<div v-bind="$props" class="bg-dark-700 overflow-hidden flex items-center justify-center">
33
<Skeleton :enabled="!src" class="!h-full !w-full">
4-
<img class="cursor-pointer bg-dark-700 ring-1 w-full h-full object-cover" :src="src" :alt="alt"
5-
:title="alt" />
4+
<img class="cursor-pointer bg-dark-700 ring-1 w-full h-full object-cover" :src="src" :alt="alt" />
65
</Skeleton>
76
</div>
87
</template>

components/composer/composer.vue

+28-10
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
<template>
2-
<div v-if="respondingTo" class="mb-4">
2+
<div v-if="respondingTo" class="mb-4" role="region" aria-label="Responding to">
33
<OverlayScrollbarsComponent :defer="true" class="max-h-72 overflow-y-auto">
44
<LazySocialElementsNotesNote :note="respondingTo" :small="true" :disabled="true"
55
class="!rounded-none !bg-pink-500/10" />
66
</OverlayScrollbarsComponent>
77
</div>
88
<div class="px-6 pb-4 pt-5">
99
<div class="pb-2 relative">
10-
<textarea :disabled="submitting" ref="textarea" v-model="content" :placeholder="chosenSplash"
10+
<textarea :disabled="loading" ref="textarea" v-model="content" :placeholder="chosenSplash"
1111
@paste="handlePaste"
12-
class="resize-none min-h-48 prose prose-invert max-h-[70dvh] w-full p-0 focus:!ring-0 !ring-none !border-none !outline-none placeholder:text-zinc-500 bg-transparent appearance-none focus:!border-none focus:!outline-none disabled:cursor-not-allowed"></textarea>
13-
<div
14-
:class="['absolute bottom-0 right-0 p-2 text-gray-400 font-semibold text-xs', remainingCharacters < 0 && 'text-red-500']">
12+
class="resize-none min-h-48 prose prose-invert max-h-[70dvh] w-full p-0 focus:!ring-0 !ring-none !border-none !outline-none placeholder:text-zinc-500 bg-transparent appearance-none focus:!border-none focus:!outline-none disabled:cursor-not-allowed"
13+
aria-label="Compose your message"></textarea>
14+
<div :class="['absolute bottom-0 right-0 p-2 text-gray-400 font-semibold text-xs', remainingCharacters < 0 && 'text-red-500']"
15+
aria-live="polite">
1516
{{ remainingCharacters }}
1617
</div>
1718
<ComposerEmojiSuggestbox :currently-typing-emoji="currentlyBeingTypedEmoji"
@@ -20,7 +21,8 @@
2021
<!-- Content warning textbox -->
2122
<div v-if="cw" class="mb-4">
2223
<input type="text" v-model="cwContent" placeholder="Add a content warning"
23-
class="w-full p-2 mt-1 text-sm prose prose-invert bg-dark-900 rounded focus:!ring-0 !ring-none !border-none !outline-none placeholder:text-zinc-500 appearance-none focus:!border-none focus:!outline-none" />
24+
class="w-full p-2 mt-1 text-sm prose prose-invert bg-dark-900 rounded focus:!ring-0 !ring-none !border-none !outline-none placeholder:text-zinc-500 appearance-none focus:!border-none focus:!outline-none"
25+
aria-label="Content warning" />
2426
</div>
2527
<ComposerFileUploader v-model:files="files" ref="uploader" />
2628
<div class="flex flex-row gap-1 border-white/20">
@@ -43,7 +45,7 @@
4345
<ComposerButton title="Add content warning" @click="cw = !cw" :toggled="cw">
4446
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:rating-18-plus" aria-hidden="true" />
4547
</ComposerButton>
46-
<ButtonsPrimary :loading="submitting" @click="send" class="ml-auto rounded-full">
48+
<ButtonsPrimary :loading="loading" @click="send" class="ml-auto rounded-full">
4749
<span>Send!</span>
4850
</ButtonsPrimary>
4951
</div>
@@ -99,6 +101,7 @@ const files = ref<
99101
file: File;
100102
progress: number;
101103
api_id?: string;
104+
alt_text?: string;
102105
}[]
103106
>([]);
104107
@@ -126,6 +129,21 @@ watch(Control_Alt, () => {
126129
chosenSplash.value = splashes[Math.floor(Math.random() * splashes.length)];
127130
});
128131
132+
watch(
133+
files,
134+
(newFiles) => {
135+
// If a file is uploading, set loading to true
136+
if (newFiles.some((file) => file.progress < 1)) {
137+
loading.value = true;
138+
} else {
139+
loading.value = false;
140+
}
141+
},
142+
{
143+
deep: true,
144+
},
145+
);
146+
129147
onMounted(() => {
130148
useListen("composer:reply", (note: Status) => {
131149
respondingTo.value = note;
@@ -154,12 +172,12 @@ const props = defineProps<{
154172
instance: Instance;
155173
}>();
156174
157-
const submitting = ref(false);
175+
const loading = ref(false);
158176
const tokenData = useTokenData();
159177
const client = useMegalodon(tokenData);
160178
161179
const send = async () => {
162-
submitting.value = true;
180+
loading.value = true;
163181
164182
fetch(new URL("/api/v1/statuses", client.value?.baseUrl ?? "").toString(), {
165183
method: "POST",
@@ -191,7 +209,7 @@ const send = async () => {
191209
}
192210
193211
content.value = "";
194-
submitting.value = false;
212+
loading.value = false;
195213
useEvent("composer:send", await res.json());
196214
})
197215
.finally(() => {

components/composer/file-uploader.vue

+52-3
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,47 @@
1515
<template v-else>
1616
<iconify-icon :icon="getIcon(data.file.type)" width="none" class="size-6" />
1717
</template>
18-
<div class="absolute bottom-1 right-1 p-1 bg-dark-800/70 text-white text-xs rounded cursor-default flex flex-row items-center gap-x-1"
18+
<!-- Shadow on media to better see buttons -->
19+
<div class="absolute inset-0 bg-black/70"></div>
20+
<div class="absolute bottom-1 right-1 p-1 bg-dark-800 text-white text-xs rounded cursor-default flex flex-row items-center gap-x-1"
1921
aria-label="File size">
2022
{{ formatBytes(data.file.size) }}
2123
<!-- Loader spinner -->
2224
<iconify-icon v-if="data.progress < 1.0" icon="tabler:loader-2" width="none"
2325
class="size-4 animate-spin text-pink-500" />
2426
</div>
25-
<button class="absolute top-1 right-1 p-1 bg-dark-800/50 text-white text-xs rounded size-6"
26-
role="button" tabindex="0" @pointerup="removeFile(data.id)" @keydown.enter="removeFile(data.id)">
27+
<button class="absolute top-1 right-1 p-1 bg-dark-800 text-white text-xs rounded size-6" role="button"
28+
tabindex="0" @pointerup="removeFile(data.id)" @keydown.enter="removeFile(data.id)">
2729
<iconify-icon icon="tabler:x" width="none" class="size-4" />
2830
</button>
31+
<!-- Alt text editor -->
32+
<Popover.Root :positioning="{
33+
strategy: 'fixed',
34+
}" v-if="data.api_id" @update:open="o => !o && updateAltText(data.id, data.alt_text)">
35+
<Popover.Trigger aria-hidden="true"
36+
class="absolute top-1 left-1 p-1 bg-dark-800 ring-1 ring-white/5 text-white text-xs rounded size-6">
37+
<iconify-icon icon="tabler:alt" width="none" class="size-4" />
38+
</Popover.Trigger>
39+
<Popover.Positioner class="!z-[100]">
40+
<Popover.Content
41+
class="p-1 bg-dark-400 rounded text-sm ring-1 ring-white/10 shadow-lg text-gray-300 !min-w-72">
42+
<textarea :disabled="data.progress < 1.0" @keydown.enter.stop v-model="data.alt_text"
43+
placeholder="Add alt text"
44+
class="w-full p-2 text-sm prose prose-invert bg-dark-900 rounded focus:!ring-0 !ring-none !border-none !outline-none placeholder:text-zinc-500 appearance-none focus:!border-none focus:!outline-none" />
45+
<ButtonsSecondary @click="updateAltText(data.id, data.alt_text)" class="w-full"
46+
:loading="data.progress < 1.0">
47+
<span>Edit</span>
48+
</ButtonsSecondary>
49+
</Popover.Content>
50+
</Popover.Positioner>
51+
</Popover.Root>
2952
</div>
3053
</div>
3154
</div>
3255
</template>
3356

3457
<script lang="ts" setup>
58+
import { Popover } from "@ark-ui/vue";
3559
import { nanoid } from "nanoid";
3660
3761
const files = defineModel<
@@ -43,6 +67,7 @@ const files = defineModel<
4367
// 1.0 -> Uploaded
4468
progress: number;
4569
api_id?: string;
70+
alt_text?: string;
4671
}[]
4772
>("files", {
4873
required: true,
@@ -88,6 +113,30 @@ watch(
88113
},
89114
);
90115
116+
const updateAltText = (id: string, altText?: string) => {
117+
// Set loading
118+
files.value = files.value.map((data) => {
119+
if (data.id === id) {
120+
return { ...data, progress: 0.5 };
121+
}
122+
return data;
123+
});
124+
125+
client.value
126+
?.updateMedia(
127+
files.value.find((data) => data.id === id)?.api_id as string,
128+
{ description: altText },
129+
)
130+
.then(() => {
131+
files.value = files.value.map((data) => {
132+
if (data.id === id) {
133+
return { ...data, progress: 1.0 };
134+
}
135+
return data;
136+
});
137+
});
138+
};
139+
91140
const getIcon = (mimeType: string) => {
92141
if (mimeType.startsWith("image/")) return "tabler:photo";
93142
if (mimeType.startsWith("video/")) return "tabler:video";

components/social-elements/notes/attachment.vue

+2-4
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,12 @@
1212
<Popover.Root :positioning="{
1313
strategy: 'fixed',
1414
}" v-if="attachment.description">
15-
<Popover.Trigger
15+
<Popover.Trigger aria-hidden="true"
1616
class="absolute top-2 right-2 p-1 bg-dark-800 ring-1 ring-white/5 text-white text-xs rounded size-8">
1717
<iconify-icon icon="tabler:alt" width="none" class="size-6" />
1818
</Popover.Trigger>
1919
<Popover.Positioner>
20-
<Popover.Content class="p-4 bg-dark-400 rounded text-sm ring-1 ring-white/10 shadow-lg text-gray-300">
21-
<Popover.Title class="font-semibold mb-2">
22-
Description</Popover.Title>
20+
<Popover.Content class="p-4 bg-dark-400 rounded text-sm ring-1 ring-dark-100 shadow-lg text-gray-300">
2321
<Popover.Description>{{ attachment.description }}</Popover.Description>
2422
</Popover.Content>
2523
</Popover.Positioner>

0 commit comments

Comments
 (0)