mirror of
https://github.com/versia-pub/frontend.git
synced 2025-12-06 08:28:20 +01:00
feat: ✨ Add alt text editor, improve accessibility
This commit is contained in:
parent
ef4a2aa0c2
commit
a643e3f8aa
|
|
@ -1,8 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-bind="$props" class="bg-dark-700 overflow-hidden flex items-center justify-center">
|
<div v-bind="$props" class="bg-dark-700 overflow-hidden flex items-center justify-center">
|
||||||
<Skeleton :enabled="!src" class="!h-full !w-full">
|
<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"
|
<img class="cursor-pointer bg-dark-700 ring-1 w-full h-full object-cover" :src="src" :alt="alt" />
|
||||||
:title="alt" />
|
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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">
|
<OverlayScrollbarsComponent :defer="true" class="max-h-72 overflow-y-auto">
|
||||||
<LazySocialElementsNotesNote :note="respondingTo" :small="true" :disabled="true"
|
<LazySocialElementsNotesNote :note="respondingTo" :small="true" :disabled="true"
|
||||||
class="!rounded-none !bg-pink-500/10" />
|
class="!rounded-none !bg-pink-500/10" />
|
||||||
|
|
@ -7,11 +7,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="px-6 pb-4 pt-5">
|
<div class="px-6 pb-4 pt-5">
|
||||||
<div class="pb-2 relative">
|
<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"
|
@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>
|
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"
|
||||||
<div
|
aria-label="Compose your message"></textarea>
|
||||||
:class="['absolute bottom-0 right-0 p-2 text-gray-400 font-semibold text-xs', remainingCharacters < 0 && 'text-red-500']">
|
<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 }}
|
{{ remainingCharacters }}
|
||||||
</div>
|
</div>
|
||||||
<ComposerEmojiSuggestbox :currently-typing-emoji="currentlyBeingTypedEmoji"
|
<ComposerEmojiSuggestbox :currently-typing-emoji="currentlyBeingTypedEmoji"
|
||||||
|
|
@ -20,7 +21,8 @@
|
||||||
<!-- Content warning textbox -->
|
<!-- Content warning textbox -->
|
||||||
<div v-if="cw" class="mb-4">
|
<div v-if="cw" class="mb-4">
|
||||||
<input type="text" v-model="cwContent" placeholder="Add a content warning"
|
<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>
|
</div>
|
||||||
<ComposerFileUploader v-model:files="files" ref="uploader" />
|
<ComposerFileUploader v-model:files="files" ref="uploader" />
|
||||||
<div class="flex flex-row gap-1 border-white/20">
|
<div class="flex flex-row gap-1 border-white/20">
|
||||||
|
|
@ -43,7 +45,7 @@
|
||||||
<ComposerButton title="Add content warning" @click="cw = !cw" :toggled="cw">
|
<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" />
|
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:rating-18-plus" aria-hidden="true" />
|
||||||
</ComposerButton>
|
</ComposerButton>
|
||||||
<ButtonsPrimary :loading="submitting" @click="send" class="ml-auto rounded-full">
|
<ButtonsPrimary :loading="loading" @click="send" class="ml-auto rounded-full">
|
||||||
<span>Send!</span>
|
<span>Send!</span>
|
||||||
</ButtonsPrimary>
|
</ButtonsPrimary>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -99,6 +101,7 @@ const files = ref<
|
||||||
file: File;
|
file: File;
|
||||||
progress: number;
|
progress: number;
|
||||||
api_id?: string;
|
api_id?: string;
|
||||||
|
alt_text?: string;
|
||||||
}[]
|
}[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
|
|
@ -126,6 +129,21 @@ watch(Control_Alt, () => {
|
||||||
chosenSplash.value = splashes[Math.floor(Math.random() * splashes.length)];
|
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(() => {
|
onMounted(() => {
|
||||||
useListen("composer:reply", (note: Status) => {
|
useListen("composer:reply", (note: Status) => {
|
||||||
respondingTo.value = note;
|
respondingTo.value = note;
|
||||||
|
|
@ -154,12 +172,12 @@ const props = defineProps<{
|
||||||
instance: Instance;
|
instance: Instance;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const submitting = ref(false);
|
const loading = ref(false);
|
||||||
const tokenData = useTokenData();
|
const tokenData = useTokenData();
|
||||||
const client = useMegalodon(tokenData);
|
const client = useMegalodon(tokenData);
|
||||||
|
|
||||||
const send = async () => {
|
const send = async () => {
|
||||||
submitting.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
fetch(new URL("/api/v1/statuses", client.value?.baseUrl ?? "").toString(), {
|
fetch(new URL("/api/v1/statuses", client.value?.baseUrl ?? "").toString(), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -191,7 +209,7 @@ const send = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
content.value = "";
|
content.value = "";
|
||||||
submitting.value = false;
|
loading.value = false;
|
||||||
useEvent("composer:send", await res.json());
|
useEvent("composer:send", await res.json());
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
|
||||||
|
|
@ -15,23 +15,47 @@
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<iconify-icon :icon="getIcon(data.file.type)" width="none" class="size-6" />
|
<iconify-icon :icon="getIcon(data.file.type)" width="none" class="size-6" />
|
||||||
</template>
|
</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">
|
aria-label="File size">
|
||||||
{{ formatBytes(data.file.size) }}
|
{{ formatBytes(data.file.size) }}
|
||||||
<!-- Loader spinner -->
|
<!-- Loader spinner -->
|
||||||
<iconify-icon v-if="data.progress < 1.0" icon="tabler:loader-2" width="none"
|
<iconify-icon v-if="data.progress < 1.0" icon="tabler:loader-2" width="none"
|
||||||
class="size-4 animate-spin text-pink-500" />
|
class="size-4 animate-spin text-pink-500" />
|
||||||
</div>
|
</div>
|
||||||
<button class="absolute top-1 right-1 p-1 bg-dark-800/50 text-white text-xs rounded size-6"
|
<button class="absolute top-1 right-1 p-1 bg-dark-800 text-white text-xs rounded size-6" role="button"
|
||||||
role="button" tabindex="0" @pointerup="removeFile(data.id)" @keydown.enter="removeFile(data.id)">
|
tabindex="0" @pointerup="removeFile(data.id)" @keydown.enter="removeFile(data.id)">
|
||||||
<iconify-icon icon="tabler:x" width="none" class="size-4" />
|
<iconify-icon icon="tabler:x" width="none" class="size-4" />
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { Popover } from "@ark-ui/vue";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
const files = defineModel<
|
const files = defineModel<
|
||||||
|
|
@ -43,6 +67,7 @@ const files = defineModel<
|
||||||
// 1.0 -> Uploaded
|
// 1.0 -> Uploaded
|
||||||
progress: number;
|
progress: number;
|
||||||
api_id?: string;
|
api_id?: string;
|
||||||
|
alt_text?: string;
|
||||||
}[]
|
}[]
|
||||||
>("files", {
|
>("files", {
|
||||||
required: true,
|
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) => {
|
const getIcon = (mimeType: string) => {
|
||||||
if (mimeType.startsWith("image/")) return "tabler:photo";
|
if (mimeType.startsWith("image/")) return "tabler:photo";
|
||||||
if (mimeType.startsWith("video/")) return "tabler:video";
|
if (mimeType.startsWith("video/")) return "tabler:video";
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,12 @@
|
||||||
<Popover.Root :positioning="{
|
<Popover.Root :positioning="{
|
||||||
strategy: 'fixed',
|
strategy: 'fixed',
|
||||||
}" v-if="attachment.description">
|
}" 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">
|
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" />
|
<iconify-icon icon="tabler:alt" width="none" class="size-6" />
|
||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
<Popover.Positioner>
|
<Popover.Positioner>
|
||||||
<Popover.Content class="p-4 bg-dark-400 rounded text-sm ring-1 ring-white/10 shadow-lg text-gray-300">
|
<Popover.Content class="p-4 bg-dark-400 rounded text-sm ring-1 ring-dark-100 shadow-lg text-gray-300">
|
||||||
<Popover.Title class="font-semibold mb-2">
|
|
||||||
Description</Popover.Title>
|
|
||||||
<Popover.Description>{{ attachment.description }}</Popover.Description>
|
<Popover.Description>{{ attachment.description }}</Popover.Description>
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
</Popover.Positioner>
|
</Popover.Positioner>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue