Commit 125576d

HPCesia <me@hpcesia.com>
2025-04-28 06:40:34
feat: improve a11y
1 parent 588a5bb
src/components/misc/CategoryBar.astro
@@ -15,7 +15,12 @@ const { categories, currentCategory } = Astro.props;
   id="category-bar"
   class="card border-base-300 bg-base-200/25 swup-transition-slide mb-4 w-full border"
 >
-  <div class="card-body flex flex-row items-center gap-2 overflow-auto px-2 py-3">
+  <nav
+    class="card-body flex flex-row items-center gap-2 overflow-auto px-2 py-3"
+    title={i18n(I18nKey.categories)}
+    aria-label={i18n(I18nKey.categories)}
+    role="navigation"
+  >
     <a
       href={`/`}
       class:list={[
@@ -38,7 +43,7 @@ const { categories, currentCategory } = Astro.props;
         </a>
       ))
     }
-  </div>
+  </nav>
 </div>
 
 <script>
src/components/search/Pagefind.vue
@@ -3,6 +3,8 @@ import type {
   PagefindSearchResult,
   PagefindSearchResults,
 } from '@/types/PagefindSearchAPI.d.ts';
+import I18nKey from '@i18n/I18nKey';
+import { i18n } from '@i18n/translation';
 import { type Ref, onMounted, ref } from 'vue';
 
 const isLoading = ref(false);
@@ -107,6 +109,8 @@ defineExpose({
     <slot></slot>
     <div
       class="search-result mt-4 flex h-fit max-h-[calc(60vh-8rem)] flex-col items-center gap-2 overflow-y-auto text-center"
+      :aria-label="i18n(I18nKey.searchResults)"
+      tabindex="-1"
     >
       <template v-if="isLoading">
         <div v-for="i in 2" :key="i" class="w-full rounded-md p-2">
@@ -117,7 +121,7 @@ defineExpose({
           <div class="skeleton mt-1 h-4 w-3/4"></div>
         </div>
       </template>
-      <template v-else-if="noResults"> No results found </template>
+      <template v-else-if="noResults">{{ i18n(I18nKey.noSearchResults) }}</template>
       <template v-else>
         <a
           v-for="result in searchResults"
src/components/widgets/SideToolBar/TocButton.vue
@@ -66,11 +66,15 @@ onUnmounted(() => {
       class="btn btn-circle btn-secondary btn-sm"
       @click="isOpen = !isOpen"
       :title="i18n(I18nKey.toc)"
+      :aria-label="i18n(I18nKey.toc)"
+      :aria-expanded="isOpen"
+      :aria-controls="'stb-toc-wrapper'"
     >
       <slot name="icon" />
     </button>
     <div
       ref="tocWrapper"
+      id="stb-toc-wrapper"
       class="rounded-box absolute w-[calc(100vw-4rem)] -translate-x-1/2 -translate-y-1/2 max-w-72 backdrop-blur-md duration-300 text-base-content text-start"
       :inert="!isOpen || isWideScreen"
       :class="{
src/components/widgets/Pagination.astro
@@ -1,4 +1,6 @@
 ---
+import I18nKey from '@i18n/I18nKey';
+import { i18n } from '@i18n/translation';
 import { Icon } from 'astro-icon/components';
 import Button from './Button.astro';
 
@@ -43,11 +45,17 @@ else {
 }
 ---
 
-<div id="pagination" class="flex w-full justify-between max-md:px-2">
+<nav id="pagination" class="flex w-full justify-between max-md:px-2">
   <div>
     {
       current > 1 && (
-        <Button id="prev-page-btn" class="btn-primary" href={getPageUrl(current - 1)}>
+        <Button
+          id="prev-page-btn"
+          class="btn-primary"
+          href={getPageUrl(current - 1)}
+          title={i18n(I18nKey.prevPage)}
+          aria-label={i18n(I18nKey.prevPage)}
+        >
           <Icon name="material-symbols:chevron-left-rounded" class="my-1 text-2xl" />
         </Button>
       )
@@ -101,13 +109,19 @@ else {
   <div>
     {
       current < total && (
-        <Button id="next-page-btn" class="btn-primary" href={getPageUrl(current + 1)}>
+        <Button
+          id="next-page-btn"
+          class="btn-primary"
+          href={getPageUrl(current + 1)}
+          title={i18n(I18nKey.nextPage)}
+          aria-label={i18n(I18nKey.nextPage)}
+        >
           <Icon name="material-symbols:chevron-right-rounded" class="my-1 text-2xl" />
         </Button>
       )
     }
   </div>
-</div>
+</nav>
 
 <style>
   /* hide arrows from number input */
src/components/Comment.astro
@@ -4,9 +4,11 @@ import Artalk from '@components/comment/Artalk.astro';
 import Giscus from '@components/comment/Giscus.astro';
 import Twikoo from '@components/comment/Twikoo.astro';
 import Waline from '@components/comment/Waline.astro';
+import I18nKey from '@i18n/I18nKey';
+import { i18n } from '@i18n/translation';
 ---
 
-<div id="page-comment">
+<div id="page-comment" title={i18n(I18nKey.comments)} aria-label={i18n(I18nKey.comments)}>
   {
     (() => {
       switch (commentConfig.provider) {
src/components/Navbar.astro
@@ -55,6 +55,7 @@ if (!title) title = 'Astral Halo';
                               ? { onclick: subItem.onclick }
                               : { id: 'side-' + subItem.onclick.id }))}
                           title={i18n(subItem.text)}
+                          aria-label={i18n(subItem.text)}
                           class="btn-ghost btn-primary rounded-field"
                         >
                           <span class="text-xl tracking-wide">{i18n(subItem.text)}</span>
@@ -78,6 +79,7 @@ if (!title) title = 'Astral Halo';
                       ? { onclick: item.onclick }
                       : { id: 'side-' + item.onclick.id }))}
                   title={i18n(item.text)}
+                  aria-label={i18n(item.text)}
                   class="btn-ghost join-item btn-primary"
                 >
                   <span class="text-xl tracking-wide">{i18n(item.text)}</span>
@@ -95,6 +97,7 @@ if (!title) title = 'Astral Halo';
                 class="nav-menu-item btn-circle btn-ghost btn-primary"
                 {...('href' in item && item.href && { href: item.href })}
                 title={i18n(item.text)}
+                aria-label={i18n(item.text)}
                 {...('onclick' in item &&
                   item.onclick &&
                   (typeof item.onclick === 'string'
@@ -113,6 +116,7 @@ if (!title) title = 'Astral Halo';
                 class="nav-menu-item btn-circle btn-ghost btn-primary"
                 {...('href' in item && item.href && { href: item.href })}
                 title={i18n(item.text)}
+                aria-label={i18n(item.text)}
                 {...('onclick' in item &&
                   item.onclick &&
                   (typeof item.onclick === 'string'
@@ -128,7 +132,9 @@ if (!title) title = 'Astral Halo';
           <label
             for="sidebar-drawer"
             class="btn btn-circle btn-ghost btn-primary"
-            title={i18n(I18nKey.menu)}
+            tabindex="0"
+            title={`${i18n(I18nKey.open)} ${i18n(I18nKey.menu)}`}
+            aria-label={`${i18n(I18nKey.open)} ${i18n(I18nKey.menu)}`}
           >
             <Icon name="material-symbols:menu-rounded" height="1.5rem" width="1.5rem" />
           </label>
@@ -141,8 +147,13 @@ if (!title) title = 'Astral Halo';
   </div>
   <div class="drawer-side z-50">
     <!-- Sidebar -->
-    <label for="sidebar-drawer" class="drawer-overlay"></label>
+    <label
+      for="sidebar-drawer"
+      class="drawer-overlay"
+      title={`${i18n(I18nKey.close)} ${i18n(I18nKey.menu)}`}
+      aria-label={`${i18n(I18nKey.close)} ${i18n(I18nKey.menu)}`}></label>
     <ul
+      role="navigation"
       class="menu from-base-100 to-base-300 dark:from-base-300 dark:to-base-100 min-h-full w-[min(calc(100%-3rem),20rem)] bg-linear-150 p-4"
     >
       <li>
@@ -171,6 +182,7 @@ if (!title) title = 'Astral Halo';
                               ? { onclick: subItem.onclick }
                               : { id: 'side-' + subItem.onclick.id }))}
                           title={i18n(subItem.text)}
+                          aria-label={i18n(subItem.text)}
                           useDefaultClass={false}
                         >
                           <span class="text-xl">{i18n(subItem.text)}</span>
@@ -196,6 +208,7 @@ if (!title) title = 'Astral Halo';
                       ? { onclick: item.onclick }
                       : { id: 'side-' + item.onclick.id }))}
                   title={i18n(item.text)}
+                  aria-label={i18n(item.text)}
                   useDefaultClass={false}
                 >
                   <span class="text-xl">{i18n(item.text)}</span>
@@ -211,6 +224,7 @@ if (!title) title = 'Astral Halo';
             <Button
               {...('href' in item && item.href && { href: item.href })}
               title={i18n(item.text)}
+              aria-label={i18n(item.text)}
               {...('onclick' in item &&
                 item.onclick &&
                 (typeof item.onclick === 'string'
src/components/Search.astro
@@ -9,7 +9,11 @@ import Pagefind from './search/Pagefind.vue';
 <dialog id="search_modal" class="modal">
   <div class="modal-box">
     <form method="dialog">
-      <button class="btn btn-circle btn-ghost btn-sm absolute top-2 right-2">✕</button>
+      <button
+        class="btn btn-circle btn-ghost btn-sm absolute top-2 right-2"
+        title={i18n(I18nKey.close)}
+        aria-label={i18n(I18nKey.close)}>✕</button
+      >
     </form>
     <div class="w-full p-4">
       {
@@ -21,7 +25,11 @@ import Pagefind from './search/Pagefind.vue';
                   <label class="input input-bordered flex w-full items-center gap-2">
                     <input
                       id="search-input"
-                      type="text"
+                      type="search"
+                      spellcheck="false"
+                      autocorrect="off"
+                      autocomplete="off"
+                      autocapitalize="off"
                       class="grow"
                       placeholder={i18n(I18nKey.search)}
                     />
@@ -60,6 +68,6 @@ import Pagefind from './search/Pagefind.vue';
     </div>
   </div>
   <form method="dialog" class="modal-backdrop">
-    <button>close</button>
+    <button>{i18n(I18nKey.close)}</button>
   </form>
 </dialog>
src/components/SideToolBar.astro
@@ -8,7 +8,11 @@ import DarkModeButton from './widgets/DarkModeButton.astro';
 import TocButton from './widgets/SideToolBar/TocButton.vue';
 ---
 
-<div id="side-toolbar" class="fixed right-0 bottom-10 z-30 grid grid-cols-1 gap-2">
+<div
+  id="side-toolbar"
+  class="fixed right-0 bottom-10 z-30 grid grid-cols-1 gap-2"
+  aria-label={i18n(I18nKey.toolBar)}
+>
   <div
     id="stb-hide"
     class="grid translate-x-full grid-cols-1 gap-2 pr-4 duration-500 ease-in-out"
@@ -19,25 +23,30 @@ import TocButton from './widgets/SideToolBar/TocButton.vue';
         const { icon, text } = item;
         if ('href' in item)
           return (
-            <Button href={item.href} target={item.blank ? '_blank' : undefined} title={text}>
+            <Button
+              href={item.href}
+              target={item.blank ? '_blank' : undefined}
+              title={text}
+              aria-label={text}
+            >
               <Icon name={icon} slot="icon" />
             </Button>
           );
         if ('onclick' in item) {
           if (typeof item.onclick === 'string')
             return (
-              <Button onclick={item.onclick}>
+              <Button onclick={item.onclick} title={text} aria-label={text}>
                 <Icon name={icon} slot="icon" />
               </Button>
             );
           return (
-            <Button id={`stb-${item.onclick!.id}`} title={text}>
+            <Button id={`stb-${item.onclick!.id}`} title={text} aria-label={text}>
               <Icon name={icon} slot="icon" />
             </Button>
           );
         }
         return (
-          <Button title={text}>
+          <Button title={text} aria-label={text}>
             <Icon name={icon} slot="icon" />
           </Button>
         );
@@ -68,6 +77,7 @@ import TocButton from './widgets/SideToolBar/TocButton.vue';
     <Button id="stb-back-to-top" class="group btn-circle btn-secondary btn-sm">
       <span
         id="stb-read-percentage"
+        aria-label={i18n(I18nKey.toolBarReadingPercentage)}
         class="absolute text-sm opacity-0 duration-300 group-hover:opacity-0">0</span
       >
       <Icon
src/i18n/langs/en.ts
@@ -9,6 +9,11 @@ export const en: Translation = {
   [Key.links]: 'Links',
   [Key.time]: 'Time',
   [Key.menu]: 'Menu',
+  [Key.close]: 'Close',
+  [Key.open]: 'Open',
+
+  [Key.prevPage]: 'Previous Page',
+  [Key.nextPage]: 'Next Page',
 
   [Key.tags]: 'Tags',
   [Key.categories]: 'Categories',
@@ -40,8 +45,14 @@ export const en: Translation = {
   [Key.categoryCount]: 'category',
   [Key.categoriesCount]: 'categories',
 
+  [Key.searchResults]: 'Search Results',
+  [Key.noSearchResults]: 'No Results Found',
+
   [Key.toc]: 'Table of Content',
 
+  [Key.toolBar]: 'Tool Bar',
+  [Key.toolBarReadingPercentage]: 'Reading Percentage',
+
   [Key.themeToggle]: 'Toggle Theme',
   [Key.lightMode]: 'Light',
   [Key.darkMode]: 'Dark',
src/i18n/langs/zh_CN.ts
@@ -9,6 +9,11 @@ export const zh_CN: Translation = {
   [Key.links]: '友链',
   [Key.time]: '时间',
   [Key.menu]: '菜单',
+  [Key.close]: '关闭',
+  [Key.open]: '打开',
+
+  [Key.prevPage]: '上一页',
+  [Key.nextPage]: '下一页',
 
   [Key.tags]: '标签',
   [Key.categories]: '分类',
@@ -40,8 +45,14 @@ export const zh_CN: Translation = {
   [Key.categoryCount]: '个分类',
   [Key.categoriesCount]: '个分类',
 
+  [Key.searchResults]: '搜索结果',
+  [Key.noSearchResults]: '没有找到结果',
+
   [Key.toc]: '目录',
 
+  [Key.toolBar]: '工具栏',
+  [Key.toolBarReadingPercentage]: '阅读进度',
+
   [Key.themeToggle]: '主题切换',
   [Key.lightMode]: '亮色',
   [Key.darkMode]: '暗色',
src/i18n/langs/zh_TW.ts
@@ -9,6 +9,11 @@ export const zh_TW: Translation = {
   [Key.links]: '連結',
   [Key.time]: '時間',
   [Key.menu]: '選單',
+  [Key.close]: '關閉',
+  [Key.open]: '開啟',
+
+  [Key.prevPage]: '上一頁',
+  [Key.nextPage]: '下一頁',
 
   [Key.tags]: '標籤',
   [Key.categories]: '分類',
@@ -40,8 +45,14 @@ export const zh_TW: Translation = {
   [Key.categoryCount]: '個分類',
   [Key.categoriesCount]: '個分類',
 
+  [Key.searchResults]: '搜尋結果',
+  [Key.noSearchResults]: '沒有找到結果',
+
   [Key.toc]: '目錄',
 
+  [Key.toolBar]: '工具列',
+  [Key.toolBarReadingPercentage]: '閱讀進度',
+
   [Key.themeToggle]: '主題切換',
   [Key.lightMode]: '亮色',
   [Key.darkMode]: '暗色',
src/i18n/I18nKey.ts
@@ -6,6 +6,11 @@ enum I18nKey {
   links = 'links',
   time = 'time',
   menu = 'menu',
+  close = 'close',
+  open = 'open',
+
+  prevPage = 'prevPage',
+  nextPage = 'nextPage',
 
   tags = 'tags',
   categories = 'categories',
@@ -37,8 +42,14 @@ enum I18nKey {
   categoryCount = 'categoryCount',
   categoriesCount = 'categoriesCount',
 
+  searchResults = 'searchResults',
+  noSearchResults = 'noSearchResults',
+
   toc = 'toc',
 
+  toolBar = 'toolBar',
+  toolBarReadingPercentage = 'toolBarReadingPercentage',
+
   themeToggle = 'themeToggle',
   lightMode = 'lightMode',
   darkMode = 'darkMode',