널리 알리는 기술 소식 다양한 접근성과 사용성, UI 개발에 대한 소식을 널리 알리고 참여하세요!
Spread your knowledge!

포럼 우리 모두의 소중한 의견이 모이는 곳입니다.

검색하기
  • tip
    [jetpack compose] announceForAccessibility composable 공유
    엔비전스 접근성 2023-05-29 15:40:38

    화면에 보이지 않는 스크린 리더용 토스트 메시지를 구현하는 것은 화면 변경에 대한 상황을 알리기 위해 때로 필요한 경우들이 있습니다.

    안드로이드 뷰 시스템에는 announceForAccessibility 속성이 있어서 특정 뷰를 참조하여 스크린 리더용 토스트 메시지를 구현할 수 있는데 젯팩 컴포즈에는 해당 메서드가 없어 liveRegion 시맨틱스 모디파이어에만 의존해야 합니다.

    그래서 웹과 마찬가지로 젯팩 컴포즈에서 사용할 수 있는 announceForAccessibility 컴포저블을 만들어 공유하게 되었습니다.

    사용방법은 너무나도 간단합니다.

    아래 컴포저블을 프로젝트에 추가하고 어나운스를 해야 하는 시점에 announceForAccessibility("하단에 삭제 버튼 표시됨") 과 같은 형식으로 사용할 수 있습니다.

    이렇게 하면 화면에 보이지 않게 liveRegion 시맨틱스 모디파이어를 적용한 텍스트가 나타났다가 사라지게 되어 스크린 리더가 이를 음성으로 출력할 수 있습니다.

     

    @Composable
    fun announceForAccessibility(text: String) {
        var currentText by remember { mutableStateOf(true) }
    
        LaunchedEffect(key1 = text) {
            delay(200)
            currentText = false
            delay(200)
            currentText = true
            delay(200)
            currentText = false
        }
    
        if (currentText) {
            Text(
                text = " ",
                modifier = Modifier.semantics {
                    liveRegion = LiveRegionMode.Polite
                    contentDescription = text
                }
            )
        }
    }

     

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] improveAccessibility.js 파일에 setAriaHiddenExceptForThis 메서드 추가
    엔비전스 접근성 2023-05-27 19:34:05

    모달 대화상자나 기타 가려진 부분을 접근성에서 접근되지 않도록 해야 할 때 적용할 수 있는 setHiddenExceptForThis 메서드를 예전에 공유한 적이 있습니다. 

    현재 해당 setHiddenExceptForThis 속성은 접근되어야 하는 영역 엘리먼트만 제대로 지정하면 가려진 부분을 다 inert 속성으로 적용하여 접근성 구현을 도울 수 있도록 만들어져 있습니다.

    그러나 상황에 따라서 inert 속성 보다는 aria-hidden true 속성으로 대체하여 사용하여야 할 경우가 있을 수도 있어 inert 속성이 아닌 aria-hidden 속성으로 적용할 수 있는 setAriaHiddenExceptForThis 메서드를 만들어 공유하게 되었습니다.

    따라서 화면에서 가려진 부분을 aria-hidden 속성으로 처리하기를 원하는 경우에는 setAriaHiddenExceptForThis 메서드를 활용하시기 바랍니다.

    1. 활용 방법은 완전히 동일합니다. 접근되어야 하는 콘텐츠가 포함되어 있는 div 와 같은 요소를 엘리먼트로 지정해 줍니다.

    예시: 

    const modalContainer = document.querySelector("#box__layer")

    setAriaHiddenExceptForThis(modalContainer)

    기본으로 on 으로 설정되어 있고 on 은 엘리먼트가 포함된 줄기를 제외한 나머지 모든 요소를 접근성에서 제거할 때, off 는 원래대로 되돌릴 때 사용하면 됩니다.

    따라서 가려진 요소들을 원래대로 되돌릴 때에는 'off' 스트링만 추가해 주면 되니다.

    예시:

    setAriaHiddenExceptForThis(modalContainer, 'off')

    2. 다만 해당 메서드에 새로 추가된 기능은 on 으로 설정 시 가려진 모든 요소에 tabindex -1 속성이 붙게 된다는 것입니다.

    off 설정 시에는 on 일때 설정된 tabindex -1 속성은 다 제거하고 기존에 tabindex 속성이 마크업되어 있는 요소들은 기존 tabindex 속성으로 값을 되돌립니다.

    해당 기능을 추가한 이유는 모달 대화상자 구현 시 탭 혹은 쉬프트 탭키를 통해 레이어 바깥으로 빠져 나가지 못하게 하는 구현을 항상 해 주어야 하는데 포커스 관련 구현 없이도 해당 메서드 적용만으로 탭키로는 가려지지 않은 부분만 탐색되도록 하기 위해서입니다.

    따라서 키보드에서는 마치 inert 속성을 적용한 것처럼 동작됩니다.

    물론 실제 inert 속성이 적용되지 않았으므로 마우스와는 아무런 상관이 없습니다.

    js 다운로드

    댓글을 작성하려면 해주세요.
  • tip
    [React-Next.js] 실시간 페이지 제목 변경 시 주의점
    엔비전스 접근성 2023-05-26 14:30:39

    React는 누가 뭐라고 하더라도 가장 핫한 웹 프레임워크입니다. React로 인해 생겨난 다양한 리액트 기반 프레임워크나 라이브러리들이 넘처나지요.

    그중에서도  Next.js는 실 서비스에서 아주 많이 사용됩니다. 그 이유는 서버, 라우팅, 페이지 렌더링을 하나로 묶어서 진행할 수 있기 때문입니다.

    Next.js는 React-Router와 유사한 라우팅 기능이 내장되어 있어서, 페이지 폴더 내에 리액트 컴포넌트 파일을 작성하면 페이지로 인식합니다. 그 페이지에 접속하려면, 개발자가 만든 그 페이지 컴포넌트 파일명을 주소 파라미터로 입력하면 되지요.

    Next.js는 이러한 라우팅 기능에 기존 React-Router와 다른 좋은 점이 하나 있는데, 페이지 로드 시, 타이틀을 지정했을 때, 스크린리더가 페이지 열림s을 인지할 수 있도록, aria-live로 알려주는 컴포넌트가 기본으로 적용되어 있다는 점입니다.

    Next.js로 만들어진 페이지에 들어가서 렌더링된 태그를 뜯어보면 next-route-announcer라는 커스텀 태그가 있을 것입니다. 이 영역에서 스크린리더 사용자에게 페이지 변경을 안내하는 것이지요.

     

    그런데, 페이지 타이틀이 바로 바뀌는 것을 원치 않는 개발자도 있을 겁니다. 그래서, 기본적으로 타이틀이 없고, 나중에 개발자가 수동으로 DOM에 접근하여 타이틀을 추가하는 사례가 있습니다. 꽤 자주 보이는 사례인데요. next의 동작과 접근성을 잘 모르는 사람은 잘 모르는 문제점이 있습니다.

    실시간 title 변경 시 Next는 스크린리더에서 실시간으로 변경정보를 전달할 수 없다.

    타이틀을 개발자가 원하는 시점에 적용할 때, next.js에서는 next-route-announcer를 업데이트해줘야 하지만 수동으로 변경된 타이틀은 해당 영역에 반영되지 않습니다. 즉, 스크린리더 사용자는 페이지 타이틀이 바뀌었지만, 적절한 때 바뀐 타이틀 정보를 전달받지 못한다는 뜻이죠.

    예제 페이지

    위 예제 페이지에 접속하면 홈 링크를 제외하고 두 개의 페이지 링크가 있습니다.

    첫번째 링크는 페이지가 2초 뒤에  바뀌는 더미 페이지이고, 두번째 페이지는 이 포럼 게시글 데이터를 가져와서 뿌리는 긴 로딩이 필요한 페이지로, 해결방법을 담고 있습니다.

    두번째 페이지에서는 로딩 커포넌트가 적용돼 있어서, axios요청이 끝나서 데이터가 불러와지면 role="alert"을 통해 사용자에게 페이지가 완전히 로드되었음을 안내합니다.

    그래서 결론이 뭘까?

    결론은, next/head안에 title태그로 제공하는 기본 방법 외에 타이틀을 조작하는 일(useEffect을 통한 조작)을 하지 말자는 겁니다.

    물론, 웹메신저같이, 새 메시지 등을 타이틀에 표시한다거나 그런 기능이필요하면 타이틀을 실시간으료 고체해야겠지만, 기본적으로는 지양하는 것이 좋습니다. 대신에, 로딩중 상태를 알리는 컴포넌트를 별도로 두고, 데이터 요청에 성공했을 때, 그 컴포넌트를 숨기는 방식을 사용해야 합니다.

    만약에 title을 실시간으로 교체해야만 한다면, 반드시, 타이틀을 상태로서 업데이트하고, useEffect를 통해, next-route-announcer 안에 있는 p 태그에 직접 바뀐 타이틀을 전달해야 합니다.

     

    참고:

    데이터를 불러오는 페이지는 링크를 누른 후 페이지에 접속하기까지 다소 느릴 수 있습니다.

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] improveAccessibility.js 파일에 joinSplitedTexts 메서드 추가
    엔비전스 접근성 2023-05-22 15:42:17

    바로 아래의 팁을 통하여 웹에서의 텍스트 콘텐츠 내의 스크립트로 추가된 별도의 텍스트 노드는 하나의 태그 내에 포함되어 있음에도 불구하고 노드가 분리되기 때문에 모바일에서 초점이 분리되는 이슈가 있다는 것과  이를 미연에 방지하는 방법에 대해 살폈습니다.

    이번에는 이미 개발되어 있는 페이지의 p, div, span 과 같은 텍스트 콘텐츠 내의 텍스트 노드가 두 개 이상인 요소를 다 찾아서 초점을 하나로 합치는 메서드를 공유합니다.

    메서드 이름은 joinSplitedTexts() 이며 보시다시피 인자 값은 비어 있습니다.

    해당 스크립트를 실행하면 텍스트 콘텐츠 태그 안에서 텍스트 노드가 두 개 이상 분리된 요소들을 찾습니다.

    그리고 iOS 이면 role text, 안드로이드이면 role paragraph 속성을 해당 태그에 추가합니다.

    이렇게 하면 적어도 텍스트 노드가 두 개 이상이서 초점이 분리되는 이슈는 없어지게 됩니다.

    js 다운로드

    댓글을 작성하려면 해주세요.
  • tip
    [HTML-JS] Text Node 분리 피하기
    엔비전스 접근성 2023-05-22 14:16:55

    웹 페이지를 파싱하는 DOM의 기본 구성 요소는 Node입니다. 태그도 Node의 한 종류이고, 태그 없이 작성된 텍스트도 text라는 하나의 Node입니다. 즉 웹페이지의 가장 기초단위라고 할 수 있으며, Node가 아닌 것이 거의 없다고 보면 됩니다.

    Javascript는 정적인 HTML의 사용자 경험을 조금 더 풍성하고 다이너믹하게 만들어줍니다. Javascript가 없는 웹페이지는 지금으로서는 상상하기 어려울 정도로 의존도가 높습니다. 의존도가 높은 수준을 넘어서, HTML은 미리 짜여진 코드로 구성되고, 개발자는 다른 개발자가 만든 프레임워크를 통해 Javascript만을 작성하여 페이지를 렌더링하지요. Vue, React, Svelte등이 대표적이죠.

    모든걸 자바스크립트로 처리하다보니, 한가지 문제점이 있습니다. 자바스크립트와 DOM의 기본동작을 이해하지 않고 마크업하거나, 개발자가 의도치 않는 문제가 생기는 것이죠. 아래 코드를 봅시다.

    <p>
      Hello,&nbsp;
      World!
    </p>

    HTML에서는 아무리 줄을 바꾼다고 해도, inline과 block개념에 의해 줄이 바뀌므로 한 줄로 표시됩니다. 코드상에는 두 줄로 주랍꿈이 되어있지만, 실제로 렌더링된 페이지는 한줄로 나오지요. 그리고, 스크린리더도 한줄로 자연스럽게 읽게됩니다.

     

    그런데, 문제는 React같은 HTML형식으로 마크업하는 다른 프레임워크에서 발생합니다.

    const LoginGreeting=({userName}:GreetingProps)=>{
      return (
        <span className="greeting">
          {userName}님 반갑습니다.
        </span>
      )
    };

    개발자가 보기에는 아무런 문제가 없는 코드입니다. 실제로 잘 동작하고, 보이는 사람한테는 아무런 영향도 미치지 않습니다.

    그러나, 스크린리더 사용자는 사정이 다릅니다. 특히 모바일 환경에서 아주 다른데요. 태그 안에 작성되는 텍스트도 하나의 노드라고 말씀드렸습니다. 위 코드를 보면 userName을 props에서 받아서 span.greeting에 뿌려주고 있습니다. 우리는 저 {userName}에 주목해야합니다. React나 Vue, Svelte등에서는 일반 텍스트에 변수를 참조하여 섞어 쓰는 것을 허용하는 문법들이 있습니다. 바로 그 문법이죠.

    React 기준으로 {variableName}을 사용하면 원하는 변수를 텍스트에 포함시킬 수가 있습니다. 문제는, 이렇게 포함시킨 변수는 하나의 TextNode가 아닌 새로운 텍스트로서 추가된다는 겁니다.

    그래서, TextNode는 기본적으로는 inline이기 때문에 TextNode끼리는 서로 붙지만, 스크린리더에서 객체를 탐색 할 때, 따로 떨어지게 됩니다. 저 예제의 결과를 말씀드리자면, 총 두개의 텍스트 노드가 문단안에 생성됩니다.

    • 접근성
    • 님 반갑습니다.

    이렇게 두 개로 쪼게지게 되는 것이죠.  실제로 개발자 도구로 열었을 때도, 두 노드로 나눠져있는걸 확인하실 수 있습니다. 큰 문제는 아니지만, 모바일 스크린리더 사용자 입장에서는 조금 많이 불편할 수도 있습니다. 중간에 링크가 있는 것도 아니고, 버튼이 있는것도 아닌데 초점이 나눠져버리니까요.

    자, 그러면 이걸 어떻게 방지할 수 있을까요? 해답은 간단합니다. 하나의 텍스트로 합쳐서 넣어주면 됩니다. 아래처럼요.

    const loginGreeting=({userName}:GreetingProps)=>{
      return (
        <span className="greeting">
          {`${userName}님 반갑습니다.`}
        </span>
      )
    };

    이렇게, 백틱 문자열 안에 ${}를 통해 변수를 포함시켜서 텍스트를 넣으면, 프레임워크에서 하나의 스트링이기 때문에 텍스트 노드를 하나로 렌더링하게 됩니다.

     

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] improveAccessibility.js 파일에 setViewMoreLinkLabel(sectionHeaders, viewMoreLinks) 메서드 추가
    엔비전스 접근성 2023-05-22 07:58:48

    웹페이지에서 여러 섹션에 더보기 링크나 버튼을 추가하는 경우 이에 대한 접근성을 조금 더 쉽게 구현할 수 있도록 메서드를 추가합니다.

    기본적으로 링크나 버튼의 레이블이 더보기 라고만 되어 있으면 스크린 리더 사용자는 무엇에 대한 더보기인지 알기 어려우며 주위를 탐색하여 더보기에 대한 섹션 헤더 레이블을 찾아야하는 불편함이 있습니다.

    본 메서드를 사용하게 되면 더보기에 대한 텍스트가 제목으로 포함되어 있을 경우 텍스트 제목을 그 제목 다음에 나오는 더보기 링크와 매칭시켜 aria-label 안에 섹션제목 더보기 와 같은 형식으로 삽입하게 됩니다.

    따라서 스크린 리더 사용자는 공지사항 더보기, 베스트 영화 더보기 와 같이 조금 더 명확한 레이블을 들을 수 있게 되는 것입니다.

    사용방법은 너무나 간단합니다.

    1. 더보기에 해당하는 제목이 들어가 있는 요소에 고유한 header 와 같은 class 또는 name 속성을 지정해줍니다.

    2. 마찬가지로 더보기 링크들이 있는 곳에도 고유한 클래스 또는 네임을 지정해 줍니다.

    참고로 제목과 더보기가 하나만 있다면 아이디를 사용할 수도 있겠습니다.

    그리고 setViewMoreLinkLabel(sectionHeaders, viewMoreLinks) 메서드의 인자값에 위에서 만들어준 클래스 또는 네임 값을 다음 예시와 같이 적용해 줍니다.

    setViewMoreLinkLabel('.header', '.more')

    단 반드시 제목 1, 제목 더보기, 제목 2, 제목 2 더보기와와 같이 하나의 쌍으로 존재해야 오류가 없습니다.

    댓글을 작성하려면 해주세요.
  • tip
    CCA(Colour Contrast Analyzer) 다크모드 업데이트 소식
    엔비전스 접근성 2023-05-16 16:17:43

    이번 5월, CCA(Colour Contrast Analyzer)가 버전 3.3.0(다운로드)으로 업데이트 되었습니다! 이번 CCA에는 한가지 기능적인 부분이 새로 추가되어 이렇게 소개드리게 되었습니다.

    새로 업데이트된 기능은 바로 다크모드 지원입니다. CCA(CCAe)는 오픈소스 기반의 프로젝트로, 누구나 번역 및 코드 추가 작업을 Git을 통해 참여할 수 있습니다.

    저희 팀에서 올해 4월부터 CCA에 다크모드를 지원하는 작은 프로젝트를 코딩하여 pull-request했고, 그것이 받아들여져 정식으로 이렇게 소개드릴 수 있게 된 것입니다.

    그리고, 기존에 CCA에서 결과 텍스트 복사 기능을 사용했을 때, 영문으로 나오던 결과 텍스트도 한글로 번역되었어요! Control-Shift-C로 지금 결과 텍스트를 복사해보세요.

    원레 릴리즈되고나서 바로 작성할 예정이었으나, 조금 늦어졌네요 :)

    이제, 다크모드를 사용할 때, 눈부신 CCA는 그만! 이제 CCA도 다크모드로 사용하세요!

    CCA에 다크모드가 적용된 모습

     

    댓글을 작성하려면 해주세요.
  • qna
    checkbox 접근성 질문 드립니다.
    능소니 2023-05-10 11:04:01

    안녕하세요

    보이스오버와 톡백 환경에서 체크박스 포커스 영역표시에 대해 질문 드립니다.

     

    아래와 같이 input 과 label이 별도로 분리가 되어 있으면

    iOS 보이스오버 에서는 input에 포커스가 가면 input과 label의 영역을 함께 잡고, label영역만 임의탐색도 가능한 반면

    AOS 톡백에서는 input만 영역을 잡고 label 영역은 무시를 하며 임의탐색 또한 불가능하더라고요

    그래서 아래와 같이 사용하게되면 접근성에 위배가 되는지 궁금합니다.

    <input type="checkbox" id=chk1">
    <label for="chk1">체크박스</label>

     

    댓글을 작성하려면 해주세요.
  • tip
    [HTML] <select> 태그의 레이블이 label for, title 또는 aria-label로 제공되는 경우
    엔비전스 접근성 2023-05-05 11:16:43

    우리가 콤보상자라고 부르는 <select> 태그를 구현할 경우 접근성을 위해 label for, aria-label 또는 title 속성으로 어떤 콤보상자인지에 대한 레이블을 정의합니다. 

    그런데 콤보상자에 현재 선택된 밸류가 포함되어 있고 접근성 레이블이 포함된 경우 톡백에서 레이블만 읽고 정작 선택된 밸류는 읽어주지 못하는 이슈가 있습니다.

    예를 들어 현재 5월이 선택되어 있고 접근성 레이블은 월 선택이라고 가정한다면 톡백에서는 월선택만 읽어주고 선택된 값은 읽어주지 못한다는 것입니다.

    물론 해당 문제는 톡백과 크롬 웹뷰 엔진에서 이슈를 해결해 주어야 하는 이슈이지만 임시 방편으로 간단하게 이를 해결할 수 있는데 바로 role combobox 속성을 select 태그에 추가하는 것입니다.

    이렇게 되면 톡백에서 콤보상자를 안드로이드 네이티브에서 사용되는 드롭다운 요소로 읽어주고 레이블 및 밸류를 다 읽어주게 됩니다.

    접근성 적용 시 참고하시기 바랍니다.

    댓글을 작성하려면 해주세요.
  • tip
    [react native] onPress 이벤트가 포함될 수 있는 곳에 커스텀 텍스트를 표시하는 경우 접근성 적용 방법
    엔비전스 접근성 2023-05-04 15:44:53

    안드로이드에서는 클릭 속성이 있으면 활성화 하려면 두 번 탭하세요 라는 힌트 메시지를 출력합니다.

    따라서 요소 유형이 없어도 스크린 리더 사용자가 텍스트 요소와 실행 가능한 요소를 구분할 수 있는 단서가 됩니다.

    그러나 만약 실행 가능하지 않음에도 힌트 메시지를 잘못 출력한다면 오히려 혼란만 줄 것입니다.

    리액트 네이티브에서 View > Text 객체는 기본적으로 순수한 텍스트이기 때문에 톡백에서 일반 텍스트로만 처리를 합니다.

    그러나 onPress 이벤트가 포함될 수 있는 TouchableOpacity 같은 객체에 onPress 이벤트 없이 TouchableOpacity > Text 와 같은 구조로 텍스트만 표시할 경우에도 안드로이드에서는 무조건 활성화 하려면 두 번 탭하라는 힌트 메시지를 출력합니다.

    따라서 가능하면 순수한 텍스트만을 사용하는 경우 접근성을 위해 텍스트 상위에는 onPress 이벤트를 포함하는 객체를 사용하지 않는 것이 좋습니다.

    그러나 어쩔 수 없는 경우라면 Touchable 혹은 Pressable 객체에 importantForAccessibility="no" 속성을 설정합니다.

    이렇게 하면 톡백에서 클릭 가능하다는 요소의 정보를 수신받지 못하게 되어 불필요한 힌트 메시지를 출력하지 않게 됩니다.

    아래에 이와 관련된 샘플 코드를 공유합니다.

    import React from 'react';
    import { View, Text, TouchableOpacity, Platform, StyleSheet } from 'react-native';

    const CustomText = ({ text, style }) => {
      return (
        <TouchableOpacity
          accessibilityState={Platform.OS === 'android' ? { disabled: true } : {}}
        >
          <Text style={style}>{text}</Text>
        </TouchableOpacity>
      );
    };

    const App = () => {
      return (
        <View style={styles.container}>
          <CustomText text="Hello, all." style={styles.text} />
          <CustomText text="I'm testing accessibility." style={styles.text} />
        </View>
      );
    };

    const styles = StyleSheet.create({
      container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
      },
      text: {
        fontSize: 20,
        fontWeight: 'bold',
        textAlign: 'center',
      },
    });

    export default App;

    댓글을 작성하려면 해주세요.
  • tip
    [react native] 다른 요소로 접근성 초점 보내기
    엔비전스 접근성 2023-05-04 06:33:02

    모달이 표시되거나 닫힐 때 혹은 여러 옵션을 선택 또는 해제하는 과정에서 view 들이 사라졌다 다시 생성되는 경우 등에서는 스크린 리더 사용자의 연속적인 탐색을 위해 특정 요소로 접근성 초점을 보내 주어야 하는 경우가 많습니다.

    리액트 네이티브에서는 AccessibilityInfo.setAccessibilityFocus(findNodeHandle API를 사용하여 접근성 초점을 다른 곳으로 보낼 수 있습니다.

    1. 접근성 초점을 보내고자 하는 버튼, 텍스트와 같은 컴퍼넌트에 ref를 설정합니다.

    예: ref={test1Ref}

    2. 위에서 만든 ref와 이름이 같은 const 변수를 만들고 useRef 훅은 널로 설정합니다.

    const test1Ref = useRef(null);

    3. 접근성 초점을 보내야 하는 시점에 다음 예시와 같이 접근성 초점을 보내줍니다.

    AccessibilityInfo.setAccessibilityFocus(findNodeHandle(test2Ref.current));

    주의하실 것은 접근성 초점을 보내야 할 요소가 초점을 보내는 시점보다 늦게 생성될 경우 당연히 작동하지 않게 되므로 상황에 따라 딜레이를 걸어 주어야 할 수 있습니다.

    아래는 접근성 초점을 보내는 간단한 예제입니다.

    import React, { useRef } from 'react';
    import { View, Text, Button, StyleSheet, AccessibilityInfo, findNodeHandle } from 'react-native';
    
    export default function App() {
      const test1Ref = useRef(null);
      const test2Ref = useRef(null);
      const textRef = useRef(null);
    
      const handleTest1Press = () => {
        AccessibilityInfo.setAccessibilityFocus(findNodeHandle(test2Ref.current));
      };
    
      const handleTest2Press = () => {
        AccessibilityInfo.setAccessibilityFocus(findNodeHandle(textRef.current));
      };
    
      return (
        <View style={styles.container}>
          <View style={styles.buttonContainer}>
            <Button
              title="test1"
              ref={test1Ref}
              onPress={handleTest1Press}
              accessibilityLabel="test1"
            />
            <Button
              title="test2"
              ref={test2Ref}
              onPress={handleTest2Press}
              accessibilityLabel="test2"
            />
          </View>
          <Text style={styles.text} ref={textRef}>This is a test</Text>
        </View>
      );
    }
    
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
      },
      buttonContainer: {
        flexDirection: 'row',
        justifyContent: 'space-evenly',
        alignItems: 'center',
        marginBottom: 20,
      },
      text: {
        fontSize: 20,
      },
    });
    

     

    댓글을 작성하려면 해주세요.
  • tip
    [react native] 모달 대화상자 닫기 제스처, 아이폰에서 두 손가락 문지르기 구현하기
    엔비전스 접근성 2023-05-03 22:17:00

    UIKit, swiftUI 에서 모달 대화상자 닫기 혹은 커스텀 뒤로 가기 버튼 구현 시 두 손가락 문지르기 동작을 정의하는 방법에 대해 다루었습니다. 

    리액트 네이티브에서도 모달 대화상자와 같은 대화상자를 닫는 목적으로 onAccessibilityEscape 속성을 통해 이를 구현할 수 있습니다.

    방법은 너무나도 간단합니다.

    Modal 컴포넌트를 사용하는 경우: Modal > View 안에 onAccessibilityEscape 속성을 추가하고 대화상자를 닫는 펑션을 추가해 주기만 하면 됩니다.

    커스텀 모달인 경우: 모달 컨테이너 View 안에 accessibilityViewIsModal 속성과 함께 onAccessibilityEscape 속성을 추가해 주면 됩니다.

    예시:

            <View
              style={styles.dialogContainer}
              accessibilityViewIsModal={true}
              onAccessibilityEscape={handleCloseDialog}
            >

     

    댓글을 작성하려면 해주세요.
  • tip
    [react native] 보이스오버 및 톡백 실행여부 탐지하기
    엔비전스 접근성 2023-05-02 14:21:39

    스크린 리더나 확대와 같은 접근성이 실행되든 그렇지 않든 동일한 기능을 제공해야 하는 것은 틀림이 없습니다. 

    그러나 롤링되는 배너를 정지하는 버튼을 어쩔 수 없이 접근성에서만 지원해야 하거나 특정 버튼을 추가로 제공해야 하는 경우에는 접근성 실행 여부를 체크해야 할 수 있습니다.

    리액트 네이티브에서는 AccessibilityInfo 내의 isScreenReaderEnabled 와 같은 여러 조건들을 사용하여 접근성 실행 여부를 캐치하고 addEventListener 함수를 통해 접근성이 중간에 실행되거나 종료됨을 캐치합니다. 

    아래에 스크린 리더가 실행되었을 때를 캐치하는 샘플 코드를 공유합니다. 리액트 네이티브에서 스크린 리더 실행에 따른 접근성 구현 시 참고하시기 바랍니다.

    참고: 아래 예제에서는 스크린 리더가 실행되면 screen reader is running, 실행되지 않을 때에는 screen reader is not running 이라는 텍스트가 표시됩니다.

    import React, { useEffect, useState } from 'react';
    import { View, Text, TouchableOpacity, Platform, StyleSheet, AccessibilityInfo } from 'react-native';
    
    const CustomText = ({ text, style }) => {
      return (
        <TouchableOpacity
          accessibilityState={Platform.OS === 'android' ? { disabled: true } : {}}
        >
          <Text style={style}>{text}</Text>
        </TouchableOpacity>
      );
    };
    
    const App = () => {
      const [isScreenReaderEnabled, setIsScreenReaderEnabled] = useState(false);
    
      useEffect(() => {
        const handleScreenReaderToggled = (isEnabled) => {
          setIsScreenReaderEnabled(isEnabled);
        };
    
        AccessibilityInfo.addEventListener('screenReaderChanged', handleScreenReaderToggled);
    
        AccessibilityInfo.isScreenReaderEnabled().then((isEnabled) => {
          setIsScreenReaderEnabled(isEnabled);
        });
    
        return () => {
          AccessibilityInfo.removeEventListener('screenReaderChanged', handleScreenReaderToggled);
        };
      }, []);
    
      return (
        <View style={styles.container}>
          <CustomText text="Hello, all." style={styles.text} />
          <CustomText text="I'm testing accessibility." style={styles.text} />
          {isScreenReaderEnabled ? (
            <CustomText text="Screen reader is running." style={styles.text} />
          ) : (
            <CustomText text="Screen reader is not running." style={styles.text} />
          )}
        </View>
      );
    };
    
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
      },
      text: {
        fontSize: 20,
        fontWeight: 'bold',
        textAlign: 'center',
      },
    });
    
    export default App;
    

     

    댓글을 작성하려면 해주세요.
  • tip
    [react native] 커스텀 모달 대화상자 시 가려진 콘텐츠 탐색되지 않게 하기
    엔비전스 접근성 2023-05-02 07:33:06

    리액트 네이티브에서 접근성을 적용할 때에는 iOS, android 모두를 다 대응해야 합니다. 

    Modal 컴포넌트를 사용해서 대화상자를 구현할 때에는 특별한 접근성 적용이 필요 없습니다.

    그러나 커스텀으로 대화상자를 구현할 경우에는 가려진 뒷배경을 탐색되지 않게 하기 위해서 별도의 접근성 대응이 필요합니다.

    1. iOS: 비교적 간단합니다. 대화상자를 품고 있는 부모 컨테이너에 accessibilityViewIsModal={true} 속성을 추가해 주기만 하면 됩니다.

    2. 안드로이드: 대화상자가 열렸을 때에는 대화상자 이외의 다른 콘텐츠를 품고 있는 부모 영역들을 찾아서 importantForAccessibility="no-hide-descendants" 속성을 일일이 추가해 주어야 합니다. 컨테이너에 추가하면 하위 콘텐츠들은 다 숨겨지며 이는 안드로이드 네이티브와 같습니다.

    대화상자가 닫히면 다시 yes 로 변경합니다.

     

    아래는 이 두 접근성을 적용한 샘플 코드입니다. 접근성 적용시 참고하시기 바랍니다.

    import React, { useState, useRef } from 'react';
    import { View, Text, TouchableOpacity, Animated } from 'react-native';
    
    const App = () => {
      const [isDialogVisible, setIsDialogVisible] = useState(false);
      const [importantForAccessibility, setImportantForAccessibility] = useState('yes')
      const fadeAnim = useRef(new Animated.Value(0)).current;
    
      const handleOpenDialog = () => {
        setIsDialogVisible(true);
        setImportantForAccessibility('no-hide-descendants')
        Animated.timing(fadeAnim, {
          toValue: 1,
          duration: 300,
          useNativeDriver: true,
        }).start();
      };
    
      const handleCloseDialog = () => {
        setImportantForAccessibility('yes')
        Animated.timing(fadeAnim, {
          toValue: 0,
          duration: 300,
          useNativeDriver: true,
        }).start(() => setIsDialogVisible(false));
      };
    
      return (
        <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
          <Text importantForAccessibility={importantForAccessibility} style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 20 }}>
            Hello, I'm N-Visions.
          </Text>
          <Text importantForAccessibility={importantForAccessibility} style={{ fontSize: 20, marginBottom: 20 }}>
            I want to try to improve accessibility.
          </Text>
    
          <TouchableOpacity accessibilityLabel="open dialog" accessibilityRole="button" importantForAccessibility={importantForAccessibility} onPress={handleOpenDialog}>
            <Text style={{ fontSize: 20, fontWeight: 'bold', marginBottom: 20 }}>
              Open Dialog
            </Text>
          </TouchableOpacity>
    
          {isDialogVisible && (
            <View
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                right: 0,
                bottom: 0,
                justifyContent: 'center',
                alignItems: 'center',
              }}
              accessibilityViewIsModal={true}
            >
              <Animated.View
                style={[
                  {
                    backgroundColor: 'white',
                    padding: 20,
                    borderRadius: 10,
                    alignItems: 'center',
                  },
                  {
                    opacity: fadeAnim,
                    transform: [
                      {
                        scale: fadeAnim.interpolate({
                          inputRange: [0, 1],
                          outputRange: [0.5, 1],
                        }),
                      },
                    ],
                  },
                ]}
              >
                <Text accessibilityRole="header" style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 20 }}>
                  Dialog Title
                </Text>
                <Text>This is the content of the dialog.</Text>
                <TouchableOpacity accessibilityRole="button" accessibilityLabel="close dialog" onPress={handleCloseDialog}>
                  <Text style={{ fontSize: 18, color: 'blue', marginTop: 20 }}>
                    Close Dialog
                  </Text>
                </TouchableOpacity>
              </Animated.View>
            </View>
          )}
        </View>
      );
    };
    
    export default App;
    

     

    댓글을 작성하려면 해주세요.
  • tip
    [react native] 커스텀 라디오버튼 접근성 적용
    엔비전스 접근성 2023-05-01 14:59:27

    옵션 1부터 옵션 3과 같은 리스트에서 단 하나의 옵션만 선택할 수 있는 경우 이를 라디오버튼으로 정의합니다.

    리액트 네이티브에서 커스텀 라디오버튼 구현 시 다음과 같이 접근성을 적용합니다.

    1. onPress 이벤트가 들어가는 객체에 accessibilityRole="radio" 적용.

    참고로 아이폰에는 라디오버튼이라는 요소 유형이 없기 때문에 보이스오버에서는 radiobutton 이라는 문자가 대체 텍스트로 삽입됩니다.

    2. onPress 이벤트가 적용된 객체에 체크됨 상태를 적용하기 위해 accessibilityState={{checked: true/false}} 적용.

    주의할 것은 라디오버튼과 관련된 선택됨 스타일이 View 에 적용되더라도 accessibilityRole, accessibilityState 는 반드시 onPress 이벤트가 있는 곳에 적용해야 합니다.

    참고로 아이폰에는 체크됨 상태정보가 없기 때문에 대체 텍스트 형태로 checked, not checked 문자가 추가됩니다.

    다음은 커스텀 라디오버튼에 접근성을 적용한 코드 예시입니다.

    import React, { useState } from 'react';
    import { View, Text, TouchableWithoutFeedback, StyleSheet, SafeAreaView } from 'react-native';
    
    const RadioButton = ({ label }) => {
      const [selectedOption, setSelectedOption] = useState('');
    
      const handleOptionSelect = (option) => {
        setSelectedOption(option);
      };
    
      return (
        <SafeAreaView style={styles.container}>
          <View style={styles.titleContainer}>
            <Text accessibilityRole="header" style={styles.titleText}>Radio Example</Text>
          </View>
          <View style={styles.radioGroupContainer} accessibilityRole="radiogroup">
            <Text style={styles.label}>{label}</Text>
            <TouchableWithoutFeedback 
              accessibilityLabel="Option 1"
              accessibilityRole="radio"
              accessibilityState={{ checked: selectedOption === 'option1' }}
              onPress={() => handleOptionSelect('option1')}
            >
              <View style={selectedOption === 'option1' ? styles.selectedOption : styles.unselectedOption}>
                <Text>Option 1</Text>
              </View>
            </TouchableWithoutFeedback>
            <TouchableWithoutFeedback 
              accessibilityLabel="Option 2"
              accessibilityRole="radio"
              accessibilityState={{ checked: selectedOption === 'option2' }}
              onPress={() => handleOptionSelect('option2')}
            >
              <View style={selectedOption === 'option2' ? styles.selectedOption : styles.unselectedOption}>
                <Text>Option 2</Text>
              </View>
            </TouchableWithoutFeedback>
            <TouchableWithoutFeedback 
              accessibilityLabel="Option 3"
              accessibilityRole="radio"
              accessibilityState={{ checked: selectedOption === 'option3' }}
              onPress={() => handleOptionSelect('option3')}
            >
              <View style={selectedOption === 'option3' ? styles.selectedOption : styles.unselectedOption}>
                <Text>Option 3</Text>
              </View>
            </TouchableWithoutFeedback>
          </View>
        </SafeAreaView>
      );
    };
    
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
      },
      titleContainer: {
        marginBottom: 20,
      },
      titleText: {
        fontSize: 20,
        fontWeight: 'bold',
      },
      radioGroupContainer: {
        alignItems: 'center',
      },
      label: {
        fontWeight: 'bold',
        marginBottom: 10,
      },
      selectedOption: {
        backgroundColor: '#333',
        padding: 10,
        borderRadius: 5,
        marginBottom: 10,
      },
      unselectedOption: {
        backgroundColor: '#eee',
        padding: 10,
        borderRadius: 5,
        marginBottom: 10,
      },
    });
    
    export default RadioButton;
    

     

    댓글을 작성하려면 해주세요.
  • tip
    [react native] 톡백 스크린 리더 호환성을 고려한 accessibilityLabel 적용 관련
    엔비전스 접근성 2023-05-01 13:40:15

    보이스오버와 달리 톡백의 경우 상태정보, 레이블, 요소 유형을 기준으로 요소의 읽는 순서를 톡백 설정에서 사용자화 할 수 있습니다. 

    그래서 기본적으로는 상태정보, 레이블, 요소 유형 순으로 읽어주지만 이것을 레이블을 먼저 읽도록 설정할 수도 있는 것입니다.

    그런데 라디오버튼, 버튼과 같은 요소 유형이 있는 리액트 네이티브에서 톡백으로 탐색해보면 무조건 요소 유형을 앞에 읽는 것을 알 수 있습니다.

    예: 버튼, 확인. 버튼, 공지사항.

    물론 읽어주는 것만 보면 이슈가 없다고 생각할 수 있지만 사용성 측면에서는 스크린 리더의 호환성과 맞지 않아 불편할 수 있습니다.

    예를 들어 한 화면에 버튼이 10개가 있는데 레이블을 빠르게 듣고 싶음에도 불구하고 항상 버튼이라는 요소 유형을 먼저 들어야 한다고 생각해 보세요.

    이러한 이슈가 발생하는 원인은 요소 유형 하위에 텍스트가 표시될 경우 톡백은 이를 구분하여 처리하기 때문입니다.

    예를 들어 TouchableOpacity > Text 객체가 있다면 accessibilityRole="button" 은 TouchableOpacity 에 추가될 것입니다.

    그러면 버튼 하위에 텍스트가 있기 때문에 버튼에는 레이블이 없는 셈이 되므로 요소 유형을 먼저 읽는 것입니다.

    그러면 accessibilityRole 자체를 Text 객체에 주면 안 되느냐는 질문을 할 수 있습니다.

    결론은 그렇게 할 경우 보이스오버에서는 요소 유형을 읽지 못합니다.

    따라서 다음 샘플과 같이 번거롭더라도 요소 유형이 있는 객체에는 하위에 텍스트 또는 버튼 타이틀이 존재하더라도 accessibilityLabel 을 텍스트 혹은 타이틀과 동일하게 제공해 주는 것이 사용성을 더 높일 수 있습니다.

     

    import React, { useState } from 'react';
    import { View, Button, Text } from 'react-native';
    
    const PlayPauseButton = () => {
      const [title, setTitle] = useState('Play');
    
      const handlePress = () => {
        setTitle(title === 'Play' ? 'Pause' : 'Play');
      };
    
      return (
        <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
          <Button
            title={title}
            onPress={handlePress}
            accessibilityLabel={title}
          />
          <Text style={{ marginTop: 16 }}>Sample text</Text>
        </View>
      );
    };
    
    export default PlayPauseButton;
    

     

    댓글을 작성하려면 해주세요.
  • tip
    [react native] announceForAccessibility 구현 시 참고사항
    엔비전스 접근성 2023-05-01 11:16:21

    리액트 네이티브에서는 AccessibilityInfo.announceForAccessibility 를 지원하여 스크린 리더 사용자에게 토스트 형태로 무언가를 알려 주어야 할 때 해당 코드를 활용할 수 있습니다.

    다만 버튼을 누르는 동시에 무언가를 토스트 형태로 알려 주어야 할 때에는 해당 코드를 딜레이 없이 사용하게 되면 iOS에서는 어나운스로 출력되는 메시지를 읽지 못하는 이슈가 있습니다.

    이는 보이스오버의 경우 버튼을 누르면 버튼 텍스트가 변경되든 그렇지 않든 버튼의 레이블을 무조건 다시 읽기 때문입니다.

    물론 AccessibilityInfo.announceForAccessibilityWithOptions 를 활용하면 기존에 읽던 것을 끝내고 어나운스를 하게 할지 등을 설정할 수 있는데 이 역시도 AccessibilityInfo.announceForAccessibilityWithOptions 가 실행되기 전에 약 0.1초 정도의 딜레이를 주어야 어나운스가 잘 작동합니다.

    따라서 특정 버튼을 눌렀을 때 버튼의 텍스트 변경 없이 복사 완료, 하단에 콘텐츠 표시됨과 같은 어나운스를 표시해야 할 때는 아래 예시와 같이 약 0.1초 정도의 setTimeout 딜레이를 주면 iOS android 두 스크린 리더에서 모두 작동할 것입니다.

    import React, { useState } from 'react';
    import { View, Text, Button, AccessibilityInfo, StyleSheet } from 'react-native';
    
    const MyComponent = () => {
      const [count, setCount] = useState(0);
    
      const handlePress = () => {
        const newCount = count + 1;
        setCount(newCount);
        setTimeout(() => {
          AccessibilityInfo.announceForAccessibility(`Count is now ${newCount}`);
        }, 100);
      };
    
      return (
        <View style={styles.container}>
          <Text style={styles.text}>Count: {count}</Text>
          <Button 
            accessibilityLabel="Increment"
            title="Increment" 
            onPress={handlePress} 
          />
        </View>
      );
    };
    
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
      },
      text: {
        fontSize: 20,
        fontWeight: 'bold',
        textAlign: 'center',
      },
    });
    
    export default MyComponent;
    

     

    댓글을 작성하려면 해주세요.
  • qna
    톡백에서 selectbox 선택시 포커스 문의 드립니다.
    능소니 2023-04-14 17:55:43

    안녕하세요.

    안드로이드 톡백에서 select의 option을 이미 선택된 값과 동일한 값을 다시 선택하는 경우 포커스를 select에 유지를 시켜야 하는데 포커스가 사라지거나 랜덤한 곳으로 위치하게 됩니다.. iOS는 정상이에요.

     

    예를 들어 아래와 같은 코드에서 '서울'이 선택된 상태로 '경기도'를 선택한다면 change이벤트를 사용해서 select 제어가 가능하지만

    '서울'이 선택된 상태에서 '서울'을 선택하면 change이벤트가 먹지 않고 그 외 다른 이벤트들도 전부 안먹더라고요..

    그래서 다시 포커스를 select로 보낼 방법이 없습니다..

    <select title="전화번호" name="telnum">
        <option value="02">서울</option>
        <option value="031">경기도</option>
    </select>

    혹시 방법이 있을까요?

    확인 부탁드립니다. 감사합니다!

    댓글을 작성하려면 해주세요.
  • tip
    [DOM] event.preventDefault()를 함부로 쓰지 마세요.
    Webacc NV 2023-04-06 10:41:26

    개발자가 EventListener를 등록할 때, 얘기치 않거나 원치 않는 동작을 제어하기 위해서 Handler 함수 내부에서 event.preventDefault()를 호출하여, 기본 이벤트를 제거합니다.

    이러한 기본 이벤트 방지는 개발자가 원하는 결과 동작을 빠르게 만들어 낼 수 있습니다. 그런데, 모르고 사용하면 정말 곤란한 상황이 발생할 수도 있습니다. 버튼에 click이벤트를 걸었는데 아무리 눌러도 눌리지 않거나 하는 문제가 생길 수 있지요.

    <button class="touchstart">touchstart</button>
    <button class="touchend">touchend</button>
    <button class="mouseup">mouseup</button>
    <button class="mousedown">mousedown</button>
    <button class="keyup">keyup</button>

    실험을 위해 HTML에 각각 class 이름을 이벤트 이름으로 지정한 버튼을 만들었습니다.

    그리고, 자바스크립트에서는 forEach로 요소를 순회하며 아래처럼 이벤트를 걸었습니다.

    [...document.querySelectorAll("button")].forEach(el=>{
      el.addEventListener('click', ()=>{alert("success")});
      el.addEventListener(el.className,(evt)=>{evt.preventDefault()});
    })

    눌렀을 때, click 이벤트가 동작하는지 확인하기 위해 "success"라는 글자가 표시되는 브라우저 alert 대화상자를 뛰우는 click 이벤트도 등록 했습니다.

     

    결과

    PC: PC에서는 keyup 이벤트 외에는 아무런 문제가 생기지 않았습니다. keyup 이벤트의 기본 동작이 방지 되었기 때문에, Enter나 Space키로 해당 버튼을 누를 수 없음을 확인했습니다. mouseup이나 mousedown에는 문제가 생기지 않았습니다.

    Mobile: 문제는 모바일에서 매우 뚜렷하게 발생했습니다. 분명 click 이벤트를 수신하고 있음에도, touchend 나 touchstart이벤트가 방지된 요소는 스마트폰 같은 터치 화면에서 누를 수 없었습니다.

    이렇게, click 이벤트 하나 갖고도, 여러 이벤트 동작이 혼합되어 있는 걸 알 수 있고, click 이벤트가 실행되기 위해 선행되는 이벤트의 동작을 막아버린다면, click 이벤트 리스너에 전달되지 않는다는 걸 알 수 있습니다.

    event.preventDefault(), 잘 알고 사용합시다.

    댓글을 작성하려면 해주세요.
  • tip
    [HTML-Javascript] 별점 막대 아이디어 공유
    Webacc NV 2023-04-05 13:25:56

    모바일에서 흔히 볼 수 있는 레이팅 바를 <input type="range"> 네이티브 슬라이더로 구현한 아이디어입니다.

    바로 전에 소개한 role="switch"와 달리 Javascript에 많이 의존해야 하지만, role="slider"로 구현하는 것보다, 조금 더 빠른 방법입니다.

    <!-- HTML 코드 -->
    <div class="wrapper">
        <h1>별점 막대</h1>
        <label for="rating">
            <b>이 컴포넌트를 평가해주세요!</b>
            <input type="range" id="rating" class="rating-bar" step="0.5" max="5" min="0">
        </label> 
    </div>

    기본 마크업 코드는 간단합니다. 다음은 스타일입니다.

    /* Common */
    *{margin:0; padding:0; box-sizing: border-box;}
    :root {font-size:1rem;}
    @media (max-width:1024px) {
        :root{font-size: 1.1em;}
        html, body{width: 100%; height: 100%;}
    }
    
    html,body{width: 100%; height: 100%; box-sizing: border-box;}
    div.wrapper { width: 98%; margin:0 auto;}
    
    
    label{
        vertical-align: middle; display: inline-flex;
        justify-content: center; align-items: center;
    }
    
    
    /* 여기서부터 range 서식의 스타일을 지우는 Rule입니다.*/
    input[type="range"].rating-bar {
        appearance: none; -webkit-appearance: none; -moz-appearance: none;
        margin-left:0.5em;  background-color: transparent;
        width: fit-content; display: inline-flex; position: relative;
        vertical-align: middle;
        align-items: center;
    }
    
    input[type="range"].rating-bar.firefox-polyfill { position: absolute; left:0; height: 100%; width: fit-content;}
    input[type="range"].rating-bar:focus{outline: none;}
    input[type="range"].rating-bar:not(.firefox-polyfill):focus-within {outline: auto;}
    .rating-bar-wrapper:focus-within {outline: auto; }
    
    input[type="range"].rating-bar::-moz-range-thumb{ background-color: transparent; border:none; }
    
    
    input[type="range"].rating-bar::-webkit-slider-thumb,
    input[type="range"].rating-bar::-webkit-slider-runnable-track {
        display: none;
    }
    
    /* Firefox용 wrapper */
    .rating-bar-wrapper {
        display: inline-flex; position: relative;
        width: auto; height: auto;
        align-items: center;
    }
    
    
    /* 여기서부터 별점에 사용될 별 디자인입니다.
    before는 그림자 효과를 위해 넣은것입니다. */
    input[type="range"].rating-bar::before, input[type="range"].rating-bar::before{
        font-family: 'Courier New', Courier, monospace;
        position: absolute; content: "★★★★★"; display: block;
        color:transparent; font-size:2rem; top:50%; transform: translateY(-50%);
        text-shadow: 1px 0 1px #000;
    }
    /*after가 슬라이더에 몇점이 들어갔는지에 따라 별이 노란색으로 보이게 할 요소입니다. */
    input[type="range"].rating-bar::after{
        font-family: 'Courier New', Courier, monospace;
        background: linear-gradient(90deg, gold var(--stared), #431 var(--remain));
        background-color: transparent;
        content: "★★★★★"; position: absolute; display: block;
        background-clip: text; -webkit-background-clip: text;
        color:transparent; font-size:2rem;
    }
    /* 아래는 firefox에서는 <input type="range">에 after와 before가 허용되지 않아 대체수단을 위해 넣어놨씁니다. */
    .rating-bar-wrapper::before{
        font-family: 'Courier New', Courier, monospace;
        content: "★★★★★"; display: block;
        color:transparent; top:0; text-shadow: 1px 0 1px #000;
        font-size:2rem;
    }
    
    .rating-bar-wrapper::after {
        font-family: 'Courier New', Courier, monospace;
        background: linear-gradient(90deg, gold var(--stared), #431 var(--remain));
        content: "★★★★★"; position: absolute; display: block;
        background-clip: text; -webkit-background-clip: text;
        color:transparent; font-size:2rem;
    }
    
    /* background-clip:text 효과로 배경색상이 별 글자에 체워지도록 할 예정 */

    var(--stared)와 var(--remain)은 각각 현재 별 개수, 남은 별 개수 비율입니다. 자바스크립트로 퍼센트가 조절될겁니다.

    /** @param {HTMLInputElement} ratingSlider */
            const setRatingBar = (ratingSlider) => {
                // <input type="range">를 받습니다.
                let mousedown = false; /* 이벤트에 사용될 mousedown 변수입니다. 마우스가 눌렸거나
                손가락으로 누르고 있는지를 확인합니다. */
                const wrapper = document.createElement("div"); // firefox를 위한 컨테이너입니다.
                const isFirefox = /Firefox/.test(navigator.userAgent) // 파이어폭스임을 확인하는 부울값
                const pointerElement = isFirefox ? wrapper : ratingSlider; /*
                    브라우저가 파이어폭스이냐 아니냐에 따라, 마우스 이벤트가 적용될 요소를 다르게 합니다.
                    firefox이면 컨테이너에, chrome이면 slider에 직접 마우스 이벤트를 넣습니다.
                 */
                if( isFirefox ) {
    
                    // 파이어폭스이면?
                    ratingSlider.parentElement.replaceChild(wrapper,ratingSlider);
                    /* 레이팅 슬라이더 부모요소 안에서 아까 만들어둔 wrapper와 ratingSlider를 DOM 상에서
                       교체합니다. */   
                    ratingSlider.classList.add('firefox-polyfill');
                    /* 파이어폭스 전용 스타일링을 위해 ratingSlider에 서브클래스를 추가합니다. */
                    wrapper.classList.add('rating-bar-wrapper');//wrapper를 위한 클래스를 추가합니다.
                    wrapper.appendChild(ratingSlider);/*wrapper로 ratingSlider가 교체되었으므로
                      wrapper 안에 슬라이더가 들어갈 수 있도록 appendChild로 넣어줍니다.
                    */
                };
                const valueUpdate = ()=>{ // 컴포넌트를 초기화하거나 업데이트하는 함수
                    const style = pointerElement.style; // 마우스 이벤트가 걸릴 요소의 스타일객체
                    const max = Number(ratingSlider.max); // 슬라이더 최댓값
                    const value = Number(ratingSlider.value); // 슬라이더 현재값
                    const stared = (value/max)*100; // 현재 별점의 퍼센트를 구함.
                    const remain = 100-stared; // 현재 별점의 퍼센트를 100에서 뺴서 남은 별 퍼센트를 구함.
                   
                    ratingSlider.setAttribute('aria-valuetext',`별점 총 5점 중 ${ratingSlider.value}점`)              /*aria-valuetext로 슬라이더 값이 숫자가 아닌 별점으로 나오도록 업데이트*/
                    /*
                      아래에서는 스타일 객체에 아까 CSS에서 쓴 변수 등록함.
                      값은 위에서 구한 퍼센트를 등록. remain은 linear-gradient 특성상 음수값으로 넣음.
                    */
                    style.setProperty("--stared",`${stared}%`);
                    style.setProperty("--remain",`${-remain}%`);
                };
    
                /*
                   마우스 / 터치 이벤트
                   포인터 땜, 터치 종료, 포인터 이탈 시 마우스다운 변수가 false로 바뀜.
                   포인터 누름, 터치 시작 시 mousedown을 true로 설정함.
                   
                   mousedown이 true이고 마우스 포인터나 터치 포인터가 움직이는 경우,
                   기본 이벤트를 막고, 슬라이더의 넓이를 구한 후, 마우스가 누른 지점과 계산하여
                   퍼센트로 변환하여, 별점을 줄 수 있게 함.
                */
                const mouseAdjustment = (evt)=>{
                    if(/pointerup|touchend|pointerout/.test(evt.type)) {
                        mousedown = false;
                    }
                    if ( /pointerdown|touchstart/.test(evt.type) ) {
                        mousedown = true;
                    }
                    if(mousedown && /touchmove|pointermove/.test(evt.type)){
                        evt.preventDefault();
                        const width = ratingSlider.offsetWidth;
                        const pos = evt.type === "touchmove" ? evt.changedTouches[0].clientX - ratingSlider.offsetLeft : evt.offsetX;
                        const value = Number((5*(pos / width)).toFixed(1));
                        ratingSlider.value = value > 100 ? 100 : value < 0.5 ? 0 : value;
                        valueUpdate();
                    }
                };
                valueUpdate()// 초기에도 별점이 올바르게 표시되어야하니 함수를 호출.
                ratingSlider.addEventListener('input',valueUpdate); // 사용자가 값을 입력할때 업데이트 함수 호출
                ratingSlider.addEventListener('change',valueUpdate); // 사용자가 값을 변경했을 때 업데이트 호출
    
                //위에서 만든 마우스 이벤트를 모두 적용
                pointerElement.addEventListener('pointermove',mouseAdjustment)
                pointerElement.addEventListener('touchmove',mouseAdjustment)
                pointerElement.addEventListener('pointerdown',mouseAdjustment)
                pointerElement.addEventListener('pointerup',mouseAdjustment)
                pointerElement.addEventListener('pointerout',mouseAdjustment)
                pointerElement.addEventListener('touchstart',mouseAdjustment)
                pointerElement.addEventListener('touchend',mouseAdjustment)
            };
    
            setRatingBar(document.querySelector(".rating-bar"));
            // HTML에 마크업한 슬라이더에 적용

    결과물:


    움직이는 사진: 별점 슬라이더를 마우스로 끌어서 조절하고, 키보드로 조작하는 모습

    댓글을 작성하려면 해주세요.
  • tip
    자바스크립트 없이 role="switch"와 CSS만으로 WAI-ARIA 스위치 컨트롤 만들기
    Webacc NV 2023-04-04 11:23:52

    웹에서 종종 스위치 컨트롤을 만들어야 하는 상황이 생깁니다. 그럴 때, 가장 간편하게 이를 해결할 수 있는 방법을 소개하고자 합니다.

    오래전에 체크 박스나 라디오 버튼은 레이블로 스타일을 씌우지 않고, after와 before를 사용할 수 있다는 사실을 알려드린 적이 있습니다.

    시각적인 모양은 CSS만 활용할 겁니다.

    그런데, 어떻게 자바스크립트도 없이 단 한 개의 속성 만으로 스위치를 구현 하냐고요? 스크린 리더 사용자에게는 스위치라는 정보를 WAI-ARIA 유형으로 주는 겁니다.  잘 모르신다면 농담처럼 들릴 수도 있겠지만 WAI-ARIA 명세를 깊게 이해하시는 분이라면 금세 아실 겁니다.

    WAI-ARIA 명세를 보시면, switch, radio, checkbox는 모두 aria-checked 속성을 통해 선택 정보를 제공합니다. 그런데, 표준 컨트롤인 <input type="checkbox">는 aria-checked는 아니지만, 이미 aria-checked와 내부적으로 동일한 정보를 AT에게 전달합니다. 즉, aria-checked를 자바스크립트 DOM 이벤트로 업데이트하지 않아도 브라우저 기본 이벤트가 있기 때문에 role만 주면 스위치로 둔갑하게 됩니다.

    <!DOCTYPE html>
    <html lang="ko">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>스위치</title>
        <link rel="stylesheet" type="text/css" href="./switch.css">
    </head>
    <body>
        <h1>초간단 스위치</h1>
        <div>
            <label for="switch1">
                스위치 끔 예제
                <input type="checkbox" role="switch" name="switch1" id="switch1" />
            </label>
        </div>
        <div>
            <label for="switch2">
                스위치 켬 예제
                <input type="checkbox" role="switch" name="switch2" id="switch2" checked />
            </label>
        </div>
        <label for="switch3">
            스위치 끔 비활성 예제
            <input type="checkbox" role="switch" name="switch3" id="switch3" disabled />
        </label>
        <label for="switch4">
            스위치 켬 비활성 예제
            <input type="checkbox" role="switch" name="switch4" id="switch4" disabled checked />
        </label>
    </body>
    </html>

    HTML 코드입니다. 그냥 일반적인 input 태그를 사용하듯 마크업했습니다. 여기다가 role="switch"를 추가했습니다. 이제 CSS만 추가하면 완성입니다.

     

    /* switch.css */
    *{margin:0; padding: 0; box-sizing: border-box;}
    html,body{width: 100%; height: 100%;}
    
    input[type="checkbox"][role=switch] {
        display: inline-flex; appearance: none; -webkit-appearance: none;
        outline-offset: 0.2em; position: relative; margin: 0.5em;
        border-radius: 1em; vertical-align: middle;
        width:2.5rem; height: 1.25rem; max-height: fit-content; max-width: fit-content;
    }
    input[type="checkbox"][role=switch]:focus{outline: none;}
    input[type="checkbox"][role=switch]:focus::before {
        outline: auto; outline-offset: 0.3em;
    }
    input[type="checkbox"][role=switch]:disabled {
        opacity: 0.5; filter: grayscale(1);
    }
    input[type="checkbox"][role=switch]::before {
        position: relative; top:0; left:0; content:""; display: block;
        width:2.5rem; height: 1.25rem; border:solid 1px; border-radius: 1em;
        background-color: white;
        border-radius: 1em; box-shadow: inset 0.1em 0.1em 0.3em 0.1em rgba(0, 0, 0, 0.5);
    }
    input[type="checkbox"][role=switch]::after {
        content:""; border-radius: 50%; position: absolute;
        box-shadow: 0 0.1em 0.2em 0.1em #000;
        width:1.3rem; height: 1.4rem;
        left:0; top:50%; transform: translateY(-50%);
        transition: left 0.2s, background-color 0.2s;
        background-color: #f47590;
    }
    input[type="checkbox"][role=switch]:checked::after {
        left:calc(100% - 1.3rem); background-color: #149d20;
    }

    CSS입니다. 체크 박스 ::after에 동그라미 단추를 넣어주고, before에는 틀을 만들어줬습니다. checked 상태에 따라 동그라미가 좌/우 끝으로 왔다 갔다 하게 됩니다. 결과물도 보시지요.

     

    결과물 렌더링 모습


    크롬 렌더링

    첫번째는 크롬에서 렌더링 된 모습입니다.

     

    파이어폭스 렌더링

    파이어폭스에서 렌더링 된 모습입니다. 단추가 살짝 밑으로 치우쳐 보이지만, 방법을 조금 달리하면 차이점을 줄일 수 있을겁니다.

     

    사파리 렌더링

    MacOS 사파리에서 렌더링 된 모습입니다.

     

    스크린 리더 호환성

    가장 중요한 부분입니다. 현재 VoiceOver를 제외한 스크린 리더에서 나름대로 해당 유형을 제공하고 있습니다.

    Sense Reader: 선택,  이름, 버튼

    NVDA: 켬, 이름 전환 버튼

    Narrator: 이름, 단추, 켬

    VoiceOver(macOS): 이름, 끔, 전환

    Talkback: 켬, 이름, 전환

    VoiceOver(iOS/iPadOS): 선택됨, 이름, 체크 박스

     

    JAWS는 테스트해보지 않았으나, iOS VoiceOver를 제외한 국내에서 많이 쓰거나 무료인 스크린 리더에서는 읽는 방식은 다르지만 잘 읽는 것을 볼 수 있습니다.

    댓글을 작성하려면 해주세요.
  • tip
    [web] improveAccessibility.js 파일에 announceForAccessibilityWithModal 함수 추가
    Webacc NV 2023-01-17 16:56:51

    웹에서 상황에 따라 화면에 없는 특정 메시지를 토스트 형태로 스크린 리더 사용자에게 알려야 할 때 사용할 수 있도록 announceForAccessibility 함수를 만들어 공유하였습니다.

    해당 함수의 특징은 특정 접근성 메시지를 뿌려준 후에 DOM에서 잽싸게 제거하여 스크린 리더가 메시지를 화살표로 내려서는 읽지 못하도록 한 것입니다.

    또한 해당 메시지가 DOM 가장 마지막에 뿌려졌다가 사라지도록 하였습니다.

    그런데 모달 대화상자가 열려 있을 때 대화상자 내에서 필터 초기화됨, 검색어 삭제됨과 같은 스크린 리더용 토스트 메시지를 뿌려 주어야 하는 경우도 있을 것입니다.

    현재 aria-modal 속성을 제대로 지원하는 스크린 리더는 iOS VoiceOver인데 role dialog, aria-modal true 속성으로 대화상자가 만들어지게 되면 철저하게 바깥 콘텐츠는 탐색되지 않도록 막습니다.

    따라서 aria-live 메시지가 대화상자 바깥에서 출력될 경우 VoiceOver에서는 해당 내용을 전혀 읽지를 못합니다.

    따라서 대화상자 내에서 토스트를 뿌려 주어야 할 경우는 announceForAccessibilityWithModal 함수를 참고할 수 있습니다.

    인자 값으로는 메시지가 생성될 요소와 메시지 문자 입니다.

    예시: announceForAccessibility(dialogParent, "이것은 접근성 토스트입니다.")

    즉 뒤의 메시지는 스크린 리더가 토스트로 읽을 메시지이고 위치할 요소는 모달이 열려 있을 때 해당 모달 div 요소를 지정해 주면 해당 요소의 하위 끝 부분에 aria-live 속성을 가진 메시지를 잠깐 동안 출력했다가 사라지게 만듭니다.

    즉 모달 내에서 어나운스가 발생되므로 모든 스크린 리더에서 메시지를 들을 수 있게 되는 것입니다.

    improveAccessibility.js 다운받기

    댓글을 작성하려면 해주세요.
  • news
    [기업 서비스 접근성 구현 사례 공유] 기획전 a11y 개선 프로젝트
    Webacc NV 2023-01-17 14:57:04

    최근 지마켓에서 발행된 접근성 적용을 위한 기획전 OCR 이미지 인식 적용 사례 관련 아티클이 있어 공유합니다. 좋은 접근성 적용 사례로 참고가 되었으면 좋겠습니다.

    기획전 a11y 개선 프로젝트 블로그 보러가기 

    댓글을 작성하려면 해주세요.
  • tip
    [android view system] containerAsCheckbox, containerAsSwitch 자바 및 코틀린 접근성 메서드 추가
    Webacc NV 2023-01-17 11:09:39

    접근성 유틸 클래스에 제목에서 언급한 바와 같이 두 가지 메서드를 추가하여 공유합니다.

    얼마전 작성한 동일한 클릭 속성을 가진 스위치, 체크박스와 텍스트뷰의 초점 합치기 팁에서 작성한 가이드 내용과 몇 가지 접근성을 조금 더 업데이트 하여 메서드 화 한 것입니다.

    해당 메서드는 체크박스와 텍스트뷰 또는 스위치와 텍스트뷰가 존재하고 클릭 속성이 두 뷰를 감싸는 컨테이너에 걸려 있어 접근성 초점이 두 개로 나누어지는 요소에 적용이 가능합니다.

    해당 메서드를 적용하면 다음과 같이 접근성 이슈가 해결됩니다.

    1. 힌트 메시지가 전환하려면 두 번 탭하세요로 변경됩니다. 이는 스위치, 체크박스는 전환 작업을 하는 요소이기 때문입니다. 

    2. 텍스트뷰의 텍스트를 contentDescription 형태로 가져와서 컨테이너뷰에 삽입하므로 상태정보 레이블, 요소 유형을 스크린 리더에서 설정한대로 들을 수 있습니다.

    3. 초점이 하나로 합쳐집니다.

    사용법은 간단합니다.

    뷰가 로딩되었을 때, 그리고 클릭 리스너가 실행되었을 때 해당 메서드를 실행하고 인자 값으로는 containerView, checkbox 또는 switchView, textView를 넣어줍니다.

    예시: a11yClass.containerAsCheckBox(clickableContainer, agreementCheckbox, contentText)

    이렇게만 적용하면 초점도 하나로 합쳐지고 마치 체크박스에 초점이 맞춰진 것처럼 힌트 메시지도 전환하려면 두 번 탭하세요로 출력되며 상태정보, 레이블, 요소 유형을 톡백에서 설정한 대로 읽어주게 됩니다.

    접근성 유틸 클래스 자바 다운로드

    접근성 유틸 클래스 코틀린 다운로드

    댓글을 작성하려면 해주세요.
  • tip
    [android view system] 접근성 적용 유틸 클래스에 buttonAsNewRoleDescription 메서드 추가
    Webacc NV 2023-01-14 17:31:30

    커스텀 뷰를 접근성에서 의미 있는 요소 유형으로 변경하기 위해서 만들어진 유틸 클래스에 buttonAsNewRoleDescription 메서드를 추가합니다.

    이는 기존 버튼 요소는 특성상 다른 요소 유형으로 변경을 해도 톡백에서 변경된 요소로 읽지 못하기 때문입니다.

    예를 들어 편집창 요소가 실제로는 편집창이 아닌 전화번호를 선택 혹은 변경하는 하는 드롭다운 요소라고 생각해 봅시다.

    이때 setAsDropdown 메서드를 사용하여 편집창을 드롭다운 즉 Spinner 클래스로 접근성 요소 유형을 변경하여 드롭다운 요소로 읽게 할 수 있습니다.

    그러나 버튼의 경우에는 기존에 가지고 있는 버튼 클래스를 라디오버튼 유형이나 토글버튼인 스위치 드응로 요소 유형을 아무리 변경해도 실제로는 기존 유형인 버튼으로만 읽습니다.

    따라서 buttonAsNewRoleDescription 메서드를 사용하여 새로운 요소 유형을 문자로 제공해 주면 해당 요소 유형으로 변경됩니다.

    인자 값으로는 버튼에 해당하는 객체와 요소 유형 스트링 메시지입니다.

    따라서 다음 예시와 같이 적용할 수 있습니다.

    accessibilityUtil.buttonAsNewRoleDescription(buttonView, "switch")

    접근성 유틸 클래스 자바 다운로드

    접근성 유틸 클래스 코틀린 다운로드

    댓글을 작성하려면 해주세요.
  • tip
    [jetpack compose] 제목 유형 주기와 관련된 참고사항
    Webacc NV 2023-01-05 16:33:53

    Jetpack compose에서는 다음 예시와 같이 semantics 모디파이어 내에서 heading() 속성을 추가하여 스크린 리더가 제목이라고 읽어주도록 구현할 수 있습니다.

    .semantics { heading() }

    단 제목 요소는 안드로이드에서는 요소 유형으로 취급하지 않기 때문에 jetpack compose에서도 다음 예시와 같이 버튼 제목과 같이 추가

    요소 유형을 추가할 수 있습니다.

    .semantics { heading() role = Role.Button}

    단 iOS VoiceOver와 달리 제목을 제외한 버튼 슬라이드와 같은 요소 유형 두 개는 추가할 수 없습니다.

    댓글을 작성하려면 해주세요.
  • tip
    [HTML] prefers-reduced-motion 탐지하여 롤링되는 배너, 처음부터 정지된 채로 구현하기
    Webacc NV 2022-12-31 17:53:04

    예전에 prefers-reduced-motion 미디어 쿼리 사용법에 대해 널리 블로그에서 다루었습니다.

    과도한 애니메이션에 대해 심한 불편함을 느끼는 분들을 위해 만들어진 미디어 쿼리입니다.

    그런데 해당 미디어 쿼리를 응용하여 애니메이션 줄이기 설정이 켜져 있으면 웹/앱에서 롤링되는 배너가 정지된 채로 로딩되게끔 구현하는 것을 고려해 볼 수 있습니다.

    게다가 이렇게 구현하면 스크린 리더 사용자에게도 사용성에 큰 도움을 줄 수 있습니다.

    스크린 리더 사용자도 웹 탐색 시 롤링되는 배너 때문에 많은 어려움을 겪는 경우가 많은데 웹페이지 구현 시에는 스크린 리더가 켜져 있는지를 캐치하여 스크린 리더 사용자를 위한 접근성을 조금 더 높인다든지 할 수 있는 방법이 없습니다.

    그러나 해당 미디어 쿼리는 주요 브라우저에서 다 동작하며 스크린 리더 사용자분들 중에는 반응 속도를 조금 더 빠르게 하기 위해서 설정에서 애니메이션 동작 줄이기를 켜 놓고 사용하는 분들도 있기 때문에 해당 쿼리를 캐치하여 배너가 롤링되지 않게 한다면 사용성이 훨씬 향상될 것이라 생각합니다.

    물론 이러한 부분들이 조금 더 우리나라에서 많이 활성화 되면 스크린 리더 사용자분들에게도 이러한 설정에 대해 알려야 할 수는 있을 것입니다.

    방법은 간단합니다. 아래 예시와 같이 스크립트에서 모션 동작 줄이기가 켜져 있으면 배너가 정지된 채로 있도록 조건문을 설정하면 됩니다.

    const reducedMotionMedia = matchMedia('(prefers-reduced-motion: reduce)');
    reducedMotionMedia.addEventListener("change",(media)=>{ // 중간에 켜거나 꺼도 작동하도록 설정
      options.paused = media.matches;
    });
    reducedMotionMedia.dispatchEvent(new Event("change")); // 이벤트를 발생시켜 페이지에 접속했을 때 멈춤 적용
    
    

     

    댓글을 작성하려면 해주세요.
  • tip
    [HTML] 이제 aria-hidden, tabindex-1 대신 inert 속성 사용을 고려해야 할 때
    Webacc NV 2022-12-31 10:39:34

    롤링되는 배너에서의 현재 화면에 보여진 배너와 감춰진 배너, 확장/축소되는 위젯의 애니메이션 기획으로 인해 하위 콘텐츠가 display:none / block, 혹은 visibility:hidden, visible로 감춰지거나 보여지는 경우가 아닐 때 탭이나 가상커서 상에서의 화살표 탐색, 모바일에서의 한 손가락 좌 또는 우 쓸기로 탐색 시 가려지는 요소들이 다 탐색되는 문제가 발생하게 됩니다.

    따라서 이를 해결하기 위하여 가려진 요소에는 aria-hidden true, 키보드 초점을 받는 곳에는 tabindex -1 속성을 적용하여 가려진 부분에 대한 접근성 문제를 해결해 왔습니다.

    그러나 이러한 방식은 해결방식에 대해 완전히 이해하지 못하고 반만 적용하거나 잘못 적용할 경우 오히려 적용을 하지 않는 것보다 못한 접근성 이슈들이 상당히 발생한 것이 사실입니다.

    대표적인 예가 aria-hidden 속성은 잘 적용했는데 tabindex -1 속성은 제공하지 않아 초점만 이동하는 경우입니다.

    이런 경우 스크린 리더가 지원하는 단축키 또는 제스처로 탐색하면 콘텐츠는 읽어주지 않는데 탭키로 접근하면 스크린 리더에 따라 내용을 읽어주거나 아무 것도 읽어주지 않는 희한한 상황이 나타나게 됩니다.

    반대로 tabindex -1 속성은 잘 제공되었는데 aria-hidden 속성을 제대로 적용하지 않은 경우에는 키보드 테스트를 하면 전혀 문제가 없어 보이는데 스크린 리더가 지원하는 제스처 및 단축키로 탐색하면 의도하지 않은 콘텐츠들이 다 접근됩니다.

    그런데 최근에 Chrome 기반의 브라우저, Safari 브라우저에서 inert라는 속성을 공식 지원하기 시작하였습니다.

    inert 속성은 aria-hidden, tabindex -1 속성을 동시에 적용해 주는 속성이라고 생각하시면 됩니다. 

    즉 해당 속성은 시각적으로는 아무런 영향을 주지 않으며 숨겨야 하는 영역이 포함된 div, li 등에 적용하면 하위의 요소들을 접근성 트리에서 제외함은 물론 키보드 이벤트 초점도 제외시키게 됩니다.

    따라서 해당 속성을 잘만 사용한다면 조금 더 쉽게 접근성 이슈를 해결할 수 있습니다.

    주의하실 것은 aria-hidden과 같이 inert 속성이 적용된 하위의 모든 요소가 다 사라지므로 여러 li 리스트 중 하나씩 보여지는 구조에서는 보여지는 li만 제외하고 각 li를 스크립트로 처리하여 inert 속성을 별도로 포함시켜 주어야 합니다.

    또한 롤링되는 배너에서는 배너 리스트 이전에 정지 버튼을 두어야 스크린 리더 사용자가 배너 정지를 먼저 한 상태로 배너 영역을 지나칠 수 있습니다.

    이는 배너 영역을 먼저 탐색하면 특정 배너에 초점을 받고 있다가 다른 배너로 교체되면서 초점을 잃을 수 있기 때문입니다.

    참고로 firefox에서는 베타 버전으로 해당 속성을 지원하기 시작하였습니다.

    다만 현재는 firefox에서는 inert 속성이 반만 동작하여 키보드 초점에서만 사라지고 접근성 트리에서는 사라지지 않는 이슈가 있으며 해당 문제는 조속히 해결될 것이라 기대합니다.

    그러나 앞에서도 언급한 바와 같이 크롬 및 사파리 기반 브라우저에서는 잘 동작하고 있습니다.

    아래는 저희가 제작한 inert 속성을 체험해 볼 수 있는 페이지입니다.

    inert 속성 체험하기

    댓글을 작성하려면 해주세요.
  • tip
    [UIKit] 테이블뷰 셀 내의 요소들을 접근성 배열에 담아야 할 때 주의사항
    Webacc NV 2022-12-30 16:32:23

    일반적으로 보이스오버에서는 접근성 순서가 틀어질 경우 틀어지는 요소들이 있는 상위 수퍼뷰를 기준으로 containerView.accessibilityElements = [b, c, a, d]와 같이 배열을 정의함으로써 초점 순서를 재정의합니다.

    이 방법은 초점 순서가 틀어졌을 때 재정의할 때도 사용하지만 특정 요소들만 접근성 초점으로 제공해야 할 때에도 사용할 수 있습니다.

    문제는 특정 테이블뷰셀 내의 요소들을 접근성 배열에 담는 경우입니다.

    TestTableCell.swift라는 파일이 있다고 가정하고 다음과 같이 접근성 배열을 정의하였다고 생각해 봅시다.

    self.accessibilityElements = [2, 1, 4, 3]

    그러면 어떤 문제가 있을까요?

    테이블뷰 요소를 한 손가락 쓸기로 탐색할 때 스크롤이 안 됩니다.

    즉 총 30행까지 있다고 가정하고 한 화면에 6행까지 있다고 가정하면 7행부터는 한 손가락 쓸기를 통해서는 탐색을 할 수 없다는 이야기입니다.

    이유는 각 셀에 기본적으로 생성되는 contentView라는 요소를 거치지 못하기 때문에 발생하는 것으로 파악하였습니다.

    따라서 테이블뷰셀 내의 여러 요소들을 accessibilityElements로 배열에 담을 때에는 반드시 contentView를 거치도록 합니다.

    self.contentView.accessibilityElements = [2, 1, 4, 3]

    댓글을 작성하려면 해주세요.
  • tip
    [swiftUI] UIKit과의 컨테이너 뷰 처리 방식의 차이 이해하기
    Webacc NV 2022-12-30 12:54:37

    테이블, 탭, 리스트와 같은 위젯 형태를 구현하는 것이 아닌, 일반 버튼이나 텍스트를 나열하는 경우 UIKit에서는 UIView를 사용하여 각 하위에 들어갈 요소들을 구성합니다. 

    swiftUI에서는 배열하고자 하는 목적에 따라 VStack, HStack, Zstack과 같은 컨테이너를 사용합니다.

    그런데 하위 뷰들의 초점이 여러 개인 경우 이를 합치기 위해서 UIKit에서는 상위 컨테이너인 UIView의 isAccessibilityElement를 true로 설정하고 accessibilityLabel, accessibilityTraits, 필요한 경우 accessibilityActivate 메서드를 사용하여 접근성을 적용하게 되는데 이때 UIView의 isAccessibilityElement를 true로 설정하는 순간 하위 뷰들의 접근성 초점은 다 무시됩니다.

    그러나 swiftUI에서는 단순히 HStack과 같은 컨테이너에 .accessibilityLabel, .accessibilityAddTraits 모디파이어만 추가한다고 해서 하위 요소들의 초점이 무시되지 않습니다.

    오히려 상위 초점과 하위 초점 두 요소가 다 제공되어 사용자에게 혼란을 줄 수 있습니다.

    따라서 상위 요소에서 하위 요소의 초점을 하나로 합치려면 .accessibilityElement(children: .combine) 혹은 상황에 따라서는 .accessibilityElement(children: .ignore) 모디파이어를 사용하시기 바랍니다.

    ignore 사용 시에는 UIKit에서 UIView에 isAccessibilityElement true로 설정한 것과 같은 상황이 됩니다.

    댓글을 작성하려면 해주세요.
  • tip
    [flutter] 커스텀 슬라이더 접근성 적용하기
    Webacc NV 2022-12-26 09:34:50

    플러터에서는 Semantics 위젯 내의 onIncrease, onDecrease 메서드를 통해 커스텀 슬라이더를 구현할 수 있습니다. 

    커스텀 슬라이더를 구현하는 경우의 수는 여러 가지이겠지만 플러터에서는 PageView 위젯 구현 시에는 슬라이더 형태로 접근성을 적용하는 것이 좋습니다. 

    이는 네이티브 PageView 위젯을 사용하였다 하더라도 플러터 엔진에서 이를 커스텀으로 구현하기 때문에 안드로이드 TalkBack만 놓고 보더라도 두 손가락으로 페이지 넘기기를 할 때 몇 페이지 중 몇 페이지와 같은 페이지 정보도 말하지 않으며 한 손가락 쓸기로 페이지 내부로 들어가더라도 멀티페이지뷰 혹은 페이지 혹은 페이지 내부와 같은 정보를 읽어주지 않기 때문입니다.

    따라서 아래 코드 예시와 같이 슬라이더 형태로 접근성을 구현할 수 있으며 이렇게 하면 스크린 리더 사용자는 한 손가락 위 또는 아래 쓸기로 페이지 자체를 전환할 수 있으므로 사용성이 훨씬 향상됩니다.

    단 반두시 Semantics 내에서 liveRegion 속성은 true로 설정해야 슬라이더 전환 시 값을 읽어주게 됩니다.

                                Semantics(
                                  liveRegion: true,
                                  onIncrease: () => _setPageIndex(_wonderIndex + 1),
                                  onDecrease: () => _setPageIndex(_wonderIndex - 1),
                                  child: WonderTitleText(currentWonder, enableShadows: true),
                                ),
    

     

    댓글을 작성하려면 해주세요.
  • tip
    [flutter] TalkBack에서 실행 가능한 요소에 대한 힌트를 제공하지 못할 때 점검 사항
    Webacc NV 2022-12-25 13:40:34

    flutter 위젯 중에서 Image, Card와 같은 요소를 탭했을 때 무언가가 실행되게끔 하려면 GestureDetector라는 뒤젯을 사용해야 합니다.

    GestureDetector 위젯 내에 onTap, onLongPress 속성을 사용하여 탭했을 때, 길게 탭했을 대의 기능을 구현할 수 있습니다.

    이렇게 구현하면 안드로이드에서는 클릭, 롱클릭 속성이 있다고 판단하고 톡백에서 해당 요소에 포커스 했을 때 활성화 하려면 두 번 탭하세요, 두 번 탭하고 길게 누르세요 라는 힌트 메시지를 출력하게 됩니다.

    그러나 어떤 경우에는 onTap, onLongPress 이벤트가 구현되어 있음에도 TalkBack에서 해당 힌트를 읽어주지 못하는 경우가 있습니다.

    이때 점검해야 할 사항은 단 하나, GestureDetector 위젯으로 인해 onTap 혹은 onLongPress 이벤트가 수신받고 있는 요소에 접근성 포커스가 가는 것이 맞는가 입니다.

    예를 들어 리스트뷰 내에 각 항목들이 구현되어 있는데 Card > Row > Image & Text 와 같은 구조로 되어 있다고 생각해 봅시다.

    이때 이벤트를 받고 있는 곳은 Card입니다. 그러나 접근성 초점이 가는 곳은 Image & Text입니다.

    따라서 이런 구조에서는 톡백에서 힌트를 받지 못하게 되는 것입니다.

    이를 해결하려면 탭 이벤트를 받고 있는 곧 바로 하위에 Semantics를 덧씌우고 하위 요소는 excludeSemantics로 접근성 노드에서 제거한 다음 label 속성을 통해 하위 요소의 텍스트를 추가해 주어야 합니다.

                    return GestureDetector(
                      child: Semantics(
                        label: list[position].animalName!,
                        excludeSemantics: true,
                        child: Card(
                          child: Row(
                            children: <Widget>[
                              Image.asset(
                                list[position].imagePath!,
                                height: 100,
                                width: 100,
                                fit: BoxFit.contain,
                              ),
                              Text(list[position].animalName!),
                            ],
                          ),
                        ),
                        onTap: () {
                        },
                        onLongPress: () {
                        },
                      ),
                    );

     

    댓글을 작성하려면 해주세요.
  • tip
    [jetpack compose] 톡백의 접근성 초점을 반드시 특정 요소로 보내야 할 때
    Webacc NV 2022-12-17 14:11:55

    안드로이드는 키보드 초점과 접근성 초점이 별도로 작동합니다. 따라서 특정 상황에서 키보드 초점을 a라는 요소로 보내게 되면 접근성 초점이 따라가지만 접근성 초점을 특정 요소로 보낸다고 해서 키보드 초점이 따라가지는 않습니다.

    또한 키보드 초점이 갈 수 있는 요소는 클릭 가능한 요소나 강제로 focusable 속성을 true로 지정한 요소만 가능하지만 접근성 포커스는 강제로 접근성 초점을 숨겨버리지 않는 이상 클릭할 수 없는 텍스트뷰에도 모두 초점이 이동합니다.

    안드로이드 view 시스템의 경우 접근성 초점을 다른 요소로 보내는 별도의 메서드가 존재했습니다. 그러나 jetpack compose에서는 아직 관련 API가 없습니다.

    그러나 focusRequester 모디파이어와 requestFocus 메서드를 활용해서 키보드 초점을 어딘가로는 보낼 수가 있습니다.

    따라서 접근성 초점을 반드시 특정 요소로 보내야 한다면 requestFocus를 활용할 수 있습니다.

    방법은 다음과 같습니다.

     

    1. FocusRequester 클래스를 활용하여 아래 예시와 같이 변수를 하나 만듭니다.

    val focusRequester = remember { FocusRequester() }

    2. 초점을 보내고자 하는 요소의 포커스 상태를 캐치하기 위해 아래 예시와 같이 onFocusChanged 모디파이어를 추가합니다.

    .onFocusChanged { it }

    3. 포커스를 보내야 하는 요소에 focusRequester 모디파이어를 추가합니다. 인자 값은 앞에서 만든 변수입니다.

    .focusRequester(focusRequester)

    4. 포커스를 보내야 하는 요소가 버튼이나 포커스가 가능한 클릭 속성을 가진 요소가 아니라면 focusable true 모디파이어를 추가합니다.

    참고로 위의 2, 3, 4번 순서를 반드시 따라야 합니다. 

    5. 초점을 보내야 하는 시점에 아래 예시와 같이 requestFocus 이벤트를 실행합니다.

    .clickable { focusRequester.requestFocus() }

     

    이렇게 하면 키보드 초점이 해당 요소에 가게 되므로 자연스럽게 톡백의 접근성 포커스도 이동합니다. 그러나 당연하게도 한번 간 포커스는 해당 요소의 포커스를 다른 곳으로 옮기기 전까지 다시 포커스를 이동시킬 수는 없습니다. 즉 키보드가 아닌 톡백 제스처로 다시 다른 요소로 이동하더라도 키보드 포커스는 여전히 해당 초점에 머물러 있기 때문입니다.

    댓글을 작성하려면 해주세요.
  • etc
    태그가 잘못 닫혀있어요
    틀렸어요 2022-12-13 10:50:08

    https://nuli.navercorp.com/guideline/s01/g01 에 잘못된 정보가 있어요.

    button 태그인데 a 태그로 닫혀있어요.

     

    잘못된 태그

    댓글을 작성하려면 해주세요.
  • tip
    [iOS-VoiceOver 알림] DispatchQueue보단 Task와 Async Await를 사용해요!
    Webacc NV 2022-11-18 18:29:12

    VoiceOver에서 버튼을 누르거나, 특정 기능이 실행되었을 때, 사용자에게 즉시 내용을 음성으로 전달해야 될 때가 있습니다. 이전 솔루션에서는 보통 DispatchQueue.main.asyncAfter를 많이 사용했습니다. 단일 알림만 사용할 때, DispatchQueue.main의 asyncAfter 클로져만으로도 충분히 이를 구현할 수 있지만, 여러 개의 알림을 보내거나, 지속적으로 멀리 있는 뷰의 변경내용을 알려야 할 때, DispatchQueue는 오류를 낼 수 있습니다.

    물론 낮은 Swift 버전 환경에서는 DispatchQueue를 써야하는 상황이겠지만, 대부분의 Apple 앱 개발환경은 최신 안정화 버전에서 진행됩니다. 그렇기 때문에 DispatchQueue보다 Task를 쓰는게 조금 더 안정적인 알림을 줄 수 있을 것으로 보입니다. 아래 코드를 보시지요.

    // Before
    func sendAnnouncementForVoiceOver(_ message:String,_ isUrgentMessage:Bool = true, speechDelay:Double)->Void {
      let NSAttrStr:NSAttributedString = NSAttributedString(string:message,attributes:.accessibilitySpeechQueueAnnouncement:!isUrgentMwssage)
      DispatchQueue.main.asyncAfter( deadline: .now() + 0.1 ) {
        UIAccessibility.post(notification:.announcement,arguments:NSAttrStr) 
      }
    }
    
    // After
    func sendAnnouncementForVoiceOver(_ message:String,_ isUrgentMessage:Bool = true, speechDelay:Double=100) async throws -> Void {  
      let NSAttrStr:NSAttributedString = NSAttributedString(string:message,attributes:.accessibilitySpeechQueueAnnouncement:!isUrgentMwssage)
      Task {
        try await Task.sleep( for:.miliseconds(speechDelay) )
        UIAccessibility.post(notification:.announcement,arguments:NSAttrStr)
      }
    }

    Task를 사용하는 것이 코드가 조금 길고 한 줄 더 많습니다. 그렇지만, DispatchQueue.main은 메인쓰레드에서 이를 처리하기 때문에 이를 제거해주지 않으면 해당 큐 자리에서 다른 음성을 실행하지 못하고 오류가 발생합니다.

    반면에 Task 클로져에 Task.sleep await을 사용하면 여러 알림을 사용하더라도 큐 위치가 겹치지 않기 때문에 오류 없이 무난하게 사용자에게 스크린리더 알림을 줄 수 있습니다.

    댓글을 작성하려면 해주세요.
  • qna
    피씨에서 폼 요소에 마우스 클릭 시 포커스 효과 있어야 할까요?
    능소니 2022-11-07 16:12:58

    피씨에서 키보드가 아닌 마우스로 폼 요소를 클릭 시

    outline이나 기타 등등으 효과로 클릭이 되었다는 표시를 해주어야 접근성에 옳바른 걸까요?

    댓글을 작성하려면 해주세요.
  • tip
    [iOS VoiceOver & Web] VoiceOver 초점 버그를 방지하는 실시간 영역 제공방법
    Webacc NV 2022-11-02 15:36:12

    iOS VoceOver는 콘텐츠의 크기, 렌더링에 매우 민감합니다. 우리가 상상한 것 이상으로 짜증나게 말이지요.

    특히 웹 플랫폼에서는 노드의 지속적으로 반복하여 새로 그려지는 과정에서 DOM 객체가 계속 새로 만들어지면
     VoiceOver로 정상적인 탐색이 불가능해지는 문제가 있습니다.

     실시간으로 누적 판매량을 바꿔가며 표시하거나, 실시간으로 시/분/초/밀리 초 등을 표시하는 실시간 D-Day 영역 등,
     실시간 정보 영역에서 이런 문제가 두드러집니다.
     
    아래는 저희가 운영하는 테스팅 사이드로, 여러 예제들을 담고 있습니다. 그 중, 실시간 정보 영역에 대한 페이지를 한번 보세요.
    실시간 렌더링 영역 테스트

    일부러 집중을 위해 aria-live는 걸어 놓지 않았습니다. 해당 페이지에는 계속 업데이트되는 두 개의 시계 영역이 있습니다.
    첫번째 시계영역은 VoiceOver 순차탐색으로 지나갈 수 없는 상태이며 두번째 시계 영역은 아무 문제 없이 탐색 후 지나갈 수 있는 상태입니다. 해당 페이지에도 설명을 해 놓았지만, 영문이므로 이 둘의 차이점을 설명하자면, 두 영역은 기존 노드에서 부분적인 것만 변경하는가, 새로운 노드가 계속 새로 교체되는가에 차이가 있습니다.
    첫번째 영역은 노드(span) 전체를 0.1초(100ms)마다 통째로 새로운 정보와 함께 교체합니다. 두 번째 영역은 span 태그가 있고, 내부에 있는 텍스트만을 계속 변경하여 표시합니다.

    데이터를 새로 고칠 때, DOM 객체를 통째로 새로 고치느냐, 기존 DOM 객체를 그대로 두고, 데이터만을 텍스트로 교체하느냐는 이토록 큰 차이가 있습니다. 요소를 통째로 빠르게 반복하며 교체하면 모바일 VoiceOver는 이를 인지하지 못하고, 심지어, 탐색 초점이 화면 끝에 있는 것 처럼, 탐색이 되지 않고 VoiceOver에서 콘텐츠가 끝났다는 퉁퉁 거리는 효과음만 들리게 됩니다.

    따라서, 데이터를 새로 실시간으로 가져와서 뿌려야 할 때는 되도록 DOM 객체(요소)를 통째로 그리지 말고, 미리 그려진 고정된 DOM 객체에 텍스트만을 교체하는 방식을 사용해야 합니다.

    댓글을 작성하려면 해주세요.
  • tip
    [swift UI] 보이스오버에서의 이중탭 액션 재정의하기
    Webacc NV 2022-10-30 10:49:26

    접근성에서 조금 더 편리한 사용성을 제공해 주기 위하여 기존의 객체들을 접근성 초점에서 제거하고 상위 뷰에 초점을 주는 경우가 있습니다.

    그런데 이 때 주의해야 하는 것이 바로 이중탭입니다.

    예를 들어 보이스오버 끈 상태에서 옆으로 스와이프를 하면 결제수단이 변경되는 화면이 있다고 생각해 봅시다.

    해당 화면의 접근성을 적용하기 위하여 결제수단들을 품고 있는 상위 요소에 초점을 주고 .accessibilityAdjustableAction 모디파이어로 한 손가락 위 또는 아래 쓸기로 결제 수단을 변경할 수 있도록 구현을 하였습니다.

    그런데 특정 결제 수단에는 계좌 충전 버튼이 있다고 생각해 봅시다.

    접근성 초점은 상위 뷰에만 주었기 때문에 충전 버튼은 초점을 받을 수 없습니다.

    이런 경우는 슬라이더 뿐만 아니라 두 번 탭에 대한 액션도 재정의를 해 주어야 합니다.

    그렇지 않으면 사용자가 이중탭을 해야 한다는 것도 모를 것이며 설사 이중탭을 한다고 하더라도 초점이 전체를 잡고 있기 때문에 액션이 실행되지도 않습니다.

    이 때 사용할 수 있는 것이 .accessibilityAction 모디파이어입니다.

    이중탭 재정의는 액션 카인드에 (default)를 정의하고 핸들러 인자에 실행될 액션을 추가하면 됩니다.

    다만 아래 예시와 같이 (default) 액션 kind는 생략이 가능합니다. 

    				.accessibilityAction {
    					self.showCharge(true)
    				}

     

    참고로 이중탭에 대한 액션을 지정하게 되면 버튼 트레이트가 자동 추가됩니다.

    따라서 위의 예시에서 생각해 본다면 버튼 조절가능으로 읽게 되는 것입니다.

    댓글을 작성하려면 해주세요.
  • tip
    [swift UI] 화면 변경 알림과 접근성 포커스 재조정하기
    Webacc NV 2022-10-29 14:44:52

    UIKit에서는 UIAccessibility.post(notification)을 사용하여 접근성 초점을 특정 요소로 보낼 수 있었습니다.

    그러나 swiftUI에서는 해당 이벤트 사용은 가능하지만 UIKit 형식으로 view를 추가하지 않는 이상 특정 요소로 접근성 초점을 보내는 것은 불가능합니다.

    즉 screenChanged, layoutChanged 사용시 argument는 반드시 nil로 주어야 하고 그렇게 되면 무조건 첫 요소로 접근성 초점이 이동합니다.

    대신 swiftUI에서는 accessibilityFocused 모디파이어를 사용하여 특정 요소로 접근성 초점을 보낼 수 있습니다.

    bool @AccessibilityFocus 스테이트 변수를 만들고 포커스를 보내고자 하는 요소에 accessibilityFocus 모디파이어를 통하여 해당 스테이트 변수를 넣어 준 다음 포커스를 보내야 하는 시점에 스테이트 변수를 true로 설정하면 끝입니다.

    따라서 .sheet(isPresented:) 모디파이어를 사용하여 네이티브 모달을 구현하는 경우에는 모달이 열리거나 사라질 때 자동으로 화면변경 알림 이벤트를 보이스오버에 제공해 주어서 모달을 닫을 때 접근성 포커스 재조정만 해 주면 되지만 완전히 모달 레이어를 opacity를 통하여 투명도를 변경함으로써 커스텀으로 구현하게 되면 screenChanged 이벤트도 함께 구현해야 합니다.

    구현 방법은 너무나 간단합니다.

    1. 다음 예시와 같이 AccessibilityFocus 스테이트를 지정합니다.

    @AccessibilityFocusState var isNameButtonFocused: Bool

    2. sheet 모디파이어가 아닌 커스텀 모달을 구현한다고 가정하고 모달이 열릴 때 다음 예시와 같이 screenChanged 이벤트를 구현합니다. 모달이 열릴 때에는 초점을 재조정할 필요가 없고 사라질 때에만 기존 모달을 여는 버튼으로 되돌려 주면 되므로 접근성 포커스는 재조정하지 않습니다.

    Button(action: {

     self.showModal(true)
     UIAccessibility.post(notification: .screenChanged, argument: nil)
    }

    3. 2번의 버튼은 모달을 닫을 때 초점을 해당 버튼으로 되돌려 줄 것이므로 1번에서 AccessibilityFocusState로 지정한 isNameButtonFocused를 accessibilityFocused 모디파이어 값으로 지정합니다.

    .accessibilityFocused($isNameButtonFocused)

    이렇게 되면 특정 조건에서 isNameButtonFocused가 true로 변경되는 순간 접근성 초점은 해당 버튼으로 이동하게 됩니다.

    4. 모달을 닫을 때는 다음 예시와 같이 접근성 초점 되돌리기와 화면 변경 알림을 함께 구현합니다.

    			.onChange(of: self.showModal) { isShow in
    				if !isShow {
    					if isName {
    						isNameButtonFocused = true
    				}
    			}
    

     

    참고: 당연한 이야기이지만 커스텀 모달 구현시에는 모달을 포함하는 뷰에 isModal trait를 추가해야 합니다(sheet 모디파이어 사용시 필요 없음).

    댓글을 작성하려면 해주세요.
  • tip
    [android view system] 동일한 클릭 속성을 가진 스위치, 체크박스와 텍스트뷰의 초점 합치기
    Webacc NV 2022-10-26 21:18:10

    켜짐/꺼짐을 나타내는 스위치, 선택됨/선택안됨을 나타내는 체크박스를 구현할 경우 체크박스 혹은 스위치와 텍스트뷰를 양 옆으로 분리하고 이 둘을 감싸는 레이아웃에 클릭 리스너를 주는 경우가 있습니다.
    이렇게 되면 터치할 수 있는 범위가 넓어지고 어느 쪽을 탭하든 스위치나 체크박스가 동작하게 됩니다. 
    그런데 톡백 사용자는 이러한 화면에서 혼란을 겪게 되는데 그것은 바로 초점이 두 개로 이동될 뿐만 아니라 두 요소 다 클릭이 가능해서 활성화 하려면 두 번 탭하세요, 전환하려면 두 번 탭하세요 라는 힌트 메시지를 출력하므로 동일한 요소라는 것을 알 수 없다는 것입니다.
    물론 이런 경우 둘 중 하나를 접근성 초점에서 없애버릴 수 있으나 이렇게 되면 저시력 사용자가 톡백을 켠 상태로 화면에 있는 요소를 임의 터치했을 때 요소 하나는 화면에 있음에도 접근성 트리에서 숨겨져서 초점이 가지 않기 때문에 당황할 수 있습니다.
    그래서 초점도 하나로 합치고 어느 쪽을 터치하든 잘 읽어줄 수 있도록 하는 방법을 공유하려고 합니다.

    방법은 너무나도 간단합니다.
    레이아웃에 클릭 이벤트가 포함되어 있으므로 하위 체크박스나 스위치의 클릭 속성은 false로 변경합니다.

    checkBox.isClickable = false

    이렇게만 하면 완전히 끝입니다. 

    참고로 클릭 리스너가 있는 레이아웃에 contentDescription 속성을 주면 하위 모든 요소를 다 무시하고 해당 대체 텍스트만 읽게 되어 상황에 따라 활용이 가능합니다. 다만 이렇게 하려면 클릭 리스너를 가진 레이아웃에 체크박스나 스위치 요소 유형 및 상태정보를 제공해야 하며 이럴 경우 유틸 클래스를 활용할 수 있습니다.

    댓글을 작성하려면 해주세요.
  • tip
    [swiftUI] 두 손가락 두 번 탭 제스처 구현하기
    Webacc NV 2022-10-10 19:10:09

    얼마전 .accessibilityAction(escape) 모디파이어를 통해서 뒤로 가기 제스처를 구현하는 방법에 대해 다루었습니다.

    오늘은 .accessibilityAction(magicTap) 모디파이어에 대해 다루겠습니다.

    재생, 일시정지와 같이 두 손가락 두 번탭으로 수행할 액션을 정의할 때 사용하는 모디파이어이며 UIKit에서는 accessibilityPerformMagicTap 메서드를 오버라이드 하는 형식으로 구현하였습니다.

    구현 방법은 간단합니다. 

    해당 뷰의 가장 상위 모디파이어에 다음 예시와 같이 두 손가락을 두 번 탭 했을 때 실행될 기능을 조건문 형태로 구현하면 됩니다.

    다만 주의하실 것은 일시정지와 재생과 같이 텍스트가 변경되어야 하는 경우 state 변수를 참조하여 실시간으로 텍스트 혹은 대체 텍스트 및 이미지 또한 변경해 주어야 합니다. 

    그렇지 않으면 버튼의 텍스트 레이블과 기능이 불일치하는 문제가 생깁니다.

    		.accessibilityAction(.magicTap) { // 접근성 적용 - 재생/정지
    			if self.audioPlayer.isPlaying {
    				self.audioPlayer.pause()
    				playCheck = false
    			} else {
    				self.audioPlayer.play()
    				playCheck = true
    			}
    		}
    

     

    댓글을 작성하려면 해주세요.
  • tip
    [WAI-ARIA Advance] aria-labelledby와 aria-describedby로 내용이 긴 버튼을 더 이해하기 쉽게 만들기
    Webacc NV 2022-10-05 17:13:10

     누를 수 있는 버튼이나 링크에는 가급적 간결하고 짧은 레이블을 사용하는 것이 바람직합니다.

    그러나, 위젯 스타일의 아름다움이나, 컨트롤의 크기, 눈으로 보는 사람들이 무슨 버튼인지 알기 쉽게 하기 위해, 혹은 홍보 효과를 위해 이미지와 제목 아래에 설명을 포함하는 링크나 버튼을 많이 사용합니다.

    안에 있는 내용을 빠짐없이 읽는다면, 이는 접근성에 문제가 되지 않습니다. 짧은 레이블이 바람직한 것과는 별계로 충분히 표준을 지킨 마크업이라고 할 수 있습니다.

    그런데, 플랫폼, 스크린리더 별로 다르지만, Windows와 NVDA를 기준으로 했을 때, 스크린 리더는 Tab 키(시스템 초점)을 통해 이동하면, 레이블 내용을 읽고, 그 후에 버튼이나 링크같은 요소 유형을 읽는게 일반적입니다. 그 다음, title이나 aria-description등의 내용을 읽게 됩니다.

    만약에, 버튼에 모든 글자를 다 넣어 놓는다면, 레이블이 너무 길어서 요소 유형을 듣기가 어려워지는 문제가 있습니다. 눈으로 보는 사람은, 보고싶지 않은 글자는 훑고, 보고 싶은 부분만 보면 되니, 이해가 잘 안 가시겠지만, 스크린리더 사용자는 해당 내용을 묵묵히 듣는 수 밖에 없습니다. 레이블과 요소 유형만 듣고 싶은 상황이 있은 사람은 고구마를 백 개쯤 먹은 느낌일 거예요.

    그러면, 이렇게, 긴 내용이 들어있는 버튼은 어떻게 정보를 재구성하는 게 좋을까요? aria-labelledby와 aria-describedby를 사용하면 됩니다. 이 둘을 쓰려면 id를 줘야하지 않는지 물어보고 싶지요?

    맞습니다. id를 주어야 하죠. 그런데, HTML에서 접근성을 구현할 때, id 속성은 기본 참조수단으로 활용됩니다. 접근성을 위해, 중요한 요소에는 id는 당연히 주는 것이 좋습니다. 일일이 주기 귀찮아도 꾹 참고 id와 WAI-ARIA를 사용하면, 사이트의 품질, 품격을 높일 수 있습니다.

    무엇을 활용해야 하는지는 설명했으니 어떻게 마크업 하면 되는지 살펴보도록 하죠.

    <h1>Long Label Button</h1>
    <main>
      <div class="comparison">
        <div class="markup normal">
          <h2>Normal</h2>
          <button>
            <div class="imgbox" id="rcmenu_imgbox"><img src="./steak.jpg" alt=""></div>
            <p class="lb-wrap" id="rcmenu_lb">Chef-Recommended Menu</p>
            <p class="desc-wrap" id="rcmenu_desc">사시사철, 매달 바뀌는 신선한 제철 채소, 해산물, 심혈을 기울인 드라이 에이징한 스테이크, 셰프의 추천 메뉴를 만나보세요.</p>
          </button>
        </div>
        <div class="markup a11y">
          <h2>접근성 계선됨</h2>
          <button aria-labelledby="spct_lb" aria-describedby="spct_desc">
            <div class="imgbox" id="spct_imgbox"><img src="./coffee.jpg" alt=""></div>
            <p class="lb-wrap" id="spct_lb">Specialty Coffee</p>
            <p class="desc-wrap" id="spct_desc">엄선된 생두로 당일 로스팅, 매일 최고의 맛과 향을 느낄 수 있는 숙성된 원두, 황금색 크래마와 아로마를 느껴보세요.</p>
          </button>
        </div>
      </div>
    </main>

    자, 두 개의 버튼이 완성되었습니다. WAI-ARIA와 스크린리더에 익숙하지 않은 분은 이 마크업이 어째서 듣고 이해하기 편한 구조인지, 와 닿지 않을 겁니다. 왜 그런지 한번 NVDA 접근성 적용 비교(Youtube 동영상)을 통해 비교해 보지요.

    센스리더는 예외로, describedby를 마치 레이블처럼 읽는 특성이 있음을 유의하시기 바라며, NVDA나 해외 스크린리더의 경우는 이렇게 버튼 뒤에 describedby의 내용을 읽습니다. 이는 VoiceOver(Youtube 동영상)Talkback(Youtube 동영상)도 마찬가지이지요.

    이렇게 짧은 내용을 aria-labelledby로 주고, describedby로 긴 내용을 제공한다면, 간략한 내용과 요소 유형을 읽은 다음, 설명을 읽어주게 되므로, 스크린리더 사용자가 설명 듣기 여부에 대한 선택권을 줄 수 있고, 버튼 레이블과 설명이 명확히 구분되기 때문에 조금 더 내용에 집중하기 좋습니다.

    댓글을 작성하려면 해주세요.
  • tip
    [iOS native] webView 구현 시 screenChanged 적용이 중요한 이유
    Webacc NV 2022-09-28 18:56:43

    앱 내에 웹뷰를 구현할 경우 페이지 DOM 자체가 새로고침되는 경우의 접근성 적용에 대해서 생각해 보려고 합니다.

    react와 같이 업데이트 된 콘텐츠만 갱신되는 경우에는 페이지 내에서 초점 이동이나 알림 등을 통하여 콘텐츠가 업데이트 되었음을 스크린 리더 사용자에게 알려 주어야 합니다.

    그러나 페이지 전체가 새로고침되는 경우에는 앱 내에서 이에 대한 접근성을 적용해야 합니다.

    그렇게 하지 않으면 보이스오버 초점이 어중간하게 이동될뿐만 아니라 페이지 전체가 새로고침되었음을 스크린 리더 사용자가 인지하기 어렵습니다.

    방법은 너무나도 간단합니다.

    웹뷰를 구현하게 되면 페이지 로딩 진행률을 표시하는 progressView가 포함되어 있는데 다음 예시와 같이 진행률이 100%가 되었을 때 screenChanged 이벤트를 추가하기만 하면 됩니다.

            func webView(_: WKWebView, didFinish _: WKNavigation!) {
                UIView.animate(withDuration: 0.33,
                               animations: {
                                   self.progressView.alpha = 0.0
                               },
                               completion: { isFinished in
                                   self.progressView.isHidden = isFinished
                })
                UIAccessibility.post(notification: .screenChanged, argument: webView.title)
            }

     

    댓글을 작성하려면 해주세요.
  • tip
    [SwiftUI - Environment] 접근성 서비스 구현 시 유용한 환경정보 검사하기
    Webacc NV 2022-09-22 13:32:48

    UIKit에서는 UIAccessibility.isVoiceOverRunning과 같이 get 프로퍼티를 통해 사용자가 VoiceOver를 사용중인지 아닌지를 체크할 수 있었습니다. SwiftUI에서는 get 프로퍼티로 객체에서 가져오는 방법이 없어 조금은 당황스러울 것으로 예상됩니다.

     

    SwiftUI에서는 @Environment() PropertyWrapper로 해당 값들을 참조할 수 있습니다. 확인 가능한 정보는 다음과 같습니다.

     

    • .accessibilityQuickActionEnabled:bool
      Assistive Touch 등 접근성의 빠른 동작 기능의 사용 여부를 부울형으로 가져옵니다.
    • .accessibilityVoiceOverEnabled:Bool
      손쉬운 사용 > VoiceOver 스크린리더의 사용 여부를 부울형으로 가져옵니다.
    • .accessibilitySwitchControlEnabled:Bool
      손쉬운 사용 > 스위치 제어 기능의 사용 여부를 부울형으로 가져옵니다.
    • .accessibilityEnabled:Bool
      손쉬운 사용의 접근성 보조 기술, VoiceOver, 스위치 제어 등의 기능 사용 여부를 부울형으로 가져옵니다.
    • .accessibilityReduceMotion:Bool
      손쉬운 사용 > 동작의 동작 줄이기 기능의 사용 여부를 부울형으로 가져옵니다.
    • .accessibilityInvertColors:Bool
      손쉬운 사용 > 디스플레이 및 텍스트 크기의 스마티 및 클래식 색상 반전 기능의 사용 여부를 부울형으로 가져옵니다.
    • .accessibilityDifferentiateWithoutColor:Bool
      손쉬운 사용 > 디스플레이 및 텍스트 크기의 색상 없이 구별 기능의 사용 여부를 부울형으로 가져옵니다.
    • .accessibilityShowButtonShape:Bool
      손쉬운 사용 > 디스플레이 및 텍스트 크기의 버튼 모양 기능의 사용 여부를 부울형으로 가져옵니다.
    • .accessibilityLargeContentViewer:Bool
      손쉬운 사용 > 디스플레이 및 텍스트 크기의 더 큰 텍스트 기능의 사용 여부를 부울형으로 가져옵니다.
    • .LegibilityWeight : LegibilityWeight?
      손쉬운 사용 > 디스플레이 및 텍스트 크기의 볼드체 텍스트 기능의 사용 여부를 가져옵니다. .regular와 .bold값이 있으며, 비교식을 활용하여 사용합니다.
    • .colorScheme : ColorScheme
      현재 테마 설정을 가져옵니다. .light와 .dark가 있습니다. 비교식을 활용하여 사용합니다.
    • .colorSchemeContrast : ColorSchemeContrast
      손쉬운 사용 > 디스플레이 및 텍스트 크기의 대비 증가 옵션이 사용 여부에 따른 상태를 가져옵니다. .standard와 .increased가 있으며, 비교식을 활용하여 사용합니다.

    @Environment 속성 래퍼는 다음과 같이 사용합니다.

    // ContentView.swift
    import SwiftUI
    
    struct ContentView : View {
      @Environment(\.accessibilityVoiceOverEnabled) var isVOEnabled:Bool
      @Environment(\.colorScheme) var currentScheme:ColorScheme
      var body:some View {
        HStack {
           Text("VoiceOver is turned "
           Text(isVOEnabled ? "On" : "OFF").foregroundColor(isVOEnabled ? .green : .red)
        }.accessibilityElement(children:.combine)
        HStack {
           Text("Your current theme is "
           Text(currentScheme == .dark ? "Dark" : "Light")
        }.accessibilityElement(children:.combine)
      }
    }
    
    //...

     

    댓글을 작성하려면 해주세요.
  • tip
    [iOS SwiftUI] 모달 대화상자의 뒤로가기(두 손가락 문지르기) 제스쳐 구현하기
    Webacc NV 2022-09-22 11:13:42

    UIKit에서는 모달 영역의 접근성을 구현할 때, accessibilityPerformEscape() 메소드를 사용하는데, 클래스에서 이 메소드를 override하여 구현해야만 했습니다. SwiftUI에서는 이를 어떻게 구현할까요?

    SwiftUI에서는 accessibilityAction이라는 Accessibility Modifier를 사용합니다. accessibilityAction에는 여러 오버로드된 항목이 있는데, 이 중에서. UIKit에서 구현하던 여러 접근성 제스쳐 기능을 구현하는 항목은 View.accessibilityAction(_ actionKind:AccessibilityActionKind, _ handler:@escaping ()->Void)->AccessibilityAttachmentModifier입니다. 이는 훨씬 세련되고 간편한 방식입니다.

    두 인자 모두, 아실 분들은 바로 아시겠지만, 호출 시 레이블을 필요로 하지 않습니다. 인자를 설명하자면 아래와 같습니다.

    actionKind 인자는 AccessibilityActionKind의 프로퍼티를 받습니다. 역시 아실 분들은 아시겠지만, 레이블이 필요 없기 때문에, 정의할 때, 첫번째 인자에서 마침표를 입력하면 유효한 값이 표시됩니다.

    handler 인자는 너무나도 익숙하실 겁니다. 실행될 코드를 작성하는 클로저입니다. 딱히 SwiftUI 를 모르더라도 뭘 작성해야 할지 아실 겁니다. 앞서 설정한 actionKind 유형의 제스처를 사용했을 때 일어날 일들을 코드로 작성합니다(ex: 대화상자가 화면에서 사라짐). Javascript의 DOM에 익숙하신 분이라면, addEventListener가 생각나겠네요.

    handler인자는 이스케이핑 클로저로, 인자를 작성하는 괄호 안에 쓰지 않고, 괄호 밖으로 빼서 작성하는 문법을 사용할 수 있습니다.

    아래는 대화상자를 만드는 예제 코드입니다.

    // ContentView.swift
    import SwiftUI
    
    struct ContentView:View {
      @State var dialogActive:Bool = false
      var body:some View {
        ZStack {
          VStack {
            Button("Greeting") {
              dialogActive = true
            }
          }
          CustomDialog(active:$dialogActive){
            Text("Welcome to Greeting Dialog. Nice to meet you!")
          }
        }
      }
    }
    ...
    //
    //  CustomDialog.swift
    //  a11yModal
    //
    //  Created by 박웅진 on 2022/09/22.
    //
    import SwiftUI
    
    struct CustomDialog<Content:View>:View {
      @Binding var active:Bool
      var content:()->Content
      init(active:Binding<Bool>, @ViewBuilder _ content:@escaping ()->Content){
        self.content = content
        self._active = active
      }
      
      var body : some View {
        if active {
          ZStack {
          Text("Close").accessibilityHidden(true).onTapGesture { // dimmed Overlay, 스크린리더를 사용하지 않는 사용자가 밖을 누르면 대화상자 종료, accessibilityHidden으로 탐색되지 않도록 숨김.
            active = false
          }.background(.black.opacity(0.5)).frame(
             width:UIScreen.main.bounds.width,
             height:UIScreen.main.bounds.height
          ).keyboardShortcut(.escape) // ESC키를 누르면 해당 텍스트 요소의 onTapGesture가 실행됨
           VStack {
              Spacer()
              content()
              Spacer()
               HStack (alignment:.center) {
                 Button("OK"){ active = true }.keyboardShortcut(.return) // Enter누르면 OK버튼이 눌리도록함.
              }
            }
          }.accessibilityAddTraits(.isModal)
           .accessibilityAction(.escape) {
              active = false
          }
        }
      }
    }
    

     

    댓글을 작성하려면 해주세요.
  • tip
    [swift UI] 커스텀 슬라이더 접근성 구현 예제
    Webacc NV 2022-09-04 20:10:07

    어제에 이어서 커스텀 슬라이더를 구현하는 방법을 설명합니다.

    커스텀 슬라이더 접근성을 구현할 때 주의해야 할 것은 초점을 어떻게 줄 것인가 입니다.

    슬라이더와 관련된 객체들을 하나로 합쳐서 초점을 제공할 수도 있을 것이고 기존에 초점을 받고 있는 요소 중 하나를 잡아서 이벤트 구현을 할 수도 있을 것입니다.

    전자의 경우에는 슬라이더 관련 정보를 감싸고 있는 상위 컨테이너에 다음과 같이 접근성 엘리먼트를 우선 초기화 합니다.

    .accessibilityElement()

    위와 같이 초기화를 하는 이유는 슬라이더를 구현하기 위해서는 기존에 원래 가지고 있는 요소의 접근성 속성을 완전히 버리고 이벤트에만 의존해야 하기 때문입니다.

    이렇게 접근성 정보를 초기화 한 다음에는 보이스오버가 해당 요소에 포커스 했을 때 읽어야 할 레이블과 밸류를 다음 예시와 같이 줍니다.

    .accessibilityLabel("Value")
    .accessibilityValue(String(value))

    여기서 accessibilityLabel 은 어떤 슬라이더인지를 가리키는 것이고 accessibilityValue 는 말 그대로 값입니다.

    그리고 .accessibilityAdjustableAction 메서드를 사용하여 한 손가락 위로 쓸기, 아래 쓸기에 대한 이벤트를 구현합니다. 핵심은 기능 구현과 함께 밸류 값을 변경해야 한다는 것입니다.

    아래 예시를 참고합니다.

    코드를 보시면 아시겠지만 해당 예시는 증가, 감소 버튼이 별도로 존재하는 화면입니다.

    그러나 스크린 리더 사용자가 해당 두 버튼을 별도로 접근하게 하지 않고 슬라이더로 구현을 하여서 조금 더 쉽게 증가나 감소를 하게끔 구현한 예시라고 보시면 됩니다.

    VStack {
        Text("Value: \(value)")
    
        Button("Increment") {
            value += 1
        }
    
        Button("Decrement") {
            value -= 1
        }
    }
    .accessibilityElement()
    .accessibilityLabel("Value")
    .accessibilityValue(String(value))    
    .accessibilityAdjustableAction { direction in
        switch direction {
        case .increment:
            value += 1
        case .decrement:
            value -= 1
        default:
            print("Not handled.")
        }
    }
    
    

     

    댓글을 작성하려면 해주세요.
  • tip
    [swift UI] accessibility traits 중 .adjustable 속성이 없는 이유
    Webacc NV 2022-09-03 18:56:11

    iOS 접근성 구현에서 traits 속성을 적절하게 주는 것은 여러 번 강조해도 지나침이 없을만큼 중요합니다.

    swift UI 에서는 .accessibilityAddTraits, .accessibilityRemoveTraits 모디파이어로 해당 traits 속성을 부여하게 되는데 UIKit에서는 traits 중에 커스텀 슬라이드를 구현할 때 적용해야 하는 .adjustable 속성이 있었지만 swift UI 에서는 해당 trait 자체가 없습니다.

    이유는 접근성 구현에 대한 실수를 줄이고자 한 손가락 위 아래 쓸기 액션만 적용하면 해당 trait 는 자동으로 추가되게끔 한 것입니다.

    현재 WAI-ARIA 를 포함하여 요소 유형을 주는 것에 대한 문제 중 하나는 요소 유형만 주고 그에 대한 이벤트는 주지 않는 경우가 많다는 것입니다.

    이렇게 되면 접근성을 반만 적용한 것이어서 슬라이드의 경우 사용자가 슬라이더라는 요소 유형은 알 수 있지만 실질적인 조작이 안 되는 문제가 발생합니다.

    이것을 방지하기 위해서 swift UI 에서는 이벤트를 주면 trait 속성이 자동으로 붙도록 한 것입니다.

    다음 시간에는 슬라이드 구현 방법을 알아보겠습니다.

    댓글을 작성하려면 해주세요.
  • tip
    [android view system] 비밀번호 보이기 숨기기 구현 시 접근성 이슈 해결에 관하여
    Webacc NV 2022-09-03 15:59:18

    와이파이 암호를 입력하거나 비밀번호를 입력하는 화면에서 비밀번호 보이기 또는 숨기기 버튼을 제공하는 경우가 있습니다.

    기본적으로는 대부분 비밀번호 형식으로 EditText 값이 표시되게 하고 보이기를 누르면 비밀번호 자체가 보이게 되는 형식이 대부분일 것입니다.

    구현 방식이 여러 가지가 있겠으나 일반적으로 비밀번호 보이기 또는 숨기기 버튼을 누르면 해당 EditText 를 참조하여 transformationMethod를 사용함으로써 보이기, 숨기기를 구현합니다.

    문제는 이렇게 구현을 하면 비밀번호 보이기를 누를 때 EditText 요소에 이벤트가 발생하면서 기존에 입력된 텍스트를 무조건 읽어버린다는 것입니다.

    비밀번호는 개인정보이기 때문에 스크린 리더 사용자는 비밀번호를 입력할 때 볼륨을 최대로 줄이거나 이어폰을 사용합니다.

    그런데 사용자가 비밀번호 편집창에 초점을 맞추지 않고 단순히 비밀번호 보이기 버튼만 눌렀을 뿐인데 전체 텍스트를 읽어버릴 경우 개인정보 노출의 위험이 있을뿐만 아니라 상당히 당황스럽게 됩니다.

    이것을 단 몇 줄의 코드로 해결을 할 수가 있는데 방법은 너무나 간단합니다.

    비밀번호가 표시되거나 숨겨지는 동안만 해당 편집창을 접근성 노드에서 숨기면 그만입니다.

    다만 반드시 해당 이벤트가 완료되면 다시 접근성 노드에 편집창을 표시해 주어야 합니다.

    아래 코드 예시를 참조합니다.

            binding.showHideBtn.setOnClickListener {
                if(binding.showHideBtn.text.toString().equals("Show")){
                    binding.pwd.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
                    binding.pwd.transformationMethod = HideReturnsTransformationMethod.getInstance()
                    binding.showHideBtn.text = "Hide"
                    binding.pwd.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES
                } else{
                    binding.pwd.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
                    binding.pwd.transformationMethod = PasswordTransformationMethod.getInstance()
                    binding.showHideBtn.text = "Show"
                    binding.pwd.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES
                }
            }
    

     

    댓글을 작성하려면 해주세요.
  • tip
    [swift UI] 장식용 이미지 보이스오버 초점에서 제거하기
    Webacc NV 2022-09-03 14:33:02

    어제의 팁에 이어서 오늘은 장식용 이미지를 보이스오버에서 초점이 제공되지 않도록 하는 방법을 공유합니다.

    방법은 두 가지입니다. 

    1. 일반 이미지 파일을 추가한 경우: 아래 예시와 같이 해당 이미지에 decorative 속성을 추가하시면 됩니다.

    Image(decorative: book.genre ?? "Fantasy")

    2. systemName 속성과 같이 decorative 속성을 사용할 수 없는 경우에는 접근성 객체에서 해당 이미지 자체를 제거합니다. swift UI 에서는 .accessibilityHidden(true) 모디파이어를 사용할 수 있습니다.

     

    댓글을 작성하려면 해주세요.
  • tip
    [swiftUI] 보이스오버에서의 이미지 처리에 관하여
    Webacc NV 2022-09-02 16:36:18

    UIKit에서는 기본적으로 ImageView 요소에 대체 텍스트를 추가하지 않으면 보이스오버에서 이미지 자체에 초점을 제공하지 않았습니다.

    그러나 swiftUI에서는 Image 요소에 대체 텍스트를 추가하지 않더라도 이미지 요소로 초점이 제공됩니다.

    그리고 파일명이 있을 경우 파일명을 읽으며 아이콘 이미지인 경우는 그냥 이미지라고만 읽게 됩니다.

    여기서 우리는 한 가지의 팁을 얻을 수 있습니다.

    접근성 진단을 할 때 swiftUI로 개발되는 화면들이 조금씩 늘어가고 있기 때문에 각 화면별로 어떤 플랫폼으로 개발되었는지를 아는 것이 굉장히 중요합니다.

    당연한 이야기이지만 해결 방안이 달라지기 때문입니다.

    그런데 접근성이 적용되지 않은 화면에서 버튼과 같은 요소가 아닌 이미지 자체 요소에 초점이 제공된다면 swiftUI로 개발되었을 가능성이 높습니다.

    다음 팁에서는 swiftUI 장식용 이미지를 접근성에서 숨기는 두 가지 방법에 대해 살펴보겠습니다.

    댓글을 작성하려면 해주세요.
  • etc
    웹 접근성 직군별 교육 수정 요구
    malangdidoo 2022-08-14 02:17:54

    https://nuli.navercorp.com/education

    웹 접근성 직군별 교육 항목 중 제 3번 색에 무관한 콘텐츠 인식 강의에서

    edu_script 내 포함된 textarea에 다음과 같은 오류문항이 추가되어 있습니다.

     

    "을 색으로만 구분하고 있어 선택된 탭 무엇인지 구분하기 힘듭니다."

     

    연락 또는 기재할 곳을 찾지못해 포럼과 맞지 않는 내용일 수 있으나,

    수정 부탁드립니다!

    댓글을 작성하려면 해주세요.
  • tip
    [센스리더] 최근 업데이트 된 크롬에서의 WAI-ARIA 지원 공유
    Webacc NV 2022-08-08 10:47:41

    몇 달전 널리 아티클을 통해 공유한 바와 같이 현재 센스리더에서는 크롬 브라우저에서의 버그 및 WAI-ARIA 지원을 지속적으로 업데이트 하고 있습니다.

    2022년 8월 현재 8.0 베타가 출시된 상태인데 몇 가지 주요한 업데이트 내용을 정리해 보았습니다.

    1. aria-current 지원: 탭컨트롤에 적용된 aria-selected에 이어서 aria-current 속성이 지원됩니다. 현재까지는 링크나 버튼 등에서듸 선택됨 여부를 센스리더 호환성을 고려하여 title 속성으로 이를 제공하는 경우가 많았으나 aria-current라는 표준 마크업을 사용할 수 있습니다.

    2. aria-label 지원 업데이트: 이제 aria-label 속성을 영역 정보에 대한 구체적인 레이블을 설정하는 목적으로 ul 및 랜드마크 role 속성에도 사용할 수 있습니다.

    예전에는 주메뉴, 하위메뉴 등의 영역 정보를 헤딩을 숨김 처리하여 제공하는 경우가 많았지만 role="navigation" aria-label="하위메뉴" 또는 <ul aria-label="카테고리메뉴"> 와 같이 제공하여 불필요한 헤딩을 여러 개 추가하지 않아도 됩니다.

     

    3. aria-describedby 지원 업데이트: 현재는 aria-describedby 속성으로 추가정보를 제공하면 이를 바로 읽어줄 수 있도록 지원합니다. 따라서 삭제, 재생, 장바구니 담기와 같이 반복되는 버튼이 리스트 형태로 존재하는 경우 이에 대한 추가 정보를 aria-describedby 속성으로 추가하면 센스리더에서도 이를 지원합니다.

    4. 가상커서 초점이 브라우저 초점과 동기화되지 않는 이슈가 대부분 수정되었습니다. 

     

    댓글을 작성하려면 해주세요.
  • tip
    [android 공통] 접근성 진단은 TalkBack으로
    Webacc NV 2022-08-04 16:55:26

    안드로이드의 공식적인 스크린 리더는 톡백입니다.

    갤럭시의 경우 톡백의 부족한 부분을 보완하기 위해 보이스어시스턴트라는 스크린 리더를 개발하여 탑재하였으나 안드로이드 11부터는 톡백에서 보이스어시스턴트의 여러 제스처를 포함시킴에 따라 톡백으로 통합되었습니다.

    그러나 안드로이드 10 이하 갤럭시 단말을 사용하시는 분들 중 여전히 접근성 진단을 톡백이 아닌 보이스어시스턴트로 진행하는 분들이 계신 것 같습니다.

    물론 톡백의 최신 버전을 사용하더라도 안드로이드의 버전에 따라 API의 지원 범위가 달라서 모든 접근성 API를 지원하지는 않지만 스크린 리더 호환성을 유지하기 위해 가급적 톡백 최신 버전을 사용하여 접근성 진단을 하는 것이 좋습니다.

    다만 보이스어시스턴트가 탑재되어 있는 단말의 경우는 플레이스토어에서 안드로이드 접근성 도구모음을 설치하셔야 합니다.

    그리고 설정 > 접근성 > 설치된 서비스에서 톡백을 실행할 수 있습니다.

    댓글을 작성하려면 해주세요.
  • tip
    [swift & swiftUI] UIAccessibility announcement 사용 시 .accessibilitySpeechQueueAnnouncement 적극 활용하기
    Webacc NV 2022-07-26 12:35:20

    iOS 팁을 작성하면서 announcement 알림에 대해 여러 번 설명하였습니다.

    announcement는 swift 뿐만 아니라 swiftUI에서도 단순히 문자열을 참조한다는 조건 아래에서는 추가 구현 없이 해당 메서드를 바로 사용할 수 있습니다.

    그런데 announcement를 구현할 때 가장 애를 먹는 것 중 하나가 바로 다른 보이스오버 알림과 겹칠 때입니다.

    특정 알림을 주었을 때 보이스오버가 다른 콘텐츠를 읽고 있었을 때에는 해당 알림을 읽지 못하는 경우가 많기 때문입니다.

    그래서 알림을 구녀할 때에는 .accessibilitySpeechQueueAnnouncement 안에다가 담아 두었다가 기존에 읽던 것을 다 읽은 후에 알림을 읽도록 하는 것이 좋습니다.

    물론 그렇게 하더라도 약 0.1초 정도의 딜레이는 주는 것이 좋습니다.

    아래의 코드 예시를 참고합니다.

                    DispatchQueue.main.asyncAfter(deadline: .now()+0.1) {
                        UIAccessibility.post(notification: .announcement, argument: NSAttributedString(string: "\(count)", attributes: [.accessibilitySpeechQueueAnnouncement: true]))
                    }

     

    댓글을 작성하려면 해주세요.
  • tip
    [CSS] 체크상자와 라디오버튼 레이블 스타일은 이제 그만!
    Webacc NV 2022-07-12 14:08:32

     

     

    Chrome 화면

    Chrome 렌더링 모습

    Firefox 화면

    Firefox 렌더링 모습

    기존의 스타일링 방식에 대하여

    위에는 input에 직접 스타일을 적용한 checkbox와 radio의 셈플 사진입니다. 우선, Chrome에서 체크상자의 체크 기호가 께진 것은 이미지가 아닌 유니코드 글자를 사용하여 폰트 차이로 인해 생기는 문제이니 무시해주세요.

    국내에서 Checkbox와 Radio에 대한 스타일을 수정할 때, 가장 많이 사용된 방식은, 웹사이트 마크업을 많이 관찰하신 분이라면 아시겠지만, checkbox나 radio input을 매우 작고, 투명하게 만든 다음, label에 텍스트와 함께 div나 after before 요소를 사용하여 스타일을 적용한 케이스가 많다는 것을 알 수 있습니다.

    그 이유는 바로, 크로스브라우징 작업때문입니다. 위와 같이 브라우저에서 그려지는 렌더링 모습이 조금씩 달라지는 부분을 최대한 똑같이 맞춰야 하는 번거러움이 있고, 특정 브라우저에서는 기술적인 한계가 있기 때문에 label에 스타일을 적용한 것이었지요.

    그러나, 위에 사진은 input태그에 직접 스타일을 적용한 것입니다. 사진을 보면 알 수 있듯, 텍스트 말고는 거의 일치하도록 렌더링 된 것을 볼 수 있습니다.

    복잡한 input 태그 내부에는 우리가 모르는 ShadowDOM이 존재한다.

    Input태그나 textarea태그 등의 네이티브 HTML 태그는 내부적으로, 사용자 에이전트나 일반적인 퍼블리셔들이 접근할 수 없는 ShadowDOM 영역이 존재합니다. Chrome 개발자 도구 설정에서 "사용자 에이전트 Shadow DOM"을 켜면 일반적으로 볼 수 없는 이 영역을 볼 수 있죠.

    최신 브라우저에서 비교적 크로스 브라우징 및 스타일 수정이 간단한 checkbox와 Radio

    그러나, Chromium(Chrome, Edge, Opera, Whale, Samsung Internet), Webkit(Safari), Quantum(FireFox) 등에서 라디오 버튼이나 체크상자는 구조가 비교적 매우 단순하여, 이 Shadow 영역이 존재하지 않습니다.  편집창이나 슬라이더 등과 달리 div나 p, span과 같은 일반 태그처럼 ::before와 ::after 가상 클래스 선택자또한 잘만 동작합니다.

    조금이라도 스타일링에 고민을 하셨던 분들이라면 감이 딱 오실겁니다. 위에서도 얘기했지만, input[type="checkbox"]나 input[type="radio"] 안에 직접 하위 태그를 마크업할 수는 없지만, ::after와 ::before 선택자를 통해, 안에 스타일링 요소로 이 두 가상요소를 침투시킬 수 있습니다.

    분명히 이 스타일링 기법도 한계는 있지만, 체크상자나 라디오버튼은 비교적 기능이 간단하여, 이 두 요소만 있어도 충분히 나만의 스타일을 만들기 어렵지 않습니다.

    코드좀 줘 보세요

    당연히 코드도 드려야지요. 그런데, 진짜 별게 없습니다. 같이 한번 아래를 봅시다.

    체크상자

    /* Checkbox */
    label{
      vertical-align: top;
    }
    input[type="checkbox"].custom-style {
      appearance: none;
      width:1.4em; height:1.4em;
      border-radius: 50%;
      display: inline-flex; text-align: center; overflow: hidden;
      position: relative;
      box-shadow:
      inset 0 0 0 0.1em #FFF,
      0 0 0 0.1em #1a5ecc;
      outline-offset: 0.25em;
      transition: all 0.2s ease-in-out;
    }
    input[type="checkbox"].custom-style:checked {
      background-color:#2d6efc;
      box-shadow:
      inset 0 0 0 0.1em #FFF,
      0 0 0 0.1em #1a5ecc;
    }
    input[type="checkbox"].custom-style::after {
      content:"\2713";
      display: flex;
      justify-self: center; align-self: center;
      align-self: center; justify-content: center;
      font-weight: 1000; font-size:120%; line-height: 1.4em;
      width:100%; height:100%; color:transparent; background-color:transparent;
    }
    input[type="checkbox"].custom-style:checked::after {
      color:white;
    }

    특별이 최신 선택자를 사용하지도 않았습니다. 그냥, 무식하게, appearance속성을 none으로 적용하여 기본 모습을 없애고, 크기를 조절하고, 음영을 주고, 색칠했습니다. 그리고, :checked 상태일 때, ::after로 체크표시 유니코드를 넘겼지요.

    라디오 버튼

    /* Radio */
    input[type="radio"].custom-style {
      appearance: none;
      overflow: hidden;
      position: relative;
      border-radius: 50%;
      display: inline-flex;
      align-items: center;
      justify-content:center;
      width:1.35em; height:1.35em;
      box-shadow:inset 0 0 0 0.1em #FFF,
      0 0 0 0.1em #1a5ecc; gap: 0;
    }
    input[type="radio"].custom-style::after {
      transition: all 0.3s;
      content:""; display: block;
      border: solid 1px transparent;
      width:0.9em; height:0.9em; border-radius: 50%;
    }
    input[type="radio"].custom-style:checked::after {
      background-color: #1a5ecc;
    }
    
    /* Common */
    .custom-style+label{
      font-weight: bold;;
      transition: color 0.4s;
    }
    .custom-style:checked+label {
      color:#1a5ecc;
    }

    Radio도 마져 보시지요. 레이블에 대한 건 덤입니다. Radio도 마찬가지로 아주 무식한 방식으로 스타일링했습니다. 머리를 굴릴 것 도 없이, 그냥 기본 모습을 none으로 지운 다음, 크기를 정하고, 음영을 주고, 색칠했습니다. 선택된 Radio는 안에 작고 진한 동그라미가 포인트이기 때문에, ::after 요소에 원 모양을 넣어 flex로 가운데 정렬한 것을 볼 수 있습니다.

    체크상자는 ::after 안에 있는 텍스트를 읽지 않아요?

    다행이도 안 읽습니다. 설령 이를 읽는 스크린리더가 있더라도, content의 값을 빈 문자열로 두고, svg같은 것을 사용하면 되지요. 이론적으로는 안 읽는것이 당연합니다. 왜냐하면, 체크상자에 label이 등록돼 있고, 보통, 안에 콘텐츠를 담을 수 있는 요소는 보이는 텍스트가 label이 되지만, aria-label 등으로 접근 가능한 이름(Accessible Name)을 선언해 버리면 덮어씌여져 없어지기 때문입니다.

    IE는 신경 안 쓰나요?

    결론만 말씀드리면, 예. 신경쓰지 않을 것입니다. 식상한 얘기가 되겠지만, Microsoft에서 Internet Explorer에 대한 지원은 전면 중단하였고, Edge를 통해 Internet Explorer 모드를 사용할 수는 있으나, 도저히 정상적인 사용으로 보기는 어렵기 때문입니다. Internet Explorer의 종료를 계기로 앞으로는 웹환경에 맞춘 보다 좋은 최신 기술로 코드 작성이 가능해질 것입니다.

    댓글을 작성하려면 해주세요.
  • tip
    [swiftUI] accessibilityRepresentation API 소개
    Webacc NV 2022-07-07 15:12:30

    swiftUI로 접근성 적용을 하다보면 아쉬운 것 중 하나가 요소 유형 혹은 상태정보를 줄 수 있는 것에 한계가 있다는 것입니다.

    그 중 하나가 토글버튼입니다. 

    토글버튼은 일반 버튼과 달리 선택 혹은 해제가 가능한 버튼이기 때문에 요소 유형 및 상태정보를 명확하게 줄 필요가 있는데 .accessibility(addTrait) 모디파이어를 활용해서는 일반 버튼에 선택됨 상태정보 외에는 줄 수 있는 정보가 없습니다.

    그런데 swiftUI에서는 accessibilityRepresentation 이라는 아주 훌륭한 모디파이어가 있습니다.

    해당 모디파이어는 기존 뷰를 무시하고 해당 모디파이어 안에서 제정의한 뷰를 가지고 접근성 요소를 대체하는 역할을 합니다.

    만약 이미지를 사용하여 체크박스를 만들었다고 가정해 봅시다.

    해당 이미지 안에다가 다음 예시와 같이 ToggleButton으로 접근성 모디파이어를 재정의하면 화면에서는 토글버튼이 보이지 않지만 보이스오버에서는 해당 이미지는 무시한채 토글버튼 요소만 읽어주게 됩니다.

                .accessibilityRepresentation {
                    Toggle(isOn: $isSelected) {
                        Text("전체선택")
                    }
                }

     

    댓글을 작성하려면 해주세요.
  • tip
    [swiftUI] 특정 영역을 그룹으로 지정하고싶을때
    Webacc NV 2022-07-02 10:19:52

    iOS UIKit에서는 특정 영역에 시맨틱한 그룹 이름을 지어주고 싶을 때 accessibilityContainerType 속성을 활용한다고 했습니다.

    기본적으로 테이블뷰와 같이 원래가 시맨틱한 그룹 속성을 가진 경우에는 accessibilityLabel을 주는 것만으로 영역 정보를 줄 수 있지만 UIView와 같이 아무런 시맨틱한 의미를 가지지 않은 영역에 정보를 주어야 할 때 사용할 수 있는 API입니다. 

    그럼 swiftUI에서는 어떻게 영역 정보를 줄 수 있을까요?

    이 때 사용할 수 있는 것이 .accessibilityElement(children: .contain) 입니다.

    만약 커스텀 도구막대를 만들었다고 가정해 봅시다.

    HStackView 안에 버튼 4개를 두었습니다.

    HStackView는 접근성 입장에서는 아무런 의미를 가지지 않기 때문에 보이스오버가 도구막대에 진입할 때 아무런 영역 정보를 말하지 않습니다.

    그러나 해당 영역을 도구막대로 정의시켜 주면 좀 더 시맨틱한 탐색을 도울 수 있습니다.

    따라서 이때 해당 HStatckView 안에 다음 예시와 같이 영역 정보를 줄 수 있습니다.

    					HStack {
    						Text("이것은 다른 텍스트")
    					}
    					.accessibilityElement(children: .contain)
    					.accessibility(label:
    					Text("편집 영역"))
        }

     

    댓글을 작성하려면 해주세요.
  • tip
    [swiftUI] 초점이 나누어진 요소 합치기
    Webacc NV 2022-07-01 12:06:34

    모바일은 보이스오버나 톡백에서 한 손가락 오른쪽 혹은 왼쪽 쓸기로 이동할 때 객체 단위로 이동하기 때문에 초점을 다루는 것이 접근성에서 상당히 중요한 이슈 중 하나입니다.

    게다가 swiftUI는 iOS, macOS 통합이므로 키보드가 중심인 macOS에서는 키보드 접근성까지 고민해야 하는 상황입니다.

    swiftUI에서도 VStack과 같은 컨테이너 안에 여러 개의 텍스트 뷰를 넣거나 버튼과 버튼의 레이블인 텍스트뷰를 분리할 경우 초점은 두 개로 나누어집니다. 

    이렇게 되면 모바일에서는 버튼, 확인 과 같이 초점이 두 개로 분리될 것이고 맥에서는 탭키를 누르면 버튼에만 포커스 되므로 탭키로 이동 시에는 버튼이라고만 읽을 것입니다.

    이를 해결하기 위해 사용할 수 있는 것이 .accessibilityElement(children: .combine) 입니다.

    이것은 기존 iOS UIKit에서는 없었던 것인데 말 그대로 컨테이너 안에 있는 요소들의 초점을 하나로 합치겠다는 것입니다.

    기존 UIKit 개발 시에는 초점을 하나로 합치려면 accessibilityLabel, accessibilityTraits를 별도로 주어야 했지만 swiftUI에에서는 좀 더 간단해졌다고 할 수 있습니다.

    따라서 초점이 분리된 것을 하나의 컨테이너 아에 넣고 위의 메서드를 적용하면 초점이 하나로 합쳐지게 됩니다.

    만약 초점도 합치고 대체 텍스트 역시 변경하고 싶다면 .accessibilityElement(children: .ignore) 를 사용하면 되겠습니다.

    이렇게 되면 아예 접근성 정보를 새로 덮어쓰겠다는 의미가 됩니다. 

    댓글을 작성하려면 해주세요.
  • tip
    [android] 안드로이드 디버깅 시에 사용할 수 있는 ui automator 파일 다운로드 제공 관련의 건
    Webacc NV 2022-06-24 19:07:22

    안드로이드 접근성 디버깅하기 관련 아티클을 통하여 ui automator viewer 파일을 통해서 접근성 트리를 디버깅하는 방법을 설명한 적이 있습니다.

    그런데 최근에 안드로이드 스튜디오를 설치하면 기본적으로 해당 파일이 포함되어 있지 않습니다.

    아무래도 해당 툴 자체가 워낙 구버전이어서 그런 것 같습니다.

    그러나 접근성 디버깅 시에 워낙 유용한 파일이기 때문에 해당 툴을 압축하여 공유합니다.

    아티클에서 명시한대로 안드로이드 스튜디오 및 자바를 설치하셨다면 아래 링크에서 해당 툴을 다운받아 적당한 곳에 놓고 실행하시면 되겠습니다.

    ui automator viewer 실행을 위한 도구 다운로드

    댓글을 작성하려면 해주세요.
  • tip
    [iOS native] 미디어 재생 버튼 구현시 startMediaSeesion trait 적용 권고
    Webacc NV 2022-06-21 16:16:40

    보이스오버는 안드로이드 톡배과 달리 특정 버튼을 이중탭하면 해당 버튼의 텍스트가 변경되든 그렇지 않든 해당 버튼의 레이블을 다시 읽는 특성이 있습니다.

    따라서 안드로이드처럼 버튼의 레이블이 변경되었는데 읽어주지 않는다거나 하는 문제는 없습니다.

    그러나 음악 재생 시에는 최대한 음악을 감상할 수 있도록 접근성을 적용하는 것이 필요합니다.

    재생 버튼은 일반적으로 이중탭하면 일시정지로 변경되는데 아무런 접근성 적용을 하지 않으면 보이스오버는 변경된 텍스트를 읽습니다.

    예외적으로 음악 재생 시에만 변경된 텍스트를 읽지 않게 하려면 startMediaSession accessibilityTraits 속성을 버튼과 함께 주면 됩니다.

    다만 재생 중 일시정지를 눌렀을 때에는 미디어가 정지되므로 이 때는 변경되는 버튼의 텍스트, 즉 재생 을 읽도록 하고 재생중일 때에만 startMediaSession을 함께 적용하면 되겠습니다.

    댓글을 작성하려면 해주세요.
  • tip
    [android mobile web] TalkBack에서의 aria-valuetext 지원 업데이트
    Webacc NV 2022-06-19 19:30:42

    모바일에서 슬라이더 구현 시 보이스오버 혹은 톡백에서 슬라이더 조절을 가능하게 하려면 반드시 <input type="range"> 태그를 사용하여 개발이 되어야 합니다.

    커스텀으로 슬라이더를 개발하고 role="slider", aria-valuemin, aria-valuemax, aria-valuenow 속성을 적절하게 주고 키보드 이벤트를 스크립트로 구현한다 하더라도 현재까지는 모바일 스크린 리더에서 슬라이더를 조절할 수 있게 하는 방법이 없기 때문입니다. 

    그런데 톡백의 경우 예전에는 슬라이더의 현재 읽어주는 텍스트를 aria-valuetext 속성으로 제공한다 하더라도 해당 슬라이더의 퍼센트를 무조건 읽는 특성이 있었습니다.

    퍼센트 정보가 전혀 필요하지 않은 온도 조절과 같은 경우에는 오히려 이러한 정보가 불필요합니다.

    그런데 현재는 aria-valuetext 속성을 주면 퍼센트 정보는 읽지 않고 aria-valuetext 값만 읽는 것으로 변경되었습니다.

    따라서 위에서 예로 든 온도 조절과 같은 슬라이더를 구현하더라도 자연스럽게 읽어줄 수 있게 되었습니다.

    다만 aria-valuetext 값이 없으면 종전과 같이 퍼센트를 읽습니다.

    또한 기본적으로 톡백에서 슬라이더를 조절하면 한번 위로 올리거나 아래로 내릴 때마다 기본적으로 5퍼센트씩 증감하게 되는데 input 요소의 step="1" 과 같이 속성을 변경하면 퍼센트가 해당 값만큼씩 증감하게 됩니다.

    댓글을 작성하려면 해주세요.
  • tip
    [android view system] 접근성 포커스가 진입하면 경과되는 시간 더 이상 읽지 못하게 하기
    Webacc NV 2022-06-13 16:23:15

    뮤직 플레이어, 비디오 플레이어와 같은 플레이어에서 경과 시간 텍스트뷰를 구현할 경우 톡백에서 해당 텍스트뷰에 포커스 하고 있으면 경과되는 시간을 계속 읽는 것을 알 수 있습니다.

    이 역시 스크린 리더 사용자 입장에서는 굉장히 소란스러운 정보가 될 수 있습니다.

    우리가 원하는 것은 포커스 했을 때에 시간을 읽고 다시 포커스 하기 전까지는 시간이 변하더라도 해당 시간 자체를 읽지 않도록 하는 것입니다.

    이 때에도 isAccessibilityFocused 메서드를 활용할 수 있습니다.

    즉 접근성 포커스가 머물러 있지 않을 때에만 경과되는 시간이 변경되도록 하라는 명령어입니다.

    주의하실 것은 경과 시간에 대한 텍스트와 대체 텍스트를 함께 제공할 경우에는 대체 텍스트 뿐만 아니라 화면에 보여지는 텍스트 역시도 접근성 포커스가 있지 않을 때에만 경과 시간을 표시하도록 해야 합니다.

    그렇지 않으면 경과되는 시간을 톡백에서 계속 읽게 됩니다.

    아래 예시를 참고합니다.

            if (!mPass.isAccessibilityFocused()) {
                mPass.setContentDescription(mPassText + pass + second);
                mPass.setText("" + pass + second);
            }
    

     

    댓글을 작성하려면 해주세요.
  • tip
    [mobile web] TalkBack에서의 라디오버튼 지원 업데이트
    Webacc NV 2022-06-06 15:46:36

    라디오버튼의 경우 선택 혹은 선택안함 상태정보 뿐만 아니라 라디오그룹을 기반으로 현재 그룹의 총 라디오 개수 및 현재 위치한 라디오버튼의 위치를 함께 읽어주는 것이 일반적입니다.

    22년 6월 6일 현재  크롬 및 톡백 최신 버전을 사용할 경우 라디오버튼에 위치하면 그룹내 옵션 3개중 2번째와 같이 안드로이드에서도 라디오버튼의 개수를 읽어주도록 업데이트 되었습니다.

    톡백의 버전이 운영체제 혹은 단말기마다 다르고 웹뷰의 경우 크롬의 브라우저 접근성 API의 영향을 많이 받기 때문에 정확하게 톡백에서 해당 기능을 업데이트 한 것인지 크롬에서 관련 API를 제공해준 것인지는 알 수 없지만 안드로이드에서도 라디오버튼 탐색 시 그룹별로 잘 마크업한다면 여러 그룹의 라디오버튼이 존재하더라도 쉽게 레이아웃을 파악할 수 있으리라 기대합니다.

    라디오를 그룹별로 잘 마크업해야 한다는 것에 대해서는 추후에 한번 다루도록 하겠습니다.

    댓글을 작성하려면 해주세요.
  • tip
    [android view system] 접근성 유틸 클래스에 setAsDropdownWithHint 메서드 추가
    Webacc NV 2022-06-02 12:38:08

    요즘에는 안드로이드 네이티브 팁을 다루면서 뷰 시스템과 더불에 제트팩 컴포즈에 대한 팁을 함께 올리고 있습니다.

    따라서 앞으로는 기존 레이아웃 방식에 대한 팁은 view 시스템으로 표기하도록 하겠습니다.

     

    오랜만에 접근성 유틸 클래스를 업데이트 합니다.

    지난 번 setAsDropdown 메서드를 만들어 공유한 적이 있습니다.

    말 그대로 커스텀 드롭다운 요소 유형 정보를 주어야 할 때 사용할 수 있는 메서드였습니다.

    기본적으로 안드로이드의 드롭다운은 레이블 정보가 없습니다.

    즉 '정렬방식, 드롭다운목록, 인기순'과 같이 읽도록 하는 것이 네이티브에서는 불가능하다는것입니다.

    그러나 정렬방식과 같은 힌트 메시지를 주지 않으면 무엇에 대한 옵션인지 예측하기 어려운 경우도 있기 때문에 해당 메서드를 추가로 만들게 되었습니다.

    사용법은 간단합니다. 드롭다운 요소로 제공해야 하는 뷰 객체와 스트링 즉 드롭다운목록 앞에 읽을 레이블 스트링을 함께 넣어 주시면 됩니다.

    예: setAsDropdownWithHint("dropdownView, "정렬방식 선택")

    유틸 클래스 자바 다운로드

    유틸 클래스 코틀린 다운로드

    댓글을 작성하려면 해주세요.
  • tip
    [jetpack compose] 편집창에 시각적으로 보이지 않는 접근성 레이블을 설정해야 할때
    Webacc NV 2022-06-01 17:54:18

    안드로이드 네이티브와 마찬가지로 제트팩 컴포즈에서도 TextField의 접근성 레이블을 contentDescription으로 설정해서는 안 됩니다.

    그 이유는 안드로이드 네이티브와 같으며 대체 텍스트 형태의 레이블을 넣으면 TalkBack 음성안내지원 메뉴에서 수정 옵션 메뉴를 호출할 수 없게 됩니다.

    컴포즈에서는 placeholder 혹은 label을 통하여 시각적으로 보여지는 텍스트필드 레이블을 넣게 되는데 기획상 레이블 자체가 없거나 레이블 텍스트와 텍스트필드가 별도의 객체로 존재하는 경우 어떻게 접근성 적용을 해야 할까요?

    이 때 사용할 수 있는 것이 바로 BasicTextField > decorationBox입니다.

    decorationBox 안에 아래 예시와 같이 Text 형태로 스트링을 넣으면 이것은 접근성을 위한 레이블로 간주하고 시각적으로는 보이지 않게 됩니다.

    @Composable
    fun TemperatureTextField(
        temperature: MutableState<String>,
        modifier: Modifier = Modifier,
        callback: () -> Unit
    ) {
        BasicTextField(
            value = temperature.value,
            onValueChange = {
                temperature.value = it
            },
            decorationBox = {
                Text(text = stringResource(id = R.string.placeholder))
            },
            modifier = modifier,
            keyboardActions = KeyboardActions(onAny = {
                callback()
            }),
            keyboardOptions = KeyboardOptions(
                keyboardType = KeyboardType.Number,
                imeAction = ImeAction.Done
            ),
            singleLine = true
        )
    }

     

    댓글을 작성하려면 해주세요.
  • tip
    [jetpack compose] 체크박스, 라디오버튼, 스위치와 레이블 텍스트의 초점이 분리될 때
    Webacc NV 2022-05-29 18:07:50

    뷰 시스템에서는 체크박스, 라디오버튼 등의 컨트롤 요소와 레이블 텍스트가 분리되어 있을 때 labelFor 속성을 이용하여 접근성을 구현하였습니다.

    제트팩 컴포즈에서는 체크박스, 라디오버튼 등의 컨트롤을 품고 있난 상위 Row에 .toggleable, 혹은 .selectable 모디파이어를 주고 그 안에서 체크박스, 라디오버튼을 클릭하였을 때의 동작을 정의하는 식으로 접근성을 구현합니다.

    즉 라디오버튼, 체크박스 등의 자체 클릭 혹은 onCheckedChange 이벤트를 null로 설정하고 상위 레이아웃에서 구현해야 한다는 것입니다.

    자세한 설명은 조만간 발행될 아티클을 참고해 주시고 본 팁에서는 아래에 관련 샘플 코드를 첨부합니다.

    @Composable
    fun CheckboxWithLabel(label: String, state: MutableState<Boolean>) {
        Row(
            modifier = Modifier
                .toggleable(
                    value = state.value,
                    onValueChange = { state.value = it },
                    role = Role.Checkbox
                )
                .clickable {
                    state.value = !state.value
                }, verticalAlignment = Alignment.CenterVertically
        ) {
            Checkbox(
                checked = state.value,
                onCheckedChange = null
            )
            Text(
                text = label,
                modifier = Modifier.padding(start = 8.dp)
            )
        }
    }

     

    댓글을 작성하려면 해주세요.
  • tip
    [jetpack compose] clearAndSetSemantics 메서드에 관하여
    Webacc NV 2022-05-28 11:16:42

    Jetpack compose 접근성 API를 살펴보면서 느끼는 것 중 하나는 기존 안드로이드 뷰 시스템의 메서드에 비해 좀 더 단어가 직관적인 것 같다는 것입니다.

    뷰 시스템에서는 특정 접근성 객체를 숨길 때 importantForAccessibility 메서드를 활용하였습니다.

    그러나 제트팩 컴포즈에서는 clearAndSetSemantics를 활용합니다.

    해당 메서드는 기존의 접근성 객체를 완전히 초기화 하고 다시 접근성 객체를 만들 때 사용하는데 두 가지 용도로 사용됩니다.

    1. 해당 요소의 원래 가지고 있는 접근성 요소를 완전히 초점에서 제거할 때.

    2. 해당 접근성 노드를 완전히 새로 지정해야 할 때.

    따라서 아래 예시와 같이 사용하면 기존에 초점을 가지던 요소를 더 이상 톡백에서 초점을 받을 수 없게 됩니다.

    modifier = Modifier.clearAndSetSemantics { }

    다만 { } 사이에 contentDescription, role 등을 삽입하면 기존의 디폴트 접근성 객체가 아닌, 새로 지정한 객체로 치환됩니다.

    또한 이 속성을 Row와 같은 레이아웃에 사용하면 하위의 모든 요소들이 초기화 됩니다.

    댓글을 작성하려면 해주세요.
  • tip
    [jetpack compose] 활성화 하려면 두 번 탭하세요 힌트 메시지 변경하기
    Webacc NV 2022-05-26 12:57:12

    레이아웃을 기준으로 개발된 안드로이드 view 시스템에서는 replaceAccessibilityAction 메서드를 통하여 활성화 하려면 두 번 탭하세요 에 대한 힌트 메시지를 변경할 수 있었습니다.

    Jetpack compose 에서는 .clickable modifier 속성 중 onClickLabel을 통해 힌트 메시지 변경이 가능합니다.

    다만 기존 안드로이드 view 시스템과 같이 이중탭하세요 라는 기본 텍스트는 변경이 불가합니다.

    예를 들어 메일 보관 작업을 하려면 이중태바세요 라는 힌트 메시지로 변경하려면 다음과 같이 적용이 가능합니다.

        Row(
            modifier = Modifier
                .clickable(onClickLabel = "메일 보관") {
                state.value = !state.value
            }, verticalAlignment = Alignment.CenterVertically
        ) { .... }
    

     

    댓글을 작성하려면 해주세요.
  • tip
    [ARIA 예제] 편집창에 바로 입력되지 않는 자동완성 예제 체험해보기
    Webacc NV 2022-05-24 12:03:38

    널리 아티클 및 포럼 팁을 통하여 자동완성 리스트가 편집창에 입력되는 형태가 아닌 경우에는 aria-activedescendant, role listbox, role option 속성을 사용하여 접근성 적용을 해야 함을 여러 번 언급하였습니다.

    관련 접근성이 잘 적용된 예제를 구현하여 해당 팁을 통해 공유합니다.

    스크린 리더를 실행한 상태에서 샘플 페이지를 방문하여 자동완성 접근성이 잘 적용된 예시를 체험해 보시기 바랍니다.

    센스리더의 경우 가상커서를 해제 후 테스트 해야 합니다.

    댓글을 작성하려면 해주세요.
  • tip
    [android native] 상태정보 커스텀으로 제공해주기
    Webacc NV 2022-05-21 13:38:18

    안드로이드 11부터 제공되고 있는 ViewCompat.setStateDescription이라는 API를 아시나요?

    일반적으로 상태정보는 선택됨, 선택안됨, 켜짐, 꺼짐 등을 제공하게 되는데 상황에 따라서는 읽지 않음, 읽음, 주문완료, 주문안됨과 같은 정보를 문자 형태로 주어야 하는 경우가 있습니다.

    이런 경우 지금까지는 대체 텍스트 형태로 해당 정보들을 다 제공해 왔는데 대체 텍스트로 제공할 경우 스크린 리더 사용자가 읽기 방식을 본인 나름대로 커스텀해서 해당 정보를 들을 수 있는 방법이 없습니다.

    즉 읽음/ 읽지 않음, 주문완료/주문안됨과 같은 정보를 콘텐츠보다 먼저 듣고싶을 수도 있고 콘텐츠 다음에 듣고 싶을 수도 있는데 이러한 선택권이 없어진다는 것입니다.

    이를 해결하기 위해 사용할 수 있는 것이 ViewCompat.setStateDescription입니다.

    인자값으로는 참조해야 할 view 객체와 상태값 문자입니다.

    예: ViewCompat.setStateDescription(binding.message, "unread")

    이렇게 하면 사용자가 톡백에서 읽기 순서를 어떻게 지정했느냐에 따라 상태값을 처음 또는 끝에 읽을 뿐만 아니라 상태값이 토글되는 경우 변경된 상태값만 깔끔하게 읽어주게 됩니다.

    그러므로 모든 것을 대체 텍스트로 넣기 보다는 상태값은 상태값으로 분류하여 구현하는 것이 사용성을 높일 수 있겠습니다.

    참고로 제트백 컴포즈로 앱을 구현한다면 semantics로 stateDescription 정보를 줄 수 있습니다.

        modifier = Modifier.semantics {
             (stateDescription = "읽지 않음")
        }

     

    댓글을 작성하려면 해주세요.
  • tip
    [iOS native] UITextView 요소를 보이스오버가 읽을 시 쉼표가 붙는 이슈 수정 관련
    Webacc NV 2022-05-20 17:12:43

    iOS 15 초기버전부터 iOS 15.4까지 UITextView 내에 한글로 텍스트를 구현하면 VoiceOver에서 스페이스를 기준으로 모두 쉼표를 붙여 읽는 문제가 있었습니다.

    예: 이것은, 접근성, 테스트를, 하기, 위해, 작성한, 것입니다.

    그래서 스크린 리더 사용자는 해당 요소를 읽을 때 모든 글자를 다 끊어 읽어서 상당히 불편한 부분이 잇었습니다.

    해당 이슈는 iOS 15.5버전부터 해결되었습니다.

    접근성 진단 시 참고하시기 바랍니다.

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] improveAccessibility.js에 announceForAutoComplete 메서드 추가
    Webacc NV 2022-05-19 11:08:30

    편집창에 특정 글자 입력 시 자동완성이 표시되는 UI에서는 자동완성이 특정 글자를 입력했을 때만 나타나므로 이에 대한 알림을 선별해서 제공해야 합니다.

    따라서 스크린 리더 사용자가 타이핑을 빠르게 입력하다보면 자동완성 리스트가 수시로 나타났다 사라졌다를 반복할 것입니다.

    이때 알림 제공을 구현할 때 조금 더 쉽게 적용할 수 있도록 announceForAutoComplete(message) 메서드를 만들어 공유하게 되었습니다.

    사용법은 너무나 간단합니다.

    자동완성 관련 리스트가 나타나는 시점에 announceForAutoComplete("자동완성 표시됨")과 같이 적용만 해 주시면 됩니다.

    다만 자동완성이 사라질 때에는 removeAnnounceForAccessibility() 함수를 적용합니다.

    그러면 자동완성 리스트가 수시로 나타났다 사라졌다를 반복하더라도 마지막 입력한 글자에서 자동완성이 표시된다면 스크린 리더에서 한번만 해당 메시지를 출력하게 됩니다.

    improveAccessibility.js 다운로드

    댓글을 작성하려면 해주세요.
  • tip
    [ARIA 예제] role="progressbar" 샘플 페이지 체험해보기
    Webacc NV 2022-05-18 11:26:56

    파일 업로드와 같이 진행률을 표시하는 경우 접근성을 적용하기 위해서는 role="progressbar" 속성을 사용하여 스크린 리더 사용자도 실시간 진행 상황을 알 수 있도록 구현해야 합니다. 

    퍼센트의 범위가 0-100인 경우에는 aria-valuemin, valuemax는 생략이 가능하며 aria-valuenow를 통해 현재 퍼센트를 실시간으로 업데이트할 수 있습니다.

    마지막으로 aria-label 혹은 aria-labelledby 속성을 통해 progressbar 요소의 레이블을 정의합니다.

    이러한 것들을 적용했을 때 스크린 리더가 어떻게 읽어주는지를 체험해 볼 수 있는 샘플 페이지를 제작하였습니다.

    스크린 리더를 실행한 상태에서 아래 페이지를 실행해 보시기 바랍니다.

    샘플 페이지 가기

     

    댓글을 작성하려면 해주세요.
  • tip
    [android] 앱이 어떤 플랫폼으로 개발되었는지 디버깅으로 알아보기
    Webacc NV 2022-05-17 09:45:10

    최근 들어 안드로이드 앱이 여러 형태로 개발되고 있습니다. 

    네이티브 앱 중에서도 xml 기반의 view 형태로 개발된 앱이 대부분이지만 선언형 함수를 사용하여 jetpack compose로 개발된 앱들도 앞으로 점점 생겨날 것이라 예상하고 있습니다.

    또한 flutter로 개발된 앱들은 실제로도 많이 출시되고 있습니다.

    이러다보니 접근성 진 단 시 해결방안을 제시할 때 같은 네이티브 앱이라도 어떤 플랫폼으로 개발되었는지를 아는 것이 중요해지게 되었습니다.

    UIAutomator를 사용하여 접근성 트리를 디버깅하면 어느정도 어떤 플랫폼으로 개발하였는지를 유추할 수 있습니다. 

    우선 flutter, jetpack compose는 xml 레이아웃을 사용하지 않았기 때문에 가장 상단의 액티비티 제목 텍스트를 제외하고 Linear, Relative와 같은 요소들이 없으며 다 View라는 요소들로 레이아웃이 구성되어 있습니다.

    따라서 View > View > EditText 와 같은 구조로 트리가 구성된다면 view 시스템이 아닌 다른 플랫폼으로 개발되었을 가능성이 큽니다.

    다만 flutter인지 jetpack compose 형태로 개발되었는지를 접근성 트리를 통해 직관적으로 아는 방법은 쉽지 않습니다. 

    오히려 이것은 접근성 트리 디버깅보다는 스마트폰에서 TalkBack으로 확인하는 것이 더 쉽습니다.

    이 부분에 대해서는 추후 다루도록 하겠습니다.

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] improveAccessibility.js에 setAsHeading 메서드 추가
    Webacc NV 2022-05-10 09:53:13

    헤딩은 스크린 리더 사용자가 페이지의 섹션을 구분하는 측면에서 너무나 중요한 요소 중 하나입니다.

    또한 두말할 나위도 없이 헤딩 단위로 탐색이 가능하므로 대략적인 페이지의 레이아웃을 빠르게 파악하는 수단이 되기도 합니다.

    그런데 제목 요소를 헤딩으로 사용하지 않고 strong과 같은 태그에 제목 스타일을 적용하는 경우들이 종종 있습니다.

    이러한 요소들의 접근성을 조금 더 빠르게 해결하기 위해 setAsHeading 메서드를 만들어 공유하게 되었습니다.

    해당 메서드 안의 인자값으로는 타겟 즉 헤딩으로 정의하고자 하는 요소와 레벨정보(숫자)가 들어갑니다.

    그러면 해당 요소에 role="heading", aria-level="인자값으로 정의된 숫자"가 마크업됩니다.

    만약 특정 클래스를 가진 요소가 다 같은 레벨을 주어도 되는 요소라면 forEach function을 사용하여 동일한 클래스에 모두 적용되도록 할 수도 있을 것입니다.

    아래는 주의사항입니다.

    1. 헤딩 레벨은 반드시 1-6 사이의 숫자만 인가자값으로 주어야 합니다.

    2. 타겟에는 strong, div, span, em과 같은 태그 외에 button, link와 같은 요소를 지정해서는 안 됩니다.

    improveAccessibility.js 다운로드

    댓글을 작성하려면 해주세요.
  • tip
    [android native] 무한 스크롤되는 ViewPager 위젯 구현 시
    Webacc NV 2022-05-07 19:10:46

    ViewPager 위젯을 사용하여 개발된 화면에서 두 손가락으로 좌 또는 우로 스크롤해서 페이지가 변경되거나 탭레이아웃에서 탭을 전환하거나 몇초 간격으로 자동으로 페이지가 전환되는 경우 TalkBack에서는 5페이지 중 3페이지와 같이 페이지 정보를 음성출력합니다.

    그런데 특정 앱에서는 TalkBack에서 페이지 번호를 19,750페이지 중 17000페이지와 같이 페이지 정보를 이상하게 출력하는 경우를 접근성 진단을 하신 분들은 경험하셨을 것입니다.

    ViewPager 위젯의 getCount()라는 메서드는 페이지의 총 개수를 정의할 때 사용합니다.

    일반적으로 페이지 콘텐츠의 갯수를 인덱스 형태로 가져와서 pageCount.size 와 같이 총 페이지 정보를 줍니다.

    그러면 이 getCount() 정보를 바탕으로 페이지의 개수가 정해지게 되며 따라서 접근성 API에서도 이 정보를 활용하여 페이지 정보를 출력하는 것입니다.

    그런데 실제 페이지는 10개이지만 getCount()에서 가져오는 정보가 1000개, 10,000개가 넘는 경우 접근성에서는 페이지 총 개수가 getCount()에서 가져온 총 개수이므로 잘못된 페이지 정보를 주게 됩니다.

    기획 의도상 실제 콘텐츠는 10개라 하더라도 무한 스크롤 형태로 페이지를 넘길 때마다 계속 콘텐츠가 순환하도록 구현하는 경우가 있기 때문입니다.

    즉 ViewPager 콘텐츠는 기본적으로 처음, 끝으로 스크롤되었을 때 끝에서 처음으로, 혹은 처음에서 끝으로 바로 스크롤할 수 없기 때문에 무한 스크롤 형태로 구현하는 경우가 있습니다.

    그러나 스크린 리더 사용자는 페이지 번호는 계속 증가하지만 콘텐츠는 10개 내에서 계속 순환하게 되므로 상당한 혼란을 겪게 되는 것입니다.

    따라서 이를 해결하려면 톡백이 켜져 있을 때에는 무한 스크롤을 없애는 것이 필요합니다. 

    즉 실제 페이지 콘텐츠만큼만 스크롤되고 더 이상은 스크롤되지 않는 일반적인 방법으로 되돌리는 것입니다.

     

    댓글을 작성하려면 해주세요.
  • tip
    [flutter] Image 위젯과 onTap 이벤트 사용시 접근성 관련 주의사항
    Webacc NV 2022-05-05 18:57:16

    플러터에서 다음과 같은 형식으로 이미지를 추가할 수 있습니다.

    leading: Image.network(book.image)

    위와 같이 이미지 위젯이 포함되면 보이스오버, 톡백 모두 해당 요소의 텍스트 뒤에 이미지라고 읽습니다.

    문제는 onTap 이벤트가 포함되는 경우입니다.

    onTap은 말 그대로 사용자가 해당 요소를 탭했을 때 실행될 이벤트를 정의하는 것인데 여러 차례 말씀드린 것처럼 안드로이드에서는 ImageView라 하더라도 클릭 이벤트가 포함되는 순간 톡백에서 해당 요소를 버튼으로 처리합니다.

    그러나 보이스오버는 톡백과 달리 클릭 이벤트를 별도로 구분하지 못합니다.

    따라서 Image 위젯과 onTap을 적용한 상태에서 톡백으로만 접근성 테스트를 하면 버튼이라고 읽어주므로 접근성에 문제가 없는 것처럼 보이지만 보이스오버로 테스트하면 단순히 이미지라고만 읽어주어 사용자가 이중탭하여 실행할 수 있는 요소라는 것을 알려주지 못하게 됩니다.

    따라서 이를 해결하기 위해 반드시 상위에 Semantics 위젯을 덧씌우고 button: true 속성을 함께 포함해 줍니다.

    즉 Image 위젯이 Semantics child로 포함되도록 하는 것입니다.

    다만 이렇게 하면 보이스오버는 버튼 이미지라고 두 개의 요소 유형을 읽게 됩니다.

    단순한 버튼으로만 읽도록 하기 원한다면 excludeSemantics: true 속성을 Semantics 위젯에 함께 줍니다.

    아래는 코드 예시 입니다.

      @override
      Widget build(BuildContext context) {
        return ListTile(
          title: Text(book.title),
          leading: Semantics(
    								button: true, child: Image.network(book.image)),
          onTap: () {
            Navigator.of(context).push(MaterialPageRoute(
              builder: (context) => DetailScreen(
                book: book,
              ),
            ));
          },
        );
      }
    

     

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] createIdForChildrenOf 메서드 추가
    Webacc NV 2022-05-02 16:04:01

    얼마전 createIdForAllTag 메서드를 공유한 적이 있습니다.

    해당 메서드는 DOM이 불러와진 상태에서 body 내의 모든 태그를 대상으로 id를 생성하는 메서드였다면 지금 소개해 드릴 createIdForChildrenOf 메서드는 메서드의 이름에서도 알 수 있듯이 인자값에 엘리먼트를 주면 그 엘리먼트를 포함한 하위 요소들의 아이디가 없는 모든 태그에 아이디를 부여할 때 사용할 수 있습니다.

    만약 인자값 없이 createIdForChildrenOf() 라고만 입력하면 기존과 같이 body 내의 아이디가 없는 모든 태그에 아이디를 생성하게 됩니다.

    해당 메서드를 가장 잘 활용할 수 있는 예시는 바로 자동완성 편집창입니다.

    검색어와 같은 자동완성을 지원하는 편집창에 값을 입력 후 화살표를 내려 자동완성 리스트를 탐색할 때 편집창에 자동완성 텍스트가 입력되지 않는 경우에는 aria-activedescendant 속성을 활용하여 현재 가리키고 있는 자동완성 링크를 아이디 값으로 연결시켜 주어야 합니다.

    이때 자동완성 리스트가 불러와질 때마다 해당 메서드를 호출하면 아이디를 자동으로 생성해 주게 되므로 aria-activedescendant 속성에 아이디를 연결하기 편리해집니다.

    아래는 aria-activedescendant로 자동완성 리스트 값을 연결할 때 주의사항입니다.

    1. 자동완성 리스트는 반드시 role listbox > role option 형태로 마크업 되어 있어야 합니다.

    2. ul > li > a 구조라면 li는 반드시 role none 속성을 줍니다.

    3. 자동완성 리스트가 표시되지 않았거나 자동완성 목록 중 하나를 가리키고 있지 않는 경우에는 편집창에 aria-activedescendant="" 형태로 마크업하고 자동완성 리스트 중 하나를 가리키고 있을 경우에는 해당 아이디를 연결해줍니다.

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] improveAccessibility.js에 createIdForAllTag 메서드 추가
    Webacc NV 2022-04-19 11:09:16

    접근성 적용시 가장 고민하는 부분 중 하나가 바로 id를 지정하는 것입니다.

    aria-describedby, aria-labelledby를 비롯하여 aria-controls로 연결할 때에도 연결하고자 하는 태그에 id가 있어야 합니다.

    그래서 body 내의 script 태그를 제외한 id가 지정되지 않은 모든 태그에 태그 이름과 숫자를 조합한 id를 생성하는 메서드를 제작하여 공유하게 되었습니다.

    적용방법은 너무나 간단합니다.

    페이지가 완전히 로딩된 상태에서 createIdForAllTag() 메서드를 실행하면 끝입니다.

    그 상태에서 스크립트 등을 이용하여 참조하고자 하는 아이디를 가져와서 사용할 수 있습니다.

    improveAccessibility.js 다운로드

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] improveAccessibility.js에 waiAriaHasPopupMenu 메서드 추가
    Webacc NV 2022-04-18 15:52:08

    윈도 탐색기 폴더에서 오른쪽 마우스 혹은 팝업 키를 눌렀을 때 표시되는 상황에 맞는 메뉴와 같은 구조의 드롭다운 메뉴를 사용해야 하는 경우에 메뉴 레이어로 초점만 보내주는 것만으로는 스크린 리더 사용자의 레이아웃 파악이 어렵습니다.

    옵션으로 표시되는 메뉴는 모달이 아니기 때문에 레이어의 구분이 명확하지 않기 때문입니다.

    해당 UI는 ARIA 메뉴로 마크업하고 키보드 접근성만 구현하면 윈도 탐색기의 상황에 맞는 메뉴와 흡사한 탐색이 가능하여 훨씬 접근성을 높일 수 있습니다.

    그러나 옵션 메뉴의 키보드 접근성을 스크립트로 구현하기에는 많은 공수가 들어갑니다.

    이러한 부분을 조금이라도 해결하기 위해 널리에서 작년에 배포한 WAI-ARIA UI 스크립트 중 waiAriaHasPopupMenu 메서드를 조금 업데이트 하여 추가하게 되었습니다.

    해당 메서드를 사용하려면 다음 형식으로 마크업을 진행해야 합니다.

    1. 메뉴를 여는 버튼에는 버튼 혹은 role button 을 사용하고 aria-haspopup true 혹은 menu 속성을 줍니다.

    하위 1단계 메뉴에는 role menuitem을 사용합니다.

    2. 메뉴를 여는 버튼과 1단계 메뉴 그룹은 aria-controls로 연결합니다.

    3. 1단계 메뉴 그룹에는 role menu 속성을 줍니다.

    ul > li> a 구조라면 ul에는 role menu, li는 role none, a는 role menuitem입니다.

    이렇게 마크업하고 waiAriaHasPopupMenu() 메서드를 적용하면 다음과 같이 키보드가 동작합니다.

    1. 옵션 메뉴를 누르면 1단계 메뉴 첫 번째 요소로 초점이 이동합니다.

    2. 위 또는 아래 화살표를 눌러 메뉴 사이를 이동할 수 있습니다.

    직접 적용해야 하는 부분 및 참고사항:

    1. 옵션 메뉴가 열린 상태에서 탭 혹은 ESC키를 누르면 일반적으로 메뉴가 닫히고 기존 버튼으로 돌아와야 합니다. 그러나 해당 부분 구현은 페이지마다 메뉴를 닫는 방법이 다양하고 일정하지 않아서 해당 메서드에서는 제외하였습니다. 

    2. 현재는 role menuitem 사이에서만 스크립트가 적용되어 있으나 조만간 role menuitemradio 속성이 있을 경우도 함께 추가할 예정입니다.

    3. 메뉴 버튼을 눌렀을 때 메뉴 레이어가 동적으로 생성되는 경우에는 메뉴 요소들이 생성된 시점에 waiAriaHasPopupMenu() 메서드를 적용해야 합니다.

    improveAccessibility.js 다운로드

    댓글을 작성하려면 해주세요.
  • tip
    [flutter] CheckBox 위젯은 VoiceOver에서 스위치버튼 역할로 정의됨
    Webacc NV 2022-04-11 19:39:35

    플러터에서는 체크박스 위젯이 존재하는데 네이티브 앱 iOS VoiceOver 에는 체크박스 역할이 없습니다.

    따라서 플러터에서 체크박스 구현 시 iOS 에서는 이를 스위치 버튼으로 읽게 됩니다.

    즉 UISwitch 요소로 구현이 되는 것입니다.

    체크됨은 켜짐, 체크되지 않음은 꺼짐입니다.

    사실상 의미론적으로는 스위치와 체크박스는 다른 성격을 가지고 있으나 플러터로 앱 개발 시 현재로서는 이러한 차이가 있음을 참고하여 접근성 진단을 진행할 필요가 있겠습니다.

    댓글을 작성하려면 해주세요.
  • tip
    [flutter] 화면에 보여지는 일반 텍스트의 레이블을 대체 텍스트로 대체할 경우
    Webacc NV 2022-04-10 11:52:32

    iOS, android에서 앱 접근성을 구현할 때 상황에 따라서는 화면에 보여지는 텍스트에 추가 정보를 제공하기 위해 대체 텍스트로 텍스트를 덮어 씌우는 경우가 있습니다.

    예를 들어 아이콘으로 수량이라는 의미를 표시한 다음 숫자만 화면에 텍스트로 표시되면 스크린 리더에서는 숫자만 읽어주게 되므로 구체적인 의미를 알기 어렵기 때문입니다.

    네이티브 앱에서는 accessibilityLabel 혹은 contentDescription 속성을 통해 텍스트뷰에 대체 텍스트를 적용하면 기존 텍스트가 덮어씌워집니다.

    그러나 플러터에서는 Senatics 위젯 안에 단순히 label만 제공하면 레이블과 화면에 있는 텍스트를 함께 읽게 됩니다.

    읽는 순서는 위젯 트리 구조와 같이 접근성 레이블 텍스트 + 화면에 보여지는 텍스트 순입니다.

    따라서 다음과 같이 정리를 할 수 있습니다.

    1. 대체 텍스트에서 화면에 있는 텍스트를 포함해야 하는 경우에는 추가 정보만 label에 포함합니다.

    2. 만약 대체 텍스트로 화면에 보여지는 텍스트를 완전히 덮어 씌우기를 원한다면 Semantics 위젯 안에 excludeSemantics: true 속성을 함께 줍니다.

    해당 속성은 말 그대로 하위의 모든 정보를 접근성 노드에서 무시하겠다는 이야기입니다.

    댓글을 작성하려면 해주세요.
  • tip
    [android wear] 워치에서의 최신 TalkBack 업데이트에 따른 클릭 리스너가 포함된 ImageView 처리방식 수정 관련
    Webacc NV 2022-04-02 12:17:29

    22년 3월, 널리 아티클을 통하여 갤럭시 워치4의 접근성 기능에 대해 다루었습니다.

    해당 아티클을 작성할 당시 ImageView 요소에 클릭 리스너가 포함되는 경우 TalkBack에서 이를 버튼이 아닌 이미지로 읽어주는 이슈가 있음을 기술하였습니다.

    그런데 얼마전 wearOS TalkBack이 한차례 업데이트 되었는데 업데이트 이후로는 스마트폰과 마찬가지로 ImageView에 클릭 리스너가 포함되면 버튼으로 읽어줍니다.

    앱 개발 시 참고하시기 바랍니다.

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] next.js 라이브러리로 웹 개발 시 페이지 타이틀 announcement 접근성 적용 관련
    Webacc NV 2022-04-01 18:37:20

    next.js 라이브러리는 react를 기반으로 하고 있습니다. 

    그래서 페이지 내에서 내비게이션의 여러 링크를 눌러 다른 페이지로 전환하더라도 페이지 전체가 새로고침되지 않기 때문에 접근성 적용을 하지 않으면 스크린 리더 사용자는 페이지가 변경되었음을 알 수 없습니다.

    그런데 next.js 라이브러리는 기본적으로 페이지 타이틀이 변경될 때 페이지 제목을 스크린 리더가 자동으로 읽어주도록 하는 태그 next-route-announcer가 기본으로 붙습니다.

    해당 태그 안에는 <p> 태그가 하나 있으며 페이지가 처음 불러와질 때에는 텍스트 없이 불러오지만 페이지가 전환되면 페이지 타이틀을 <p> 안에 자동으로 삽입하게 됩니다.

    <p> 태그에는 다음의 속성이 포함됩니다.

    <p aria-live="assertive" id="__next-route-announcer__" role="alert" style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap; overflow-wrap: normal;"></p>

    즉 해당 요소의 텍스트는 스크린 리더용 메시지이며 화면상으로는 출력되지 않는 것입니다.

    그런데 실제 next.js로 페이지를 개발하다보면 해당 기능에 버그가 있는 것을 알 수 있습니다.

    1. 페이지를 전환할 때 변경된 페이지 제목을 텍스트로 가지고 오지 못하거나 또는 이전 페이지 제목을 텍스트로 삽입해버리는 경우: 내비게이션 메뉴의 특정 링크를 누르면 어떤 경우에는 페이지 제목 타이틀을 잘 가지고 오지만 어떤 경우에는 이전의 텍스트를 그대로 유지하여 실시간으로 변경되는 페이지 제목을 제대로 읽어주지 못하는 경우입니다. 한번 페이지 제목 텍스트를 갱신하지 못하면 그 다음 다른 페이지로 전환 시 이전 제목 텍스트를 가지고 오게 되어 오히려 스크린 리더 사용자에게 더 혼란을 줍니다.

    2. 스크린 리더용 제목 텍스트가 사라지지 않는 문제: next-route-announcer 태그는 페이지가 전환될 때 토스트 형태로 스크린 리더가 제목을 읽도록 하는 역할을 하는데 해당 텍스트가 스크린 리더 가상커서에 계속 남아 있습니다.

    따라서 스크린 리더 사용자가 페이지를 탐색할 때 가장 아래쪽에 토스트 메시지를 한번 더 읽어주게 되는 이슈가 있습니다.

    마치 토스트로 떴다가 사라져야 할 텍스트가 화면 상에 계속 남아 있는 것과 같습니다.

    따라서 해당 이슈가 next.js 라이브러리 자체에서 수정되기 전까지는 다음과 같은 간단한 방법으로 해결이 가능합니다.

    1. 페이지가 최초로 불러와질 때 next-route-announcer 태그는 aria-hidden true 속성을 추가하여 스크린 리더에서 숨깁니다.

    이렇게 되면 페이지 전환 시 우선 아무런 내용도 읽지 않게 됩니다.

    2. 저희가 현재 배포하고 있는 improveAccessibility.js 안에 포함되어 있는 announceForAccessibility 메서드를 활용할 수 있습니다.

    페이지 내비게이션이 있는 영역을 클릭할 때 약 0.5초 정도 딜레이를 주어 변경된 페이지 제목(document.title) 텍스트를 인자 값으로 주어 페이지 제목을 스크린 리더에서 읽도록 할 수 있습니다.

    announceForAccessibility 함수에 대한 자세한 설명은 관련 팁을 참고하시면 됩니다.

    댓글을 작성하려면 해주세요.
  • news
    2022년 3월 15일부터 17일까지 개최되는 axe 컨퍼런스 소개
    Webacc NV 2022-03-12 10:33:43

    axe 는 접근성 자동화 진단 도구로서 웹의 경우 크롬 확장 프로그램으로 설치하여 쉽게 누구나 사용할 수 있는 것이 특징입니다.

    axe 를 개발하고 있는 Deque 에서 axe 접근성 컨퍼런스를 온라인으로 개최합니다.

    3월 15일부터 17일까지 진행되는 이번 행사에는 개발, 디자인, 기관의 더 나은 접근성 및 기타 주제 등 여러 트랙으로 나누어 다양한 접근성 관련 세미나가 진행되며 등록 신청을 하면 무료로 들을 수 있습니다.

    특히 올해 컨퍼런스 키노트에서는 The Future of the Web and Accessibility 라는 주제로 Sir Tim Berners-Lee 가 발표를 진행합니다.

    자세한 내용은 axe 컨퍼런스 홈페이지를 참고해 주시기 바랍니다.

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] modalDialog, setAsModal 메서드 업데이트
    Webacc NV 2022-03-11 11:07:23

    널리 아티클 및 팁을 통하여 대화상자 콘텐츠가 화면을 덮는 형태의 대화상자 팝업에서 접근성을 적용할 수 있는 라이브러리를 공유하였습니다.

    modalDialog 메서드는 대화상자를 여는 버튼과 대화상자 콘텐츠를 연결하여 사용할 수 있는 것이고 setAsModal 은 대화상자 팝업이 동적으로 생성되는 경우 대화상자가 열린 상태에서 라이브러리를 적용할 수 있는 메서드라고 했습니다.

    해당 두 메서드의 업데이트 내용이 있어 공유합니다.

    이제는 firstTab, lastTab 을 CSS 에 별도 추가하지 않아도 됩니다.

    기존에는 대화상자 내에서 탭키로 초점이 가야 하는 요소 들 중 첫 요소와 마지막 요소에 반드시 firstTab lastTab 속성을 HTML CSS 에 추가를 해야만 했습니다.

    그러나 해당 속성이 없어도 대화상자 내의 전체 포커스 가능한 요소(탭인덱스 0 포함)을 가져와서 firstTab, lastTab 을 자동 지정하게 됩니다.

    따라서 다음과 같이 사용할 수 있습니다.

    1. modalDialog: 인자값이 없으며 modalDialog() 라고 선언하면 대화상자를 여는 버튼과 대화상자 컨테이너 전체를 가져와서 접근성을 적용합니다.

    대화상자를 여는 버튼에는 aria-haspopup="dialog" aria-controls="대화상자와 연결된 아이디"로 마크업합니다.

    대화상자 컨테이너는 대화상자가 열리고 닫힐 때 스타일의 변화가 있거나 aria-hidden true/false 값으로 변경되는 컨테이너에 role dialog, aria-modal true 속성을 추가합니다.

    대화상자 내부에 있는 요소들 중 대화상자를 닫는 요소에 closeModal CSS 속성을 추가합니다.

    2. setAsModal: 인자값으로는 대화상자를 포함하는 div 를 가리키면 됩니다. 그리고 대화상자가 표시되는 시점에 해당 메서드를 적용합니다. 물론 대화상자 컨테이너에는 반드시 role dialog, aria-modal true 속성이 포함되어 있어야 합니다.

    취소 키를 눌러 대화상자가 닫히도록 하려면 대화상자를 닫는 버튼에 closeModal class 속성을 추가하면 도비니다. 해당 속성이 없으면 취소 키는 아무런 동작도 하지 않습니다.

    improveAccessibility.js 다운로드

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] radioAsButton 메서드 추가 및 샘플 페이지 소개
    Webacc NV 2022-03-09 21:50:13

    라디오버튼은 여러 옵션 중 하나를 선택한다는 의미를 가지고 있습니다.

    그런데 요즘에는 라디오 버튼을 단순히 옵션을 선택하는 용도로만 사용하지 않고 콘텐츠 자체를 갱신시키는 경우에도 사용하는 경우를 종종 보곤 합니다.

    이때는 스크린 리더 사용자나 키보드를 사용하여 라디오버튼을 선택하는 경우 상당히 난감한 상황을 겪을 수 있습니다.

    라디오버튼은 위젯의 특성상 키보드에서 화살표를 누르면 이전 혹은 다음 라디오버튼이 자동 선택됩니다.

    그런데 선택되자마자 페이지가 갱신되는 경우 라디오버튼 자체의 초점을 잃을 수 있습니다.

    따라서 한 라디오그룹의 옵션이 여러 개인 경우 화살표를 누를 때마다 초점을 잃게 되므로 의도치 않은 콘텐츠 실행이 지속적으로 발생하게 되는 것입니다.

    라디오버튼은 되도록 옵션을 선택하는 용도로만 사용해야 합니다.

    그러나 부득이 그렇지 못할 경우 접근성 적용을 위해 radioAsButton() 메서드를 만들고 샘플 페이지와 함께 공유를 하게 되었습니다.

    원리는 간단합니다.

    일반적으로 라디오버튼은 접근성을 적용하기 위해 label for 와 함께 사용을 합니다.

    혹은 암묵적 레이블 하위에 라디오 버튼을 둡니다. 따라서 다음과 같은 방법을 사용할 수 있습니다.

    1. 동적으로 페이지가 변경되는 라디오버튼을 구현하는 경우에는 <input type="radio"> 는 inert 속성으로 숨깁니다. 해당 속성은 브라우저에서 화면에서 보이는 것과 별개로 해당 요소를 초점 및 접근성 트리에서 사라지도록 합니다.

    2. 각 label 에 role button 속성을 줍니다.

    3. 각 label 에 키보드 접근성을 구현합니다.

    4. 라디오가 체크되면 연결된 label 에 aria-current true 속성을 줍니다.

    위와 같이 구현하면 키보드 사용자는 각 라디오버튼은 접근이 되지 않고 키보드 접근성이 구현된 커스텀 버튼만 접근이 되므로 의도치 않은 콘텐츠 실행을 방지할 수 있습니다.

    아래는 radioAsButton 메서드 설명입니다.

    1. 인자값에는 컨테이너 즉 라디오버튼들이 있는 요소를 줍니다.

    2. 각 레이블과 라디오 인풋은 for 로 연결되어 있어야 합니다.

    위와 같이 적용하면 radioAsButton 인자값으로 준 요소 하위의 라디오, 레이블을 찾아 접근성을 구현하게 됩니다.

    단 레이블에 대한 키보드 접근성은 ariaButton() 메서드를 통해 추가할 수 있으므로 radioAsButton 에는 추가하지 않았습니다.

    샘플 페이지 테스트해보기

    improveAccessibility.js 다운로드

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] improveAcessibility.js 파일에 추가된 focusTogetherForMobile 메서드 소개
    Webacc NV 2022-03-08 15:13:10

    모바일 디바이스, 특히 아이폰에서 여러 텍스트 스타일이 하나의 링크로 구성된 페이지를 VoiceOver로 탐색할 때 수많은 초점 분리로 인해 탐색이 비효율적인 경험을 해 보셨을 줄로 압니다.

    해당 이슈는 VoiceOver에서 개선을 해 주 어야 하는 이슈이지만 임시 방편으로나마 해당 유형의 링크 탐색 시 스크린 리더 사용성을 개선할 수 있도록 focusTogetherForMobile() 메서드를 만들어 공유하게 되었습니다.

    해당 메서드의 인자값은 없으며 improveAccessibility.js 를 추가한 상태에서 해당 메서드를 실행해 주기만 하면 됩니다.

    특징은 다음과 같습니다.

    1. 아이폰, 안드로이드 모바일 디바이스에서만 동작합니다.

    2. <a href 속성이 있고 링크 안에 헤딩이나 이미지가 없는 모든 요소에서 내부 텍스트는 다 aria-hidden 으로 숨깁니다.

    3. 대신 aria-label 안에 모든 링크의 텍스트를 포함시킵니다.

    improveAccessibility.js 다운로드

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] improveAccessibility.js 파일에 ariaCurrent() 메서드 추가
    Webacc NV 2022-03-03 14:52:00

    페이지가 새로고침되지 않은 상태에서 정렬 옵션과 같이 여러 요소 중 하나가 현재 선택된 상태임을 표시할 때 aria-current 속성을 사용할 수 있습니다. 

    aria-current 에는 page, true, step 과 같은 여러 속성들이 있는데 그 중 특정 요소 하위의 버튼들 사이에서 aria-current 속성이 true/false 로 변경되어야 하는 경우 접근성을 쉽게 적용할 수 있도록 ariaCurrent 메서드를 만들어 공유하게 되었습니다.

    인자 값으로는 aria-current true/false 값들을 품고 있는 요소명을 적어주면 됩니다.

    예: ariaCurrent(documentQuerySelector("#sort > ul"))

    그러면 다음과 같이 동작합니다.

    1. 인자 값으로 들어 있는 요소 하위의 aria-current 속성을 모두 찾습니다.

    2. aria-current 속성을 가진 요소 중 하나를 클릭하면 클릭한 것은 true, 나머지는 false로 설정합니다.

    따라서 해당 메서드를 적용하기 전에 aria-current 속성이 기본 마크업 되어 있어야 하며 모두 false 이거나 하나가 true 여야 합니다.

    improveAccessibility.js 다운로드

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] waiAriaListBox() 메서드 추가
    Webacc NV 2022-03-01 15:24:01

    improveAccessibility.js 라이브러리에 waiAriaListBox() 메서드를 추가하여 사용법을 공유합니다.

    '정렬 선택 옵션, 인기순'과 같은 버튼이 있고 버튼을 누르면 다른 옵션 중 하나로 변경할 수 있는 리스트가 표시되는 커스텀 콤보상자 위젯에서 사용할 수 있습니다.

    해당 메서드를 사용하면 위 또는 아래 화살표키로 옵션을 선택할 수 있고 ESC, 탭, 쉬프트 탭을 눌러 옵션 축소 및 기존 버튼으로 초점을 되돌릴 수 있습니다.

    주의: role listbox, role option 마크업은 반드시 하위 옵션이 하나의 리스트로만 구현되고 옵션 리스트만 있을 때 사용하시기 바랍니다. 

    즉 커스텀 콤보박스 하위에 편집창 체크박스 등의 추가 옵션들이 있을 경우는 리스트박스형 위젯으로 접근성 적용을 하는 것은 적절하지 않습니다.

    해당 메서드를 사용하려면 다음과 같은 마크업이 필요합니다.

    1. 버튼과 옵션 리스트 컨테이너는 aria-controls로 반드시 연결되어 있어야 합니다.

    이는 버튼을 눌렀을 때 aria-expanded 속성이 true로 변경되면 그 버튼과 연결된 컨테이너에 들어 있는 옵션으로 초점을 보내기 때문입니다. 

    옵션 컨테이너에 id를 부여하고 aria-controls="id" 형식으로 연결할 수 있습니다.

    옵션 컨테이너에 id를 부여할 때에는 반드시 스타일 또는 aria-hidden 또는 하위에 요소가 추가 삭제되는 곳에 부여합니다.

    2. 옵션을 표시하거나 숨기는 버튼에는 aria-expanded="false", aria-haspopup="listbox"로 마크업을 합니다.

    3. 옵션 컨테이너에서 각 옵션들은 role="option", 옵션을 감싸는 컨테이너는 role="listbox"로 마크업합니다.

    4. 기본 선택된 옵션에는 aria-selected="true", 선택되지 않은 옵션들에는 "false" 입니다. 

    5. ul > li > a 혹은 ul > li > button 같은 구조에서는 ul은 role="listbox", li는 role="none", a 또는 button은 role="option" 입니다.

    위와 같이 마크업을 하였다면 improveAccessibility.js를 로드한 상태에서 다음과 같이 사용할 수 있습니다.

    1. waiAriaListBox() 메서드를 실행합니다.

    2. ariaExpanded() 메서드를 함께 실행합니다. 

    단 ariaExpanded() 메서드는 이미 확장 축소에 대한 구현을 해 놓은 상태라면 추가하지 않아도 됩니다.

    이렇게 하면 다음과 같이 동작합니다.

    1. 버튼을 눌러 aria-expanded 속성이 true로 변경되면 초점을 옵션 중 aria-selected true 요소로 보냅니다. 

    만약 선택된 요소가 없으면 첫 요소로 보냅니다.

    2. aria-selected true 속성에는 tabindex 0, 나머지 요소에는 tabindex -1을 적용합니다.

    aria-selected true 속성이 없으면 첫 번째 요소에 tabindex 0을 적용합니다.

    3. 위 또는 아래 화살표로 옵션을 탐색하게 하고 옵션에서 엔터를 누르면 선택한 옵션이 aria-slected true, 나머지 옵션은 false로 재조정합니다.

    4. 옵션이 사라지면 포커스는 다시 옵션을 여는 버튼으로 보냅니다.

    improveAccessibility.js 다운로드

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] ariaExpanded() 메서드 업데이트
    Webacc NV 2022-02-25 15:20:35

    확장, 축소를 하는 버튼에 페이지 로딩 시의 기본 상태를 aria-expanded true 혹은 false 로 마크업을 하고 aria-controls 속성으로 확장 축소 영역과 연결시키면 사용자가 버튼을 누를 때마다 확장 혹은 축소되는 영역의 display 혹은 aria-hidden 속성을 캐치하여 자동으로 aria-expanded true 혹은 false 가 적용되는 스크립트를 소개한바 있습니다.

    이번에 해당 스크립트에 약간의 업데이트가 적용되었습니다.

    확장 축소되는 영역을 캐치할 때 어떤 페이지에서는 display block 속성은 유지한 채 확장되었을 때 하위 콘텐츠가 표시되었다가 축소되면 하위 콘텐츠 자체가 없어지는 형식으로 구현되기도 합니다.

    위와 같은 형식으로 영역이 컨트롤되는 경우에는 기존 ariaExpanded() 메서드로는 캐치를 할 수 없기 때문에 업데이트를 하게 되었습니다.

    따라서 현재는 aria-expanded false 인 상태에서 사용자가 클릭을 했을 때 확장되는 영역에 display block 이면서 하위에 자식 요소드링 있어야 true 로 변경되고 true 인 상태에서 사용자가 클릭을 했을 때 display block 이라도 하위에 자식 요소들이 없어지면 false 로 적용되도록 했습니다.

    물론 true 상태에서 사용자가 클릭했을 때 display 가 none 으로 변경되면 기존과 같이 false 로 변경됩니다.

    따라서 해당 스크립트를 사용할 때는 aria-controls 와 연결시키는 id 를, 확장 축소될 때 display 속성이 변경되거나 혹은 하위 요소가 사라졌다 나타났다 하는 div 에 주어야 합니다.

    improveAccessibility.js 다운로드

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] createElementsId 메서드 업데이트
    Webacc NV 2022-02-24 15:52:16

    반복되는 텍스트가 많은 콘텐츠에서 타겟 1들을 순서대로 아이디로 지정하고 타겟2에 aria-controls 혹은 aria-describedby 속성으로 쉽게 타겟 1의 각 아이디를 연결할 수 있는 메서드를 얼마전 공유했습니다.

    해당 메서드가 다음과 같이 업데이트 되면서 사용법이 약간 변경되었습니다.

    1. 이제는 타겟 1의 아이디를 타겟 2, 3, 4와 같이 여러 요소에 aria-describedby 또는 aria-controls 속성으로 연결할 수 있습니다.

    방법은 다음과 같습니다. 

    targetValue2 라는 변수를 만들고 그 안에 아이디를 가지고 aria-descriedby 혹은 aria-controls 로 연결할 값들을 적어 줍니다.

    값은 클래스 아니면 네임입니다. 

    예시: var targetValue2 = ["button__favorite-item", "element-info-qty-minus", "element-info-qty-plus"];

    2. 기존에는 대상 범위를 문서 전체로 자동 지정하고 아이디 역시 타겟밸류1 값을 가져와서 자동 지정했으나 이렇게 하면 여러 오류가 있을 수 있어 엘리먼트 및 아이디는 함수 안에 포함하도록 했습니다.

    따라서 인자값으로는 범위를 지정하는 element, id를 하나씩 생성할 요소(클래스 혹은 네임. 대부분 텍스트를 가지고 있는 네임이나 클래스가 해당될 것입니다), 아이디(입력 시 0, 1, 2 등의 숫자가 하나씩 붙으며 자동 생성됩니다), 변수로 지정한 targetValue2, 마지막으로 aria-describedby 혹은 aria-controls 입니다.

    주의: 해당 라이브러리를 사용하실 때에는 반드시 아이디를 지정하는 요소와 해당 아이디를 바탕으로 aria-controls 혹은 aria-describedby 로 지정하는 요소의 개수가 반드시 동일해야 합니다. 

    다음은 예시 입니다.

    var targetValue2 = ["button__favorite-item", "element-info-qty-minus", "element-info-qty-plus"];
    var container = document.querySelector("#contentRegion");
    createElementsId(container, "element-info-name", "a11y", targetValue2, "aria-describedby");

    참고: targetValue2에 들어가는 요소, 즉 aria-describedby 속성이 들어가는 대상이 단 하나인 경우는 굳이 targetValue2에 대한 변수를 만들지 않고 해당 클래스명 혹은  name 속성만 넣어 주시면 됩니다.

    improveAccessibility.js 다운로드

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] improveAccessibility.js 파일에 setAsModal 메서드 추가
    Webacc NV 2022-02-22 15:10:17

    기존에 널리 아티클로도 공개하였고 improveAccessibility.js 파일에 추가되어 있는 modalDialog 메서드와 더불어서 setAsModal 메서드를 하나 더 추가하게 되었습니다.

    기존 modalDialog 메서드는 메서드를 선언한 후부터 특정 조건을 만족할 때 동작하는 메서드라면 setAsModal 메서드는 모달 대화상자가 표시된 상태에서 바로 모달 접근성을 적용해야 할 때 사용할 수 있습니다.

    따라서 대화상자가 표시된 영역 엘리먼트를 인자 값으로 주면 됩니다.

    예: setAsModal(document.querySelector("#BaseContainer"));

    1. 엘리먼트는 대화상자가 사라질 때 스타일 혹은 속성의 변화가 있는 곳이어야 합니다.

    혹은 엘리먼트 자체가 대화상자가 사라질 때 같이 사라지는 경우도 캐치가 가능합니다.

    2. 해당 메서드를 선언하면 해당 엘리먼트의 속성이 변경될 때까지만 모달 접근성이 동작하게 됩니다.

    3. 대화상자 내부에 class="firstTab" class="lastTab" class="closeModal" 속성은 반드시 필요합니다.

    firstTab 은 대화상자 내에서 탭 키를 통해 첫 번째 포커스 되는 요소이며 lastTab은 마지막 요소, closeModal은 ESC를 눌렀을 때 동작하는 대화상자를 닫는 엘리먼트에 추가합니다.

    단 초점이 가는 요소가 단 하나라면 lastTab은 없어도 됩니다.

    4. 대화상자가 표시되었을 때 해당 메서드를 실행하면 class="firstTab" 요소에 자동 포커스 됩니다.

    5. 대화상자를 닫을 때에는 포커스가 가야 할 요소를 지정해 주어야 합니다.

    improveAccessibility.js 다운로드

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] improveAccessibility.js에 ariaTab() 메서드 추가
    Webacc NV 2022-02-21 18:22:41

    WAI-ARIA 탭 컨트롤 적용 시 마크업만으로 키보드 관련 접근성을 적용할 수 있는 스크립트를 추가합니다.

    기존 improveAccessibility.js 파일을 다운받으면 ariaTab() 메서드가 추가되어 있습니다. 해당 메서드를 사용하면

    1. 탭키를 누르면 선택된 탭에만 초점 이동.

    2. 오른쪽 왼쪽 화살표로 탭 전환.

     

    사용하려면 다음과 같이 마크업을 합니다.

    1. <ul> <li> 하위에 <a> <button> 등의 구조일 경우 li에는 role none 속성을 추가합니다.

    2. <ul>에는 role tablist, 각 탭에는 role tab을 추가합니다.

    tab을 div가 감싸고 있으면 div 에 role tablist 를 추가합니다.

    3. 페이지 로딩 시 기본적으로 선택된 탭에는 aria-selected true 속성을, 선택되지 않은 탭에는 false를 추가합니다.

    4. 탭과 연결된 본문 컨테이너에 id를 주고 선택된 탭에 aria-controls로 id를 연결합니다.

    5. 탭을 전환하여도 초점이 유지되는 경우는 상관이 없으나 탭을 전환하는 순간 초점을 잃어버리는 방식으로 페이지가 갱신되는 경우에는 반드시 role tablist와 함께 data-mode="aria1.2" 속성을 추가해 주시기 바랍니다. 이렇게 하면 탭 사이를 화살표로 이동해도 포커스만 이동할 뿐 탭이 전환되지 않게 됩니다.

    improveAccessibility.js 다운받기

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] improveAccessibility.js에 setHiddenExceptForThis 함수 추가
    Webacc NV 2022-02-18 19:51:50

    improveAccessibility.js에 포함된 여러 함수 중 하나인 modalDialog()를 사용하면 마크업 조건을 충족할 경우 모달로 지정한 줄기를 제외한 나머지 모든 영역에는 inert="true"가 자동으로 추가되는 함수가 포함되어 있습니다.

    해당 함수를 별도로 사용할 수 있도록 setHiddenExceptForThis 함수를 글로벌로 추가하여 improveAccessibility.js를 업데이트 했습니다.

    모달 대화상자 접근성 적용시 다른 부분은 잘 적용되어 있는데 inert 관련 부분만 적용하지 못했을 경우 사용할 수 있습니다.

    인자값은 inert true에서 제외해야 할 요소(element)입니다.

    예: setHiddenExceptForThis(document.querySelector("#containerLayer"));

    위와 같은 형식으로 모달 대화상자가 열린 시점에 적용합니다.

    그리고 대화상자가 사라질 때에는 off 스트링을 아래 예시와 같이 추가합니다.

    setHiddenExceptForThis(document.querySelector("#containerLayer"), 'off');

    js 다운로드

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] improveAccessibility.js 파일에 추가된 createElementsId 메서드 소개
    Webacc NV 2022-02-16 14:18:58

    뮤직 리스트, 상품리스트 등의 리스트 형식으로 되어 있는 페이지에는 각각의 아이템마다 재생, 삭제, 찜하기 등의 반복되는 레이블을 가진 버튼들이 표시되는 경우가 만습니다.

    이 버튼들의 레이블은 다 동일한 삭제, 재생, 찜하기 등일 것입니다.

    그런데 스크린 리더 사용자가 버튼 단위로 이동할 경우 버튼 레이블이 다 동일하므로 어떤 요소에 대한 삭제, 어떤 요소에 대한 재생 등인지를 바로바로 파악할 수 없는 문제가 있습니다.

    이를 해결하는 방법 중 하나가 각 버튼에 aria-describedby 속성을 통해서 연관된 텍스트와 연결하는 것입니다.

    그러면 버튼 단위로 이동하거나 탭키를 눌러 요소 단위로 이동할 때 마치 힌트처럼 연결된 텍스트를 읽어주게 됩니다.

    문제는 각각의 텍스트에 id 속성이 있어야 한다는 것입니다.

    그래서 기존에 id 속성이 없는 텍스트에 aria-describedby 속성을 위해 id를 생성하는 함수를 만들어 공유하게 되었습니다.

    함수 이름은 createElementsId 이며 인자값으로는 3가지 요소가 들어가게 됩니다.

    1. targetValue1: id를 생성해야 할 텍스트의 class 네임이나 name 속성값을 넣습니다.

    2. targetValue2: 만들어진 각 id를 순서대로 aria-describedby 속성으로 매칭시켜야 할 요소의 class 네임 혹은 name 속성을 넣습니다.

    3. ariaProperty: aria-describedby 혹은 aria-controls 속성 중 하나를 넣습니다.

    이렇게 하면 해당 페이지의 targetValue1, 2를 찾아 aria-describedby 혹은 aria-controls로 연결하게 됩니다.

    단 id로 지정해야 할 요소와 연결할 요소의 개수는 반드시 동일해야 합니다.

    아래는 해당 함수를 적용한 예시입니다.

    createElementsId("item_name", "sd_favorite", "aria-describedby");

    해당 함수는 improveAccessibility.js 를 다운받아 사용할 수 있습니다.

    댓글을 작성하려면 해주세요.
  • tip
    [iOS native] UITextField 요소를 VoiceOver에서 버튼으로 읽도록 해야 할 때
    Webacc NV 2022-02-05 10:12:19

    UITextField는 사용자의 입력값을 받을 때 사용되는 텍스트필드 요소입니다.

    따라서 당연하게도 UITextField 객체를 사용하는 순간 사용자가 해당 요소에 초점을 맞추면 VoiceOver에서는 입력하려면 이중탭하라는 힌트 메시지를 자동으로 출력합니다.

    그런데 상황에 따라서는 UITextField를 버튼으로 읽도록 해야 하는 경우가 있습니다.

    개발 시에 특정 값이 텍스트필드로 들어오게 한 다음 텍스트필드를 탭하면 특정 기능이 실행되도록 하는 경우가 있기 때문입니다.

    이런 경우에도 VoiceOver에서 텍스트필드로 읽는다면 스크린 리더 사용자는 데이터를 입력하는 요소로 생각하게 될 것입니다.

    이쯤 되면 접근성을 아시는 분들은 해당 객체에 accessibilityTraits = .button을 사용하면 될 것이라고 생각할 수 있습니다.

    그러나 안타깝게도 그렇게 하면 버튼 텍스트필드라고 읽어주고 입력하려면 이중탭하라는 힌트 메시지를 여전히 출력합니다.

    따라서 순수한 버튼으로 읽도록 하려면 accessibilityTraits 안에 staticText와 button을 함께 추가해 주어야 합니다.

    예: nameTextField.accessibilityTraits = [.staticText, .button]

    댓글을 작성하려면 해주세요.
  • tip
    [javascript] improveAccessibility.js 파일에 radio.js 함수 추가
    Webacc NV 2022-02-03 19:51:41

    널리에서 지난 9월에 배포한 WAI-ARIA UI js 기능 중 radio/js 파일을 일부 업데이트하고 improveAccessibility.js에 ariaRadio() 함수를 추가하게 되었습니다.

    커스텀 라디오버튼에 다음과 같이 WAI-ARIA 속성을 추가하고 ariaRadio() 함수를 사용하면 네이티브 라디오와 유사하게 키보드 접근성이 구현됩니다. 

    1. 라디오버튼에는 role="radio" 속성 추가.

    2. 체크된 라디오버튼에는 aria-checked="true", 체크되지 않은 라디오버튼에는 false 추가.

    디폴트로 체크된 라디오버튼이 없는 경우에는 모두 aria-checked="false"로 마크업하면 됩니다.

    3. 사용 불가한 라디오버튼이 있다면 해당 요소에 aria-disabled="true" 속성 추가.

    참고: 각 라디오그룹들은 라디오를 품고 있는 요소에 role="radiogroup" 속성을 추가하여야 합니다.

    각 radiogroup에는 aria-label 혹은 aria-labelledby 속성을 통하여 각 라디오그룹의 접근성 네임을 지정할 수 있습니다.

     

    마크업이 완료되었다면 DOM이 다 불러와진 상태에서 ariaRadio() 함수를 추가하면 다음이 적용됩니다.

    1. 탭키로는 선택된 라디오버튼 하나만 초점 제공되고 화살표로 라디오버튼 선택.

    선택된 라디오버튼이 없으면 첫 번째 라디오버튼에 초점이 제공됩니다.

    2. 오른쪽과 아래쪽 화살표는 다음 라디오버튼, 왼쪽과 위쪽 화살표는 이전 라디오버튼으로 포커스 되고 aria-checked 속성이 true로 변경되며 클릭 이벤트를 보냅니다. 또한 라디오버튼 사이를 순환하여 첫 라디오버튼에서 왼쪽 혹은 위쪽 화살표를 누르면 마지막 라디오버튼으로, 마지막 라디오버튼에서 오른쪽 혹은 아래쪽 화살표를 누르면 첫 번째 라디오버튼으로 이동됩니다.

    3. aria-disabled="true" 속성이 있으면 해당 요소에는 aria-checked="true"로 변경되지 않습니다.

    업데이트된 improveAccessibility.js 다운받기

    댓글을 작성하려면 해주세요.
  • tip
    [mobile web] 텍스트 본문 마크업 시 분리되는 초점 문제 해결하기
    Webacc NV 2022-01-26 10:25:12

    뉴스기사와 같이 긴 본문의 내용을 읽을 때는 스크린 리더에서 제공하는 연속 읽기 기능을 사용하는 경우가 많습니다.

    그런데 TalkBack, VoiceOver에서 특정 페이지의 기사 혹은 긴 본문의 내용을 읽을 때 스타일이 분리된 여러 키워드마다 초점이 분리되어 연속으로 읽기가 불편한 경우가 있습니다.

    예를 들어 다음 문장이 있다고 가정해 보겠습니다. 

     

    오늘 아침 8시쯤 서울역에서는 특별한 행사가 있었습니다.

     

    그런데 마크업 시 접근성을 고려하지 않으면 저 문장을 하나의 초점으로 읽지 못하고 다음과 같이 초점이 여러 개로 분리될 수 있습니다.

    오늘 아침

    8시

    쯤 

    서울

    역에서는 특별한

    행사

    가 있었습니다.

     

    위와 같이 초점이 분리되면 아무리 연속 읽기로 본문을 읽는 다 하더라도 초점이 이동되는 시간 때문에 상당한 딜레이가 발생하게 됩니다.

    사실 보이스오버와 톡백이 이러한 부분을 다르게 처리하고 있어서 이 문제를 해결하기는 조금 번거롭습니다. 그러나 다음과 같이 처리하면 깔끔하게 문제가 해결됩니다.

    1. if isAndroid: 문단이 시작되는 곳에 role="paragraph" 속성을 사용합니다.

    2. if isIOS: 문단이 시작되는 곳에 role="text" 속성을 사용합니다. 

    물론 role text 속성은 최신 ARIA 1.2 스펙에서는 제외되었습니다. 그러나 현재는 이렇게 하는 것 외에는 방법이 없습니다.

    주의하실 것은 문단 내에 링크와 같이 분리된 의미를 가진 요소가 있을 때에는 절대 해당 role text 속성을 사용해서는 안 됩니다.

    만약 <p> <a href> </a> </p>와 같은 마크업에서 p 태그에 role text 속성을 추가해 버리면 보이스오버에서는 링크와 텍스트를 다 텍스트로만 처리하게 되기 때문입니다. 

    따라서 role text의 경우는 반드시 모든 요소가 다 일반 텍스트로만 이루어지는 곳에만 사용하도록 합니다.

    참고: 

    안드로이드는 <p> 태그는 하위에 인라인 스타일이 분리되더라도 초점이 나뉘어지지 않지만 아이폰은 <p> 태그를 사용하더라도 중간에 다른 스타일이 있으면 초점이 분리됩니다. 

    따라서 <p> 태그로 마크업되고 하위에 스타일이 분리된 경우에는 아이포만 대응하면 되고 <p> 태그 자체가 없이 여러 스타일로 분리된다면 해당 요소들을 포함하는 div와 같은 곳에 안드로이드 대응을 위해서 if isAndroid, if isIOS 조건문을 사용해야만 합니다.

    댓글을 작성하려면 해주세요.
  • tip
    [android native] java 및 kotlin 접근성 유틸 클래스에 setTooltipText 메서드 추가
    Webacc NV 2022-01-24 16:05:05

    접근성 구현 시 반복되는 버튼에 대해 반복된 대체 텍스트를 제공하는 것은 권장하지 않고 있습니다.

    예를 들어 한 화면에 여러 콘텐츠가 있고 각 콘텐츠마다 구독 버튼이 있다고 가정할 때 구독 버튼에 대한 대체 텍스트는 구독, 유형 정보는 Switch 혹은 ToggleButton, 상태정보는 켜짐/꺼짐 등이 될 것입니다.

    그런데 이렇게 되면 구독이라는 대체 텍스트가 한 화면에 엄청 많아질 것이고 톡백에서 컨트롤 단위로 탐색 시 스크린 리더 사용자가 듣게 되는 레이블은 구독 뿐입니다.

    이를 해결하기 위해서 접근성을 좀 더 꼼꼼히 구현해 주시는 분들은 각 구독 요소에 어떤 콘텐츠의 구독인지를 contentDescription 형태로 함께 추가해 주는 경우도 있습니다.

    그러나 모든 정보를 contentDescription에 포함하는 것보다는 부가정보는 부가정보라는 것을 알 수 있도록 별도로 넣어 주는 것이 사용성을 높일 수 있습니다.

    그래서 setAsTooltipText 메서드를 추가하게 되었습니다.

    이 메서드 사용 시 부가정보는 기본 레이블 및 유형, 상태정보 후에 툴팁 형태로 음성 출력하며 시각적으로는 툴팁 형태로도 보이지 않습니다.

    예: 켜짐, 구독 스위치, 용의 눈물.

    해당 메서드에 필요한 인자 값은 두 개입니다.

    1. view: 어떤 view에 적용을 시킬 것인지를 지정합니다.

    2. textMessage: 레이블 및 요소 유형정보, 상태정보를 다 읽은 후에 알려 주어야 할 텍스트를 넣습니다.

    사용 예시:

    val a11yUtil = AccessibilityKotlin
                a11yUtil.setTooltipText(holder.deleteButton, items[position].toString())

    유틸클래스 자바 다운로드

    유틸 클래스 코틀린 다운로드

    댓글을 작성하려면 해주세요.