액세스 가능한 반응 아코디언 구성 요소
38868 단어 reacta11ywebdevjavascript
나는 Accordion Design Pattern in WAI-ARIA Authoring Practices 1.1에만 관심을 가지고 다른 것은 아무것도 없다.
이 강좌의 중점은 a11y와React이기 때문에 우리는 JS에서 npm나 CSS 또는 다른 어떤 내용까지 포장하는 방법에 관심이 없습니다.이런 상황에서 가장 간단한 시작 방식은createreact 앱을 사용하는 것이다.
독자적으로 창립하다
프로젝트를 시작하겠습니다.
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>
</>
);
a11y 추가
아름답다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
은 변경할 필요가 없습니다.제목과 패널에는 두 가지 요소가 있습니다.제목(role="button"
)에는 id
및 aria-controls
(상응하는 패널의 id
)이 있습니다.패널(role="region"
)에는 id
및 aria-labelledby
(상응하는 제목의 id
)이 있습니다.aria-expanded
, 세그먼트가 확장되는지 여부입니다.hidden
은 단면의 전개 여부와 상반됩니다.나는 매우 간단하다고 생각한다.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:
}
}}
>
잠깐 멈추래요.
이 점에서 이것은 이미 상당히 좋은 결과이다.우리는 a11y의 절반의 요구를 완성했고 그렇게 많은 일이 없었다.
공백 또는 리턴
탭
Shift+Tab
더 많은 a11y
Next section은 좀 복잡하다.
아래쪽 화살표
위쪽 화살표
useState
?좋아, 하지만 초점이 바뀔 때 구성 요소의 재렌더링을 촉발하고 싶지 않아.내 추측으로는 useRef
이다.export const Accordion = ({ children }) => {
const focusRef = useRef(null);
focusRef
은 현재 초점 부분의 id
또는 null
을 포함합니다(선택하지 않은 경우).우리는 헤더에서 focus
과 blur
사건을 추적해야 한다.<div
role="button"
...
onFocus={() => {
focusRef.current = id;
}}
onBlur={() => {
focusRef.current = null;
}}
그렇다면 우리는 어떻게 focusRef
을 Accordion
에서 AccordionSection
으로 전달합니까?우리는 도구(React.Childre.map
과 React.CloneElement
)나 상하문을 통해 이 점을 실현할 수 있다.나는 상하문 개념을 더욱 좋아한다. 왜냐하면 그것은 더욱 깨끗한 API를 만들었기 때문이다.컨텍스트를 작성하려면 다음과 같이 하십시오.
const AccordionContext = createContext({
focusRef: {}
});
export const useAccordionContext = () => useContext(AccordionContext);
focusRef
을 Context
에 전달 (나는 useMemo
을 사용하여 Context
의 업데이트로 인해 불필요한 재방송이 발생하지 않도록 확보)const context = useMemo(
() => ({
focusRef
}),
[]
);
return (
<AccordionContext.Provider value={context}>
{children}
</AccordionContext.Provider>
);
AccordionSection
년const { focusRef } = useAccordionContext();
좋아, 이렇게 하면 우리가 현재 선택한 부분을 포획할 수 있어.이제 키보드 이벤트에 응답하여 루트 구성 요소에 프로세서를 추가해야 합니다.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}
>
또한 DOM 요소를 실제로 호출하려면 useEffect
을 사용해야 합니다.문제는 언제 이런 효과가 촉발됩니까?사용자가 ArrowDown
또는 ArrowUp
등을 터치할 때마다 탭에서 변경 사항을 선택해야 합니다. 따라서 일부 변수에 저장하고 변경할 때마다 효과를 터치해야 합니다.export const AccordionSection = ({}) => {
...
useEffect(() => {
if (id === selected && labelRef.current) {
labelRef.current.focus();
}
}, [id, selected]);
변경을 선택할 때마다 선택한 항목은 현재 항목과 같고 이 항목에 초점을 맞춥니다.selected
의 값을 어디에 저장합니까?루트에서 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
에 심각하게 의존한다. 만약 개발자가 제공을 잊어버리면 그들은 매우 미묘한 실수를 하게 될 것이다.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는 onToggle 리셋이 필요합니다. 이 리셋은 id나 모든 부분에 연결된 것이 유일하다고 가정합니다.이 API는 사용하기 어렵다.Let's instead pass id
to callback . 이렇게 하면 개발자는 모든 부분에 대해 하나의 저장소와 하나의 리셋을 사용할 수 있다.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
과 onToggle
을 반복해야 한다. 반대로 우리는 그것을 Accordion
으로 한 번 전달할 수 있다.<Accordion expanded={expanded} onToggle={onToggle}>
<AccordionSection title="section 1" id="id1">
...
</AccordionSection>
<AccordionSection title="section 2" id="id2">
...
</AccordionSection>
</Accordion>
이렇게 하면 더욱 깨끗해 보인다.이 밖에state의 id와 AccordionSection
의 id가 같음을 확보해야 한다는 단점도 있습니다. 그렇지 않으면 일부 부분은 작업을 할 수 없습니다.우리는 더욱 진일보할 수 있다. 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입니다.
덧붙이다
만약 내가 게으르지 않다면, 만약 이 글이 흥미가 있다면, 나는Cypress로 이 구성 요소를 테스트하는 방법, 그리고 내가 글을 쓴 후에 발견한 음흉한 버그를 복구하는 방법에 대해 쓸 것이다.
Reference
이 문제에 관하여(액세스 가능한 반응 아코디언 구성 요소), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/stereobooster/accessible-react-accordion-component-4p99텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)