액세스 가능한 반응 아코디언 구성 요소

접근 가능한 아코디언 구성 요소를 작성하는 간단한 작은 실험 (튜토리얼에 제출됨)나는 이전에 접근할 수 있는 경험이 없었다. (응, 아마도 기본적인 것이 있을 것이다. 예를 들어alt-param을 사용하고, 링크를 단추로 사용하지 마라.)
나는 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")에는 idaria-controls(상응하는 패널의 id)이 있습니다.패널(role="region")에는 idaria-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을 포함합니다(선택하지 않은 경우).우리는 헤더에서 focusblur 사건을 추적해야 한다.
    <div
      role="button"
      ...
      onFocus={() => {
        focusRef.current = id;
      }}
      onBlur={() => {
        focusRef.current = null;
      }}
    
    그렇다면 우리는 어떻게 focusRefAccordion에서 AccordionSection으로 전달합니까?우리는 도구(React.Childre.mapReact.CloneElement)나 상하문을 통해 이 점을 실현할 수 있다.나는 상하문 개념을 더욱 좋아한다. 왜냐하면 그것은 더욱 깨끗한 API를 만들었기 때문이다.
    컨텍스트를 작성하려면 다음과 같이 하십시오.
    const AccordionContext = createContext({
      focusRef: {}
    });
    export const useAccordionContext = () => useContext(AccordionContext);
    
    focusRefContext에 전달 (나는 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
    };
    
    또한 ids가 유일하다고 가정합니다. 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}
    
    나는 이런 것을 좋아하지 않는다. 우리는 각 부분을 위해 expandedonToggle을 반복해야 한다. 반대로 우리는 그것을 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로 이 구성 요소를 테스트하는 방법, 그리고 내가 글을 쓴 후에 발견한 음흉한 버그를 복구하는 방법에 대해 쓸 것이다.

    좋은 웹페이지 즐겨찾기