feat: Add alt text editor, improve accessibility

This commit is contained in:
Jesse Wierzbinski 2024-06-05 17:48:12 -10:00
parent ef4a2aa0c2
commit a643e3f8aa
No known key found for this signature in database
4 changed files with 83 additions and 19 deletions

View file

@ -1,8 +1,7 @@
<template>
<div v-bind="$props" class="bg-dark-700 overflow-hidden flex items-center justify-center">
<Skeleton :enabled="!src" class="!h-full !w-full">
<img class="cursor-pointer bg-dark-700 ring-1 w-full h-full object-cover" :src="src" :alt="alt"
:title="alt" />
<img class="cursor-pointer bg-dark-700 ring-1 w-full h-full object-cover" :src="src" :alt="alt" />
</Skeleton>
</div>
</template>

View file

@ -1,5 +1,5 @@
<template>
<div v-if="respondingTo" class="mb-4">
<div v-if="respondingTo" class="mb-4" role="region" aria-label="Responding to">
<OverlayScrollbarsComponent :defer="true" class="max-h-72 overflow-y-auto">
<LazySocialElementsNotesNote :note="respondingTo" :small="true" :disabled="true"
class="!rounded-none !bg-pink-500/10" />
@ -7,11 +7,12 @@
</div>
<div class="px-6 pb-4 pt-5">
<div class="pb-2 relative">
<textarea :disabled="submitting" ref="textarea" v-model="content" :placeholder="chosenSplash"
<textarea :disabled="loading" ref="textarea" v-model="content" :placeholder="chosenSplash"
@paste="handlePaste"
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>
<div
:class="['absolute bottom-0 right-0 p-2 text-gray-400 font-semibold text-xs', remainingCharacters < 0 && 'text-red-500']">
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"
aria-label="Compose your message"></textarea>
<div :class="['absolute bottom-0 right-0 p-2 text-gray-400 font-semibold text-xs', remainingCharacters < 0 && 'text-red-500']"
aria-live="polite">
{{ remainingCharacters }}
</div>
<ComposerEmojiSuggestbox :currently-typing-emoji="currentlyBeingTypedEmoji"
@ -20,7 +21,8 @@
<!-- Content warning textbox -->
<div v-if="cw" class="mb-4">
<input type="text" v-model="cwContent" placeholder="Add a content warning"
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" />
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"
aria-label="Content warning" />
</div>
<ComposerFileUploader v-model:files="files" ref="uploader" />
<div class="flex flex-row gap-1 border-white/20">
@ -43,7 +45,7 @@
<ComposerButton title="Add content warning" @click="cw = !cw" :toggled="cw">
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:rating-18-plus" aria-hidden="true" />
</ComposerButton>
<ButtonsPrimary :loading="submitting" @click="send" class="ml-auto rounded-full">
<ButtonsPrimary :loading="loading" @click="send" class="ml-auto rounded-full">
<span>Send!</span>
</ButtonsPrimary>
</div>
@ -99,6 +101,7 @@ const files = ref<
file: File;
progress: number;
api_id?: string;
alt_text?: string;
}[]
>([]);
@ -126,6 +129,21 @@ watch(Control_Alt, () => {
chosenSplash.value = splashes[Math.floor(Math.random() * splashes.length)];
});
watch(
files,
(newFiles) => {
// If a file is uploading, set loading to true
if (newFiles.some((file) => file.progress < 1)) {
loading.value = true;
} else {
loading.value = false;
}
},
{
deep: true,
},
);
onMounted(() => {
useListen("composer:reply", (note: Status) => {
respondingTo.value = note;
@ -154,12 +172,12 @@ const props = defineProps<{
instance: Instance;
}>();
const submitting = ref(false);
const loading = ref(false);
const tokenData = useTokenData();
const client = useMegalodon(tokenData);
const send = async () => {
submitting.value = true;
loading.value = true;
fetch(new URL("/api/v1/statuses", client.value?.baseUrl ?? "").toString(), {
method: "POST",
@ -191,7 +209,7 @@ const send = async () => {
}
content.value = "";
submitting.value = false;
loading.value = false;
useEvent("composer:send", await res.json());
})
.finally(() => {

View file

@ -15,23 +15,47 @@
<template v-else>
<iconify-icon :icon="getIcon(data.file.type)" width="none" class="size-6" />
</template>
<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"
<!-- Shadow on media to better see buttons -->
<div class="absolute inset-0 bg-black/70"></div>
<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"
aria-label="File size">
{{ formatBytes(data.file.size) }}
<!-- Loader spinner -->
<iconify-icon v-if="data.progress < 1.0" icon="tabler:loader-2" width="none"
class="size-4 animate-spin text-pink-500" />
</div>
<button class="absolute top-1 right-1 p-1 bg-dark-800/50 text-white text-xs rounded size-6"
role="button" tabindex="0" @pointerup="removeFile(data.id)" @keydown.enter="removeFile(data.id)">
<button class="absolute top-1 right-1 p-1 bg-dark-800 text-white text-xs rounded size-6" role="button"
tabindex="0" @pointerup="removeFile(data.id)" @keydown.enter="removeFile(data.id)">
<iconify-icon icon="tabler:x" width="none" class="size-4" />
</button>
<!-- Alt text editor -->
<Popover.Root :positioning="{
strategy: 'fixed',
}" v-if="data.api_id" @update:open="o => !o && updateAltText(data.id, data.alt_text)">
<Popover.Trigger aria-hidden="true"
class="absolute top-1 left-1 p-1 bg-dark-800 ring-1 ring-white/5 text-white text-xs rounded size-6">
<iconify-icon icon="tabler:alt" width="none" class="size-4" />
</Popover.Trigger>
<Popover.Positioner class="!z-[100]">
<Popover.Content
class="p-1 bg-dark-400 rounded text-sm ring-1 ring-white/10 shadow-lg text-gray-300 !min-w-72">
<textarea :disabled="data.progress < 1.0" @keydown.enter.stop v-model="data.alt_text"
placeholder="Add alt text"
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" />
<ButtonsSecondary @click="updateAltText(data.id, data.alt_text)" class="w-full"
:loading="data.progress < 1.0">
<span>Edit</span>
</ButtonsSecondary>
</Popover.Content>
</Popover.Positioner>
</Popover.Root>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { Popover } from "@ark-ui/vue";
import { nanoid } from "nanoid";
const files = defineModel<
@ -43,6 +67,7 @@ const files = defineModel<
// 1.0 -> Uploaded
progress: number;
api_id?: string;
alt_text?: string;
}[]
>("files", {
required: true,
@ -88,6 +113,30 @@ watch(
},
);
const updateAltText = (id: string, altText?: string) => {
// Set loading
files.value = files.value.map((data) => {
if (data.id === id) {
return { ...data, progress: 0.5 };
}
return data;
});
client.value
?.updateMedia(
files.value.find((data) => data.id === id)?.api_id as string,
{ description: altText },
)
.then(() => {
files.value = files.value.map((data) => {
if (data.id === id) {
return { ...data, progress: 1.0 };
}
return data;
});
});
};
const getIcon = (mimeType: string) => {
if (mimeType.startsWith("image/")) return "tabler:photo";
if (mimeType.startsWith("video/")) return "tabler:video";

View file

@ -12,14 +12,12 @@
<Popover.Root :positioning="{
strategy: 'fixed',
}" v-if="attachment.description">
<Popover.Trigger
<Popover.Trigger aria-hidden="true"
class="absolute top-2 right-2 p-1 bg-dark-800 ring-1 ring-white/5 text-white text-xs rounded size-8">
<iconify-icon icon="tabler:alt" width="none" class="size-6" />
</Popover.Trigger>
<Popover.Positioner>
<Popover.Content class="p-4 bg-dark-400 rounded text-sm ring-1 ring-white/10 shadow-lg text-gray-300">
<Popover.Title class="font-semibold mb-2">
Description</Popover.Title>
<Popover.Content class="p-4 bg-dark-400 rounded text-sm ring-1 ring-dark-100 shadow-lg text-gray-300">
<Popover.Description>{{ attachment.description }}</Popover.Description>
</Popover.Content>
</Popover.Positioner>