---
# System prepended metadata

title: Accessibility 學習筆記(1)
tags: [ally, 無障礙設計, React, accessibility]

---

Accessibility 學習筆記(1)
===

![](https://i.imgur.com/nwfFQyB.png)

###### tags: `accessibility`, `React`, `ally`, `無障礙設計`


# 使用工具來偵測網頁無障礙以及確保每一個網頁都具備 h1 標籤

這篇主要紀錄自己在學習無障礙設計相關知識，此篇分享的方法來自於 Marcy Sutton 的 [Testing Accessibility](https://testingaccessibility.com/) 課程。

## 幫助網頁進行無障礙設計檢測的擴充工具

Chrome 擴充裡面有幾個好用的工具，可以協助開發者進行無障礙相關的檢測，以下分享幾個常用的：

1. [Accessibility Insights for Web](https://chrome.google.com/webstore/detail/accessibility-insights-fo/pbjjkligggfmakdaogkfomddhfmpjeni)
2. [Web developer](https://chrispederick.com/work/web-developer/)
3. [WAVE Evaluation tool](https://chrome.google.com/webstore/detail/wave-evaluation-tool/jbbplnpkjmmeebjpijfedlgcdilocofh)

## heading 標籤的使用

一個網頁的 heading 要怎麼規劃，一般來說每一個網頁都須具備一個 `h1` 標籤，接著按照網頁的規劃從 `h2`、`h3` 到 `h6`，以下我們將透過 **Web developer** 工具來查看網頁的標題標籤的結構。

課程內提供的網頁如下：

![](https://i.imgur.com/UmfbmiS.jpg)

### 使用 Web DEveloper 工具：

接著我們按照這個步驟來檢視網頁的結構：
1. 到 Chrome 安裝 **Web developer** 擴充。
2. 安裝完畢後會出現在網頁的右上角，圖似一個小齒輪，左鍵點擊。
3. 選擇 **View Document Outline**。

![](https://i.imgur.com/ddPXT0f.png)


檢視結果如下：發現我們缺少一個 `h1` 標籤。
![](https://i.imgur.com/IBSH2DW.png)

> `h1` 主要在做什麼的？
> 對於螢幕閱讀器使用者來說，進入一個網頁內最需要瞭解的莫過於該網頁的主要目的，`h1` 主要提供給他們一個快速理解所在網頁的目的或者相關資訊。

### 其他視覺上的小缺失

另外我們也可以觀察到網頁中有一些副標題類的，似乎太小，視覺上並沒有區分出層級以及距離。

![](https://i.imgur.com/YSsR9fk.png)

## 該怎麼把 `h1` 放進去？

假設網頁由你規劃，在了解無障礙設計的原則之後，相信都會把標題標籤的使用規劃進去，但萬一是你要維護的舊網頁呢？幾個面向可以思考：

1. 把網頁再重新規劃。
2. 直接把 `h1` 加在導航欄下。

以上都可以做到，但是都會需要動用已經規劃好的結構，以及可能會需要和相關部門如 UI/UX 等溝通協調。

## 解決辦法

透過 `React` 提供的 `Portal` 功能，[官網](https://reactjs.org/docs/portals.html#gatsby-focus-wrapper)是這樣描述這個功能的

`Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.`


我們先在 `index.html` 內建立一個 `<div id="portal-root"></div>` 並置於最上層，這樣當我們使用 `HeaderPortal` 組件時，所呈現的任何内容都將高於網頁中其他所有内容的層次結構。

### portal-root(naming is not mandatory)

```htmlembedded!
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1" />
    <title>CampSpots</title>
  </head>
  <body>
    <div id="portal-root"></div>
    <div id="app-root"></div>
    <script type="module" src="index.js"></script>
  </body>
</html>
```

### creatPortal
透過建立一個 `HeaderPortal` 的組件，然後引用 `createPortal` 模組。
這個組件將會在網頁渲染後，抓取到 `id` 為 `portal-root` 的 `<div>`，然後建立另一個 `<div>` 並加入到 `<div id="portal-root"></div>` 之下。

```javascript!
import React, { useEffect } from "react"
import { createPortal } from "react-dom"

const HeaderPortal = ({children}) => {
    const mount = document.getElementById("portal-root")
    const el = document.createElement("div")

    useEffect(() => {
        mount.appendChild(el)
        return () => mount.removeChild(el)
    }, [el, mount])
    
    return createPortal(children, el)
}

export default HeaderPortal
```

```javascript!
// 網頁組件
// 此省略其餘引用的組件和模組等

import HeaderPortal from './header-portal'

const Listing = props => {
  const data = ListingsData.listings[props.id]
  const headerImageUrl = LoadedImageUrl(imageURLs, data.detailHeaderImageSrc)
  return (
    <BodyClassName className="header-overlap page-listing-detail">
      <>
          <div
            className="page-header"
            style={{ backgroundImage: `url(${headerImageUrl}` }}
          >
            <div className="page-header-content wide-layout">
              <div className="listing-name">{data.listingName}</div>
              <p className="location">{data.location}</p>
            </div>
          </div>
          <div className="wide-layout two-parts-70-30">
            <div>
              <div>Description</div>
              <div className="description-text" dangerouslySetInnerHTML={{ __html: sanitizeHtml(data.description) }} />
              <div>
                Amenities
              </div>
              <div className="amenity-icons grid">
                {data.amenities.map((amenity, index) => {
                  return <div key={index}>
                    <Icon name={amenity} showText={true} />
                  </div>
                })}
              </div>
            </div>
            <div>
              <div className="h4-style">
                Calendar
                <DatePicker />
              </div>
            </div>
          </div>
          <div className="wide-layout">
            <div className="detail-images">
              {data.detailImages.map((image, index) => {
                let detailImageUrl = LoadedImageUrl(imageURLs, image.imageSrc)
                return <img
                  key={index}
                  src={detailImageUrl}
                  alt={image.imageAlt}
                />
              })}
            </div>
          </div>
        </div>
      </>
    </BodyClassName>
  )
}

export default Listing
```

上面是尚未將 `HeaderPortal` 組件加入到該網頁的組件中的 DOM tree 結構，可以看到它只是單純的一個 `<div>`，如下圖：

![](https://i.imgur.com/LrOinsd.png)

```javascript!
import HeaderPortal from './header-portal'

const Listing = props => {
  const data = ListingsData.listings[props.id]
  const headerImageUrl = LoadedImageUrl(imageURLs, data.detailHeaderImageSrc)
  return (
    <BodyClassName className="header-overlap page-listing-detail">
      <>
       // 這邊加入 <HeaderPortal> 組件
        <HeaderPortal>
          <h1>Camp Spots - {data.listingName}</h1>
        </HeaderPortal>
        ...
      </>
   )
 }

// 以下省略
```

下圖是把 `<HeaderPortal>` 加入到組件的頂層後，在 Devtools 內的 DOM tree 的結構，會看到我們順利把新的元素嵌入進去。

![](https://i.imgur.com/IL7QBTn.png)


但是問題來了，把標題加進去後，毀了我們的版面，此時我們可以使用 `CSS` 來隱藏它，且也可以與行銷部門討論該怎麼描述這個 `h1` 標題，這樣即使隱藏起來，螢幕閱讀器使用者也依舊可以知道這個網頁的用途和相關資訊了，下圖是把 `h1` 標籤內文再寫得更詳細，透過螢幕閱讀器的樣子。

![](https://i.imgur.com/PWuqdPx.png)

下圖是使用螢幕閱讀器的顯示，這樣視障人士就會知道說這個頁面是要預定旅程的。
![](https://i.imgur.com/kQv7fiL.png)



## 整理其他標籤
經過後續的整理，就有結構比較完整的網頁了。
![](https://i.imgur.com/FnT413S.png)
