アクセス可能な反応アコーディオン成分
38845 ワード
どのように簡単に反応するアクセス可能なアコーディオンコンポーネントを書くことになります小さな実験(それはチュートリアルに).アクセシビリティの以前の経験はありません(よく、多分いくつかの基本的なもの、例えば、Alt Paramの使用はボタンとしてリンクを使用しないように).
私は続けたAccordion Design Pattern in WAI-ARIA Authoring Practices 1.1 何もない.
このチュートリアルの焦点はA 11 Yです、そして、反応します、それで、我々はJSまたは何か他でNPMまたはCSSにパックする方法について気にかけません.この場合に開始する最も簡単な方法は、CREATERANTアプリです.
プロジェクトをブートストラップしましょう
コンポーネントのAPIについて考える時間です.典型的なアコーディオンを見てみましょう
ルートコンポーネントとセクションがあります.各セクションのタイトルとコンテンツがあります.正しい?これに基づいてAPIがどのように見えるかを想像することができます.
いいね.開きましょうAccordion Design Pattern in WAI-ARIA Authoring Practices 1.1 をコピーします.
Let's add some styles .
Now it's time to add state and event handling .
状態とコールバックcontrolled component ):
この時点で、すでにかなり良い結果です.我々は仕事の多くではなく、A 11 Yの要件の半分を果たす.
スペースまたは入力 折り返しセクションのアコーディオンヘッダーにフォーカスがある場合は、セクションを展開します.
タブ 次のフォーカス可能な要素にフォーカスを移動します. アコーディオン内のすべてのフォーカス可能な要素は、ページのタブシーケンスに含まれています.
シフトタブ 前のフォーカス可能な要素にフォーカスを移動します. アコーディオン内のすべてのフォーカス可能な要素は、ページのタブシーケンスに含まれています. あなたが少なくともこれをするならば、それはすでに何よりもよいでしょう.
Next section もう少し複雑です.
ダウンアロー フォーカスがアコーディオンヘッダーにある場合は、次のアコーディオンヘッダーにフォーカスを移動します. フォーカスが最後のアコーディオンヘッダーにある場合は、最初のアコーディオンヘッダーにフォーカスを移動します.
上矢 フォーカスがアコーディオンヘッダーにあるときは、前のアコーディオンヘッダーにフォーカスを移動します. フォーカスが最初のアコーディオンヘッダーにあるときは、最後のアコーディオンヘッダーにフォーカスを移動します. これを行うには、次の、または前のセクションを選択できるように、フォーカスがどこにあるかを追跡する必要があります.このアコーディオンあたり1回、変数として格納する必要があります.だから
コンテキストを作成する
私たちは
どこでストア
ホーム フォーカスがアコーディオンヘッダーにあるとき、最初のアコーディオンヘッダーにフォーカスを移動します. 終わり フォーカスがアコーディオンヘッダーにあるときは、最後のアコーディオンヘッダーにフォーカスを移動します.
開発者の世話をしましょう.我々は大いに依存している
我々はさらに行くことができますprovide a custom hook for default behavior .
思ったほど怖くなかった.Wai - ariaオーサリングの実践はよく書かれている👏. 私はあなたが適切なマークアップとキーボードイベントを使用するたびに
オンラインデモhere . フルソースコードhere .
私が怠惰でないならば、そして、このポストが関心を持つならば、私はサイプレスとこの構成要素をテストする方法と私がポストを書いた後に気がついた1つの卑劣なバグを修正する方法について書きます.
私は続けたAccordion Design Pattern in WAI-ARIA Authoring Practices 1.1 何もない.
このチュートリアルの焦点はA 11 Yです、そして、反応します、それで、我々はJSまたは何か他でNPMまたはCSSにパックする方法について気にかけません.この場合に開始する最も簡単な方法は、CREATERANTアプリです.
ブートストラップ
プロジェクトをブートストラップしましょう
npx create-react-app my-app
cd my-app
npm start
Remove all unrelated things .デザインAPI
コンポーネントのAPIについて考える時間です.典型的なアコーディオンを見てみましょう
ルートコンポーネントとセクションがあります.各セクションのタイトルとコンテンツがあります.正しい?これに基づいてAPIがどのように見えるかを想像することができます.
const App = () => (
<Accordion>
<AccordionSection title="section 1">content 1</AccordionSection>
<AccordionSection title="section 2" expanded>
content 2
</AccordionSection>
</Accordion>
);
指定されたAPIのコンポーネントの最初のドラフトを書きましょうimport React from "react";
export const Accordion = ({ children }) => <div>{children}</div>;
export const AccordionSection = ({ children, title, expanded }) => (
<>
<div>{title}</div>
<div>{expanded && children}</div>
</>
);
A 11 Yを加える
いいね.開きましょうAccordion Design Pattern in WAI-ARIA Authoring Practices 1.1 をコピーします.
export const AccordionSection = ({ children, title, expanded, id }) => {
const sectionId = `section-${id}`;
const labelId = `label-${id}`;
return (
<>
<div
role="button"
aria-expanded={expanded}
aria-controls={sectionId}
id={labelId}
tabIndex={0}
>
{title}
</div>
<div
role="region"
aria-labelledby={labelId}
id={sectionId}
hidden={!expanded}
>
{expanded && children}
</div>
</>
);
};
Accordion
変更する必要はありません.ヘッダーとパネルの2つの要素があります.ヘッダrole="button"
) has id
and aria-controls
( id
を返します.パネルrole="region"
) has id
and aria-labelledby
( id
を返します.aria-expanded
を返します.hidden
セクションが展開されるかどうかの反対.かなり簡単なIMO.Let's add some styles .
Now it's time to add state and event handling .
状態とコールバックcontrolled component ):
function App() {
const [expanded1, setExpanded1] = useState(false);
return (
<Accordion>
<AccordionSection
...
expanded={expanded1}
onToggle={() => setExpanded1(!expanded1)}
>
イベント処理export const AccordionSection = ({
...
expanded,
onToggle
}) => {
...
return (
<>
<div
role="button"
...
onClick={onToggle}
onKeyDown={e => {
switch (e.key) {
case " ":
case "Enter":
onToggle();
break;
default:
}
}}
>
休止しましょう
この時点で、すでにかなり良い結果です.我々は仕事の多くではなく、A 11 Yの要件の半分を果たす.
スペースまたは入力
タブ
シフトタブ
より多くのA 11 Y
Next section もう少し複雑です.
ダウンアロー
上矢
useState
? しかし、フォーカスが変更されたときにコンポーネントの再描画をトリガーしたくない.Then useRef
でしょう.export const Accordion = ({ children }) => {
const focusRef = useRef(null);
focusRef
を含むid
現在集中したセクションまたはnull
を返します.我々は追跡する必要がありますfocus
and blur
ヘッダーのイベント.<div
role="button"
...
onFocus={() => {
focusRef.current = id;
}}
onBlur={() => {
focusRef.current = null;
}}
では、どうやってパスしますかfocusRef
からAccordion
ダウントゥAccordionSection
? 私たちは小道具を介してこれを行うことができますReact.Childre.map
and React.CloneElement
) または、コンテキストでこれを行うことができます.私はより多くのクリーンAPIを作成するので、コンテキストのアイデアをもっと好きです.コンテキストを作成する
const AccordionContext = createContext({
focusRef: {}
});
export const useAccordionContext = () => useContext(AccordionContext);
パスfocusRef
to Context
私はuseMemo
我々は2011年に更新のために不要なrerdersをトリガしないことを確認してくださいContext
)const context = useMemo(
() => ({
focusRef
}),
[]
);
return (
<AccordionContext.Provider value={context}>
{children}
</AccordionContext.Provider>
);
とAccordionSection
const { focusRef } = useAccordionContext();
OK、この方法で現在選択されているセクションをキャプチャできます.ここで、キーボードイベントに対応する必要があります.export const AccordionSection = ({}) => {
...
return (
<div
onKeyDown={e => {
switch (e.key) {
case "ArrowDown":
break;
case "ArrowUp":
break;
case "Home":
break;
case "End":
break;
}
}}
>
<AccordionContext.Provider value={context}>
の場合ArrowDown
我々は、見つける必要がありますfocus
エレメントインchildren
と次のいずれかを選択します.すべての配列を得ることができますid
のchildren
元素const ids = React.Children.map(children, child => child.props.id);
次に、フォーカスされた要素のインデックスを見つけるconst index = ids.findIndex(x => x === focusRef.current);
次に、次の値if (index >= ids.length - 1) {
return ids[0];
} else {
return ids[index + 1];
}
良い.しかし、どのように我々は実際にフォーカスの変更をトリガするのだろうか?🤔私たちは
focus()
メソッドです.DOM要素を取得するには、リファレンスを使用する必要があります.export const AccordionSection = ({}) => {
const labelRef = useRef();
...
return (
<>
<div
role="button"
...
ref={labelRef}
>
同様に我々は使用する必要がありますuseEffect
DOM要素のメソッドを実際に呼び出すには問題はいつこの効果を引き起こすことですか?タブを変更するたびに、ユーザーがトリガするたびにトリガする必要がありますArrowDown
or ArrowUp
などを変更するたびにいくつかの変数とトリガ効果を格納する必要がありますのでexport const AccordionSection = ({}) => {
...
useEffect(() => {
if (id === selected && labelRef.current) {
labelRef.current.focus();
}
}, [id, selected]);
たびに選択の変更と選択された項目は、現在のものと同じですそれにフォーカスを置く.どこでストア
selected
値?rootでは、1つの変数Accordion
. どうやって通るの?文脈を通して、我々が通過したのと同じようにfocusRef
. インAccordion
:const focusRef = useRef(null);
const [selected, setSelected] = useState(null);
const context = useMemo(
() => ({
focusRef,
selected
}),
[selected]
);
...
case "ArrowDown":
{
const ids = React.Children.map(children, child => child.props.id);
const index = ids.findIndex(x => x === focusRef.current);
if (index >= ids.length - 1) {
setSelected(ids[0]);
} else {
setSelected(ids[index + 1]);
}
}
とAccordionSection
:const { focusRef, selected } = useAccordionContext();
フィル!私たちはそれを作りました.完全にアクセス可能なコンポーネント.論理を追加することを忘れないでください開発者経験
開発者の世話をしましょう.我々は大いに依存している
id
s、開発者がそれを提供するのを忘れるならば、彼らは非常に微妙な誤りを得ます.Let's check if it is present and warn otherwise :AccordionSection.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
title: PropTypes.string.isRequired,
expanded: PropTypes.bool,
onToggle: PropTypes.func
};
私たちはid
sはユニークです.let's check it too :if (process.env.NODE_ENV === "development") {
const uniqueIds = new Set();
React.Children.forEach(children, child => {
if (uniqueIds.has(child.props.id)) {
console.warn(
`AccordionSection id param should be unique, found the duplicate key: ${
child.props.id
}`
);
} else {
uniqueIds.add(child.props.id);
}
});
}
今のところ、我々のコンポーネントのAPIは、そのIDにバインドされるか、各セクションのためにユニークであると仮定するontoggleコールバックを必要とします.このAPIは使いにくいです.Let's instead pass id
to callback . この方法では、1つのストアと1つのコールバックをすべてのセクションで使用できます.const [expanded, setExpanded] = useState({ "2": true });
const toggle = id => {
setExpanded({
...expanded,
[id]: !expanded[id]
});
};
...
<AccordionSection
title="section 1"
id="1"
expanded={expanded["1"]}
onToggle={toggle}
>
...
<AccordionSection
title="section 2"
id="2"
expanded={expanded["2"]}
onToggle={toggle}
私は、我々が繰り返しなければならないのが好きでありませんexpanded
and onToggle
各セクションでは、代わりに、我々は一度それを渡すことができますAccordion
:<Accordion expanded={expanded} onToggle={onToggle}>
<AccordionSection title="section 1" id="id1">
...
</AccordionSection>
<AccordionSection title="section 2" id="id2">
...
</AccordionSection>
</Accordion>
それはこのようにきれいに見えます.同様にいくつかの欠点があることを確認する必要があるIDの状態とIDAccordionSection
sは同じです(さもなければ、いくつかのセクションは動作しないかもしれません).我々はさらに行くことができますprovide a custom hook for default behavior .
import { useState } from "react";
export const useAccordionState = intialState => {
const [expanded, setExpanded] = useState(intialState);
const onToggle = id => {
setExpanded({
...expanded,
[id]: !expanded[id]
});
};
return { expanded, onToggle };
};
最終的なコードは次のようになります.function App() {
const accordionProps = useAccordionState({ });
return (
<Accordion {...accordionProps}>
<AccordionSection title="section 1" id="id1">
結論
思ったほど怖くなかった.Wai - ariaオーサリングの実践はよく書かれている👏. 私はあなたが適切なマークアップとキーボードイベントを使用するたびに
onClick
). 完全にアクセス可能なコンポーネントを実装する楽しい学習運動することができます.オンラインデモhere . フルソースコードhere .
PS
私が怠惰でないならば、そして、このポストが関心を持つならば、私はサイプレスとこの構成要素をテストする方法と私がポストを書いた後に気がついた1つの卑劣なバグを修正する方法について書きます.
Reference
この問題について(アクセス可能な反応アコーディオン成分), 我々は、より多くの情報をここで見つけました https://dev.to/stereobooster/accessible-react-accordion-component-4p99テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol