From dac42e062c62ad385be0f7c2f223cdfb6edc378c Mon Sep 17 00:00:00 2001
From: jyn <github@jyn.dev>
Date: Mon, 21 Oct 2024 05:00:51 -0400
Subject: [PATCH] feat(a11y): make menu buttons in 'More' dropdown selectable
 with the keyboard (#2976)

Co-authored-by: userquin <userquin@gmail.com>
---
 components/account/AccountMoreButton.vue    | 14 ++++++++++++
 components/common/dropdown/DropdownItem.vue | 25 ++++++++++++++-------
 components/status/StatusActionsMore.vue     | 21 +++++++++++++++++
 3 files changed, 52 insertions(+), 8 deletions(-)

diff --git a/components/account/AccountMoreButton.vue b/components/account/AccountMoreButton.vue
index 7b2fe672..e7b5d089 100644
--- a/components/account/AccountMoreButton.vue
+++ b/components/account/AccountMoreButton.vue
@@ -71,6 +71,7 @@ async function removeUserNote() {
         />
       </NuxtLink>
       <CommonDropdownItem
+        is="button"
         v-if="isShareSupported"
         :text="$t('menu.share_account', [`@${account.acct}`])"
         icon="i-ri:share-line"
@@ -81,12 +82,14 @@ async function removeUserNote() {
       <template v-if="currentUser">
         <template v-if="!isSelf">
           <CommonDropdownItem
+            is="button"
             :text="$t('menu.mention_account', [`@${account.acct}`])"
             icon="i-ri:at-line"
             :command="command"
             @click="mentionUser(account)"
           />
           <CommonDropdownItem
+            is="button"
             :text="$t('menu.direct_message_account', [`@${account.acct}`])"
             icon="i-ri:message-3-line"
             :command="command"
@@ -94,6 +97,7 @@ async function removeUserNote() {
           />
 
           <CommonDropdownItem
+            is="button"
             v-if="!relationship?.showingReblogs"
             icon="i-ri:repeat-line"
             :text="$t('menu.show_reblogs', [`@${account.acct}`])"
@@ -101,6 +105,7 @@ async function removeUserNote() {
             @click="toggleReblogs()"
           />
           <CommonDropdownItem
+            is="button"
             v-else
             :text="$t('menu.hide_reblogs', [`@${account.acct}`])"
             icon="i-ri:repeat-line"
@@ -109,6 +114,7 @@ async function removeUserNote() {
           />
 
           <CommonDropdownItem
+            is="button"
             v-if="!relationship?.note || relationship?.note?.length === 0"
             :text="$t('menu.add_personal_note', [`@${account.acct}`])"
             icon="i-ri-edit-2-line"
@@ -116,6 +122,7 @@ async function removeUserNote() {
             @click="addUserNote()"
           />
           <CommonDropdownItem
+            is="button"
             v-else
             :text="$t('menu.remove_personal_note', [`@${account.acct}`])"
             icon="i-ri-edit-2-line"
@@ -124,6 +131,7 @@ async function removeUserNote() {
           />
 
           <CommonDropdownItem
+            is="button"
             v-if="!relationship?.muting"
             :text="$t('menu.mute_account', [`@${account.acct}`])"
             icon="i-ri:volume-mute-line"
@@ -131,6 +139,7 @@ async function removeUserNote() {
             @click="toggleMuteAccount (relationship!, account)"
           />
           <CommonDropdownItem
+            is="button"
             v-else
             :text="$t('menu.unmute_account', [`@${account.acct}`])"
             icon="i-ri:volume-up-fill"
@@ -139,6 +148,7 @@ async function removeUserNote() {
           />
 
           <CommonDropdownItem
+            is="button"
             v-if="!relationship?.blocking"
             :text="$t('menu.block_account', [`@${account.acct}`])"
             icon="i-ri:forbid-2-line"
@@ -146,6 +156,7 @@ async function removeUserNote() {
             @click="toggleBlockAccount (relationship!, account)"
           />
           <CommonDropdownItem
+            is="button"
             v-else
             :text="$t('menu.unblock_account', [`@${account.acct}`])"
             icon="i-ri:checkbox-circle-line"
@@ -155,6 +166,7 @@ async function removeUserNote() {
 
           <template v-if="getServerName(account) !== currentServer">
             <CommonDropdownItem
+              is="button"
               v-if="!relationship?.domainBlocking"
               :text="$t('menu.block_domain', [getServerName(account)])"
               icon="i-ri:shut-down-line"
@@ -162,6 +174,7 @@ async function removeUserNote() {
               @click="toggleBlockDomain(relationship!, account)"
             />
             <CommonDropdownItem
+              is="button"
               v-else
               :text="$t('menu.unblock_domain', [getServerName(account)])"
               icon="i-ri:restart-line"
@@ -171,6 +184,7 @@ async function removeUserNote() {
           </template>
 
           <CommonDropdownItem
+            is="button"
             :text="$t('menu.report_account', [`@${account.acct}`])"
             icon="i-ri:flag-2-line"
             :command="command"
diff --git a/components/common/dropdown/DropdownItem.vue b/components/common/dropdown/DropdownItem.vue
index 2a1abc33..525e71b7 100644
--- a/components/common/dropdown/DropdownItem.vue
+++ b/components/common/dropdown/DropdownItem.vue
@@ -1,16 +1,24 @@
 <script setup lang="ts">
-const props = withDefaults(defineProps<{
+const {
+  is = 'div',
+  text,
+  description,
+  icon,
+  checked,
+  command,
+} = defineProps<{
   is?: string
   text?: string
   description?: string
   icon?: string
   checked?: boolean
   command?: boolean
-}>(), {
-  is: 'div',
-})
+}>()
+
 const emit = defineEmits(['click'])
 
+const type = computed(() => is === 'button' ? 'button' : null)
+
 const { hide } = useDropdownContext() || {}
 
 const el = ref<HTMLDivElement>()
@@ -24,11 +32,11 @@ useCommand({
   scope: 'Actions',
 
   order: -1,
-  visible: () => props.command && props.text,
+  visible: () => command && text,
 
-  name: () => props.text!,
-  icon: () => props.icon ?? 'i-ri:question-line',
-  description: () => props.description,
+  name: () => text!,
+  icon: () => icon ?? 'i-ri:question-line',
+  description: () => description,
 
   onActivate() {
     const clickEvent = new MouseEvent('click', {
@@ -46,6 +54,7 @@ useCommand({
     v-bind="$attrs"
     :is="is"
     ref="el"
+    :type="type"
     w-full
     flex gap-3 items-center cursor-pointer px4 py3
     select-none
diff --git a/components/status/StatusActionsMore.vue b/components/status/StatusActionsMore.vue
index e6f1be5a..e4f47ce3 100644
--- a/components/status/StatusActionsMore.vue
+++ b/components/status/StatusActionsMore.vue
@@ -146,6 +146,7 @@ function showFavoritedAndBoostedBy() {
       <div flex="~ col">
         <template v-if="getPreferences(userSettings, 'zenMode') && !details">
           <CommonDropdownItem
+            is="button"
             :text="$t('action.reply')"
             icon="i-ri:chat-1-line"
             :command="command"
@@ -153,6 +154,7 @@ function showFavoritedAndBoostedBy() {
           />
 
           <CommonDropdownItem
+            is="button"
             :text="status.reblogged ? $t('action.boosted') : $t('action.boost')"
             icon="i-ri:repeat-fill"
             :class="status.reblogged ? 'text-green' : ''"
@@ -162,6 +164,7 @@ function showFavoritedAndBoostedBy() {
           />
 
           <CommonDropdownItem
+            is="button"
             :text="status.favourited ? $t('action.favourited') : $t('action.favourite')"
             :icon="useStarFavoriteIcon
               ? status.favourited ? 'i-ri:star-fill' : 'i-ri:star-line'
@@ -176,6 +179,7 @@ function showFavoritedAndBoostedBy() {
           />
 
           <CommonDropdownItem
+            is="button"
             :text="status.bookmarked ? $t('action.bookmarked') : $t('action.bookmark')"
             :icon="status.bookmarked ? 'i-ri:bookmark-fill' : 'i-ri:bookmark-line'"
             :class="status.bookmarked
@@ -189,6 +193,7 @@ function showFavoritedAndBoostedBy() {
         </template>
 
         <CommonDropdownItem
+          is="button"
           :text="$t('menu.show_favourited_and_boosted_by')"
           icon="i-ri:hearts-line"
           :command="command"
@@ -196,6 +201,7 @@ function showFavoritedAndBoostedBy() {
         />
 
         <CommonDropdownItem
+          is="button"
           :text="$t('menu.copy_link_to_post')"
           icon="i-ri:link"
           :command="command"
@@ -203,6 +209,7 @@ function showFavoritedAndBoostedBy() {
         />
 
         <CommonDropdownItem
+          is="button"
           :text="$t('menu.copy_original_link_to_post')"
           icon="i-ri:links-fill"
           :command="command"
@@ -210,6 +217,7 @@ function showFavoritedAndBoostedBy() {
         />
 
         <CommonDropdownItem
+          is="button"
           v-if="isShareSupported"
           :text="$t('menu.share_post')"
           icon="i-ri:share-line"
@@ -218,6 +226,7 @@ function showFavoritedAndBoostedBy() {
         />
 
         <CommonDropdownItem
+          is="button"
           v-if="currentUser && (status.account.id === currentUser.account.id || status.mentions.some(m => m.id === currentUser!.account.id))"
           :text="status.muted ? $t('menu.unmute_conversation') : $t('menu.mute_conversation')"
           :icon="status.muted ? 'i-ri:eye-line' : 'i-ri:eye-off-line'"
@@ -237,6 +246,7 @@ function showFavoritedAndBoostedBy() {
         <template v-if="isHydrated && currentUser">
           <template v-if="isAuthor">
             <CommonDropdownItem
+              is="button"
               :text="status.pinned ? $t('menu.unpin_on_profile') : $t('menu.pin_on_profile')"
               icon="i-ri:pushpin-line"
               :command="command"
@@ -244,6 +254,7 @@ function showFavoritedAndBoostedBy() {
             />
 
             <CommonDropdownItem
+              is="button"
               :text="$t('menu.edit')"
               icon="i-ri:edit-line"
               :command="command"
@@ -251,6 +262,7 @@ function showFavoritedAndBoostedBy() {
             />
 
             <CommonDropdownItem
+              is="button"
               :text="$t('menu.delete')"
               icon="i-ri:delete-bin-line"
               text-red-600
@@ -259,6 +271,7 @@ function showFavoritedAndBoostedBy() {
             />
 
             <CommonDropdownItem
+              is="button"
               :text="$t('menu.delete_and_redraft')"
               icon="i-ri:eraser-line"
               text-red-600
@@ -268,6 +281,7 @@ function showFavoritedAndBoostedBy() {
           </template>
           <template v-else>
             <CommonDropdownItem
+              is="button"
               :text="$t('menu.mention_account', [`@${status.account.acct}`])"
               icon="i-ri:at-line"
               :command="command"
@@ -275,6 +289,7 @@ function showFavoritedAndBoostedBy() {
             />
 
             <CommonDropdownItem
+              is="button"
               v-if="!useRelationship(status.account).value?.muting"
               :text="$t('menu.mute_account', [`@${status.account.acct}`])"
               icon="i-ri:volume-mute-line"
@@ -282,6 +297,7 @@ function showFavoritedAndBoostedBy() {
               @click="toggleMuteAccount(useRelationship(status.account).value!, status.account)"
             />
             <CommonDropdownItem
+              is="button"
               v-else
               :text="$t('menu.unmute_account', [`@${status.account.acct}`])"
               icon="i-ri:volume-up-fill"
@@ -290,6 +306,7 @@ function showFavoritedAndBoostedBy() {
             />
 
             <CommonDropdownItem
+              is="button"
               v-if="!useRelationship(status.account).value?.blocking"
               :text="$t('menu.block_account', [`@${status.account.acct}`])"
               icon="i-ri:forbid-2-line"
@@ -297,6 +314,7 @@ function showFavoritedAndBoostedBy() {
               @click="toggleBlockAccount(useRelationship(status.account).value!, status.account)"
             />
             <CommonDropdownItem
+              is="button"
               v-else
               :text="$t('menu.unblock_account', [`@${status.account.acct}`])"
               icon="i-ri:checkbox-circle-line"
@@ -306,6 +324,7 @@ function showFavoritedAndBoostedBy() {
 
             <template v-if="getServerName(status.account) && getServerName(status.account) !== currentServer">
               <CommonDropdownItem
+                is="button"
                 v-if="!useRelationship(status.account).value?.domainBlocking"
                 :text="$t('menu.block_domain', [getServerName(status.account)])"
                 icon="i-ri:shut-down-line"
@@ -313,6 +332,7 @@ function showFavoritedAndBoostedBy() {
                 @click="toggleBlockDomain(useRelationship(status.account).value!, status.account)"
               />
               <CommonDropdownItem
+                is="button"
                 v-else
                 :text="$t('menu.unblock_domain', [getServerName(status.account)])"
                 icon="i-ri:restart-line"
@@ -322,6 +342,7 @@ function showFavoritedAndBoostedBy() {
             </template>
 
             <CommonDropdownItem
+              is="button"
               :text="$t('menu.report_account', [`@${status.account.acct}`])"
               icon="i-ri:flag-2-line"
               :command="command"