홈으로

포럼 참고용 예제

로드중...

NUli 포럼 계시글 목록

[HTML-Javascript] 초점 반환, 두 줄씩만 넣어도 간편해집니다.

엔비전스 접근성 | 2024-02-05 13:07:11

브라우저에서 제공하는 UI의 확장성에 불만이 많은 웹 개발자 및 디자이너는 메뉴나 콤보박스, 대화상자를 따로 만들기 시작했고, 오늘 날에는 형형색색 다양한 UI가 존재합니다. 이로 인해 생기는 문제점은 당연히 '웹 표준'이나 '접근성'이 취약하다는 점입니다.

특히, 대화상자는 초점을 관리해주지 않아서 서비스를 이용하는 데, 큰 어려움이 있어왔고, 여전히 ~ing입니다. 접근성을 조금이라도 신경쓰는 기업 또는 개발자는 적어도, 대화상자가 나타났을 때, 초점을 대화상자 안으로 보내주는 노력이 이어지고는 있지만, 여전히 대체로 구현되지 않습니다.

평상시에는 대화상자가 열렸을 때 초점 관리에 관해 다뤘다면, 오늘은 대화상자가 닫혔을 때 초점에 관해 팁을 드리고자 합니다. "보내주기만 하면 됐지, 뭘 또 하라는거냐"라고 생각하실 수 있고, 바쁜 거 압니다. 하지만, 코드로 봤을 때, 메소드에 한 두줄씩만 더 추가해서 더 좋은 결과를 낼 수 있습니다.

대화상자를 닫았을 때, 마우스 사용자는 대화상자 밖에 있는 어떤 요소든 다시 클릭하는 데, 오랜 시간이 걸리지 않습니다. 보이는 스크롤 위치부터 보고, 포인터를 움직여서 누르기만 하면 되니까요. 

키보드 사용자는 어떨까요? 키보드 사용자는, 개발자의 설계 의도나 특별한 소프트웨어가 없다면 링크나 버튼 등을 모두 순서대로 하나씩 전부 탐색하게 됩니다. 만약에, 초점을 대화상자가 연 요소로 이동해주지 않고 대화상자만 닫힌다면 어떨까요? 문서에서 초점을 잃어버리기 때문에 처음부터 다시 탐색해야 합니다.

즉, 탐색과 조작의 연속성이 매우 떨어지게 됩니다.  이것을 "초점 반환"이라고 표현하겠습니다. 초점반환은 지난 아티클인 '조작의 연속성'과 아주 밀접한 연관이 있습니다.

지금부터 당신은 컴퓨터 사용이 서툰 사람이라고 가정해봅시다. 만약에, 회원가입 페이지가 있는데, 회원가입을 완료했더니, "가입하신걸 진심으로 환영합니다!"라는 페이지로 리디렉팅 된다고 생각해봅시다. 그런데, 리디렉팅된 페이지에 "로그인"이나 "홈으로 이동"과 같은 링크가 없다면 어떨까요?

매우 불편할 것입니다. 홈이나 로그인 url을 알고 있어서, 다시 접속해야겠지요. 컴퓨터 사용이 미숙한 분이라면 아마 매우 큰 수고가 필요할 겁니다. 이런 관점에서 이 초점 반환은 대화상자나, 메뉴, 콤보박스를 구현할 때, 반드시 필요한 과정입니다.

class CustomModalDialog extends HTMLElement {
    #returnFocus;
    set opened(v) { this.toggleAttribute('open',v); }
    get opened() { return this.hasAttribute("open"); }

    open (  ) {
        this.#returnFocus = document.activeElement;
        document.body.querySelectorAll(":scope>*:not(.modal-wrapper)").forEach(_=>{
            _.setAttribute('inert',"");
        });
        this.opened = true;
        this.focus();
    }
    close (  ) {
        this.opened = false;
        document.body.querySelectorAll(":scope>*:not(.modal-wrapper)").forEach(_=>{
            _.removeAttribute('inert',"");
        });
        this.returnFocus.focus();
    }

    set returnFocus (element) { this.#returnFocus = element; }
    get returnFocus () { return this.#returnFocus; }
    constructor() {
        super();
        this.attachShadow({mode:"open"});
        this.opened = false;
        this.role = "dialog";
        this.tabIndex = -1;
        this.ariaModal = true;
        const templateHTML = `<style>
            :host {
                top:0; left:0;
                width:100%; height:100%;
                display:flex;
                position:fixed;
                z-index:997;
                font-size:1.45rem;
            }
            :host(:not([open])) {
                display:none;
            }
            #backdrop {
                display:flex;
                width:100%;
                height:100%;
                background:rgba(0,0,0,0.5);
            }

            #body {
                display:flex;
                flex-direction:column;
                background-color:white;
                min-width:50%; max-width:80%; border-radius:0.5rem;
                aspect-ratio:16/9;
                overflow:hidden;
                margin: auto;
                padding:2rem;
            }
        </style>
        <div id="backdrop">
            <div id="body">
                <slot></slot>
            </div>
        </div>`;
        const templateElement = document.createElement("template");
        templateElement.innerHTML = templateHTML;
        this.shadowRoot.append(templateElement.content.cloneNode(true));
        
    }
    connectedCallback() {
        
        this.shadowRoot.querySelector("#backdrop").addEventListener("click",e=>{
            if(e.target == this.shadowRoot.querySelector("#backdrop")) this.close();
        });
        this.addEventListener("keydown",e=>{
            if(e.key == "Escape") this.close();
        });
        this.querySelectorAll(".btn-close").forEach(btn=>{
            btn.addEventListener("click",()=>{
                this.close();
            })
        });

        if( 
            !this.parentElement.classList.contains('modal-wrapper')
            ||
            ![...document.body.childNodes].find(_=>_=== this.parentElement)
        ) {
            console.error("[Error] modal-dialog 태그는 body>.modal-wrapper 컨테이너 안에 배치해야 합니다.")
            this.parentElement.removeChild(this);
            delete this;
        }
    }
}

customElements.define("modal-dialog",CustomModalDialog);

위 코드를 한번 보세요. 이것은 customElements API로 만든 대화상자 컴포넌트입니다. 그리 긴 코드가 아닙니다. 이 컴포넌트의 open 메소드를 보시면 this.returnElement에 document.activeElement를 저장하는 것을 볼 수 있습니다. 이 녀석이 뭐길레 저장할까요? 

현재 키보드 초점을 가리키는 속성: document.activeElement

알 사람도 알겠지만, 웹 표준 DOM의 document 객체에는 activeElement라는 녀석이 있습니다. 현재 키보드 초점이 있는 요소 위치를 감지할 수 있는 속성입니다. 대화상자가 열 때, 특정 변수나 속성에 이 activeElement를 담으면, 그 시점에 초점을 가지고 있는 요소가 담기게 됩니다.

이렇게 담겨진 요소는 나중에 close 메소드가 여러 이벤트에서 실행될 때, 초점을 다시 대화상자가 열리기 전 요소로 반환하는 용도로 사용할 수 있습니다. 특히, Windows 환경에서 이 document.activeElement는 유용합니다.

모바일은 어쩌고요?

모바일은 안타깝게도 TalkBack과 호환성이 맞지 않습니다. iOS만 놓고 보면, VoiceOver 커서는 마치 키보드의 Tab 키를 눌러서 이동하는 것처럼 링크나 버튼, 편집창에 커서가 있으면, 시스템 초점도 따라가게 됩니다. 그런데, TalkBack의 터치 커서는 시스템 커서에 관여하지 않기  때문에 한계가 있습니다.

그렇기 때문에, 일반적으로 모바일까지 고려한다면, aria-haspopup="dialog", aria-controls를 사용하여 둘을 연결하는 것을 권장합니다. 다만, 단순히 PC 페이지에서 사용될 대화상자라면, 이 document.activeElement만으로 초점을 쉽게 반환할 수 있습니다.

[HTML] 커스텀 키패드 접근성 적용 관련

엔비전스 접근성 | 2024-01-30 11:55:38

요즘 비밀번호 입력과 같은 편집창에 input을 사용하지 않고 커스텀 키패드를 사용하는 경우가 점점 더 늘어나고 있는 것 같습니다. 

스크린 리더 사용자 입장에서는 모든 접근성을 적용해 주는 input을 사용하는 것이 가장 좋지만 커스텀 키패드를 적용할 때 몇 가지 고려했으면 하는 사항들을 아래에 정리해 보았습니다.

1. 편집 가능한 커스텀 요소에 role textbox, aria-label, aria-live 속성 주기: 비밀번호, 금액 입력과 같은 값이 들어오는 요소에 아래 예시와 같이 적용할 수 있습니다.

<span id="dollarInput" role="textbox" aria-live="polite" class="input-field" aria-current="true" aria-label="달러 입력"></span>

여기서 aria-current true 속성은 편집창이 현재 활성화 되었다는 가정 하에 넣은 것입니다. 일반적으로 커스텀 키패드가 활성화 된 채로 화면이 표시되는 경우가 대부분이므로 해당 편집창에 aria-current true 속성을 주면 현재 편집창이 활성화된 상태라는 것을 바로 알 수 있습니다. 

그리고 aria-live 속성을 주면 일부 스크린 리더에서 키패드를 입력할 때마다 입력된 글자를 읽어주므로 좀 더 실시간으로 내가 입력한 밸류 값을 들을 수 있게 됩니다.

2. 입력된 값 처리: 키패드에서 숫자 등을 입력할 때마다 해당 값이 role textbox가 있는 요소에 텍스트로 포함된다면 스크린 리더는 이를 밸류로 처리하므로 추가 접근성 적용이 필요 없습니다.

<span role="textbox">325</span>

그러나 값이 동그라미로 채워지거나 이미지로 표시된다면 aria-valuetext 속성을 넣어 입력된 값을 업데이트 해 주어야 합니다.

<span role="textbox" aria-valuetext="325"></span>

비밀번호 입력시에는 *, **, **** 와 같이 값을 넣어줍니다.

3. 키패드가 표시될 때에는 키패드 표시됨 이라는 어나운스를 해줍니다. 이때 announceForAccessibility 함수를 사용할 수 있습니다.

Unlocking the Power of SQL Triggers

zain | 2024-01-04 19:23:06

I'm delving into SQL triggers, and I'm curious about their use and functionality. Could you provide an example of when you might use a trigger in a database, and how it can be beneficial? I'd appreciate a real-world scenario to better understand their practical application.
Thanks!

Smooth Scroll Animation Issue with HTML Page Links

iqratech | 2024-01-04 16:02:50

I'm creating a simple webpage with multiple sections, and I want to add smooth scrolling when users click on navigation links that lead to different sections. However, it's not working as expected. The page just jumps to the next section without any smooth animation.

Here's my code:

 

<!DOCTYPE html>
<html lang=""en"">
<head>
    <meta charset=""UTF-8"">
    <meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
    <title>Smooth Scroll Struggles</title>
    <link rel=""stylesheet"" href=""styles.css"">
</head>
<body>

    <nav>
        <ul>
            <li><a href=""#section1"">Section 1</a></li>
            <li><a href=""#section2"">Section 2</a></li>
            <li><a href=""#section3"">Section 3</a></li>
        </ul>
    </nav>

    <section id=""section1"">
        <h2>Section 1</h2>
        <p>This is the first section of my webpage.</p>
    </section>

    <section id=""section2"">
        <h2>Section 2</h2>
        <p>This is the second section of my webpage.</p>
    </section>

    <section id=""section3"">
        <h2>Section 3</h2>
        <p>This is the third section of my webpage.</p>
    </section>

    <script src=""script.js""></script>
</body>
</html>




Any idea why the smooth scrolling isn't working?

[android legacy view] 레이아웃 끼리의 초점 순서 재조정하기와 setAsContainer 메서드가 추가된 AccessibilityKotlin 업데이트

엔비전스 접근성 | 2024-01-01 16:13:19

Jetpack Compose에서는 traversalIndex 속성으로 접근성 초점 순서를 조정할 수 있지만, 레거시 뷰는 accessibilityTraversalAfter나 before 속성을 사용합니다. 레거시 뷰의 문제는 초점을 재조정하려는 객체와 해당 속성에 지정된 객체가 모두 접근성 노드에서 시맨틱해야 합니다. 따라서 LinearLayout 같은 요소들은 접근성 노드에 영향을 미치지 않아 기본적으로 초점 순서를 재조정하기 어려웠습니다.

그러나 안드로이드 14부터는 레이아웃을 접근성 노드의 컨테이너로 처리하는 메서드가 추가되어 이 문제를 해결할 수 있게 되었습니다. 예를 들어, 화면에 '위로 이동' 버튼과 웹뷰가 있을 때, 버튼을 툴바의 supportActionBar?.setDisplayHomeAsUpEnabled(true)로 구현하면, 툴바에 포함된 위로 이동 버튼은 버튼 객체로 별도로 만들 수 있는 방법이 없기 때문에 상위 레이아웃인 툴바를 접근성 컨테이너로 설정한 후 웹뷰의 accessibilityTraversalAfter를 툴바로 지정하여 초점 순서를 조정할 수 있습니다. 다만 컨테이너로 설정하기 위해서는 AccessibilityNodeInfo 객체를 사용해야 하는 번거로움이 있기 때문에 이를 간단히 구현하기 위해 AccessibilityKotlin.kt에 setAsContainer 메서드를 추가했습니다. 이 메서드는 레이아웃 뷰와 컨테이너 타이틀을 아규먼트로 받습니다.

예: AccessibilityKotlin.setAsContainer(toolbar, "toolbar").

설정된 컨테이너는 컨테이너 하위에 포함된 접근성 요소들 전체에 영향을 미치므로 따라서 A 객체 전으로 B 레이아웃 컨테이너 초점 재조정을 설정하면 B 컨테이너 하위 3개 객체 다음에 지정한 A 객체가 탐색됩니다.

다만 현재까지는 컨테이너 타이틀을 지정해도 톡백에서 이를 인식하지 못하며 앞에서도 언급했듯이 안드로이드 14 버전부터 지원합니다.

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

[android 공통] 액티비티에 접근성 화면 전환 제목을 위한 setTitle 혹은 title 구현 시 참고 사항

엔비전스 접근성 | 2023-12-22 11:38:25

여러번 말씀드렸지만 안드로이드에서는 새로운 액티비티가 실행되면 해당 액티비티의 레이블 또는 onCreate 메서드 내에서 선언된 타이틀 문자를 가지고 와서 톡백이 화면 제목으로 처리한다고 했습니다.

그리고 이러한 문자열이 없으면 앱 네임을 매 화면 제목마다 읽습니다.

그런데 이러한 타이틀 적용 시에 한 가지 고민되는 것은 화면 상단에 툴바 또는 액션바를 올려 놓고 타이틀은 커스텀으로 적용하고 싶을 때입니다.

왜냐하면 안드로이드에서 제공하는 기본 title 스타일을 디자인 측면에서 원하지 않거나 타이틀 텍스트 자체를 표시하지 않고 싶을 수 있기 때문입니다.

이 때는 기존 방식대로 타이틀은 커스텀으로 적용하되 톡백을 위한 title 또는 setTitle 을 우선 함게 작성합니다.

그리고 아래 코드를 사용해 주세요.

supportActionBar?.setDisplayShowHomeEnabled(true)

이렇게 하면 화면에서는 타이틀이 안 보이지만 톡백에서는 타이틀 문자열로 화면 제목을 읽어주게 됩니다.

[jetpack compose] isTraversalGroup, traversalIndex 속성을 활용한 접근성 초점 순서 재조정하기

엔비전스 접근성 | 2023-11-21 19:33:04

젯팩 컴포즈 1.5 버전부터 제목에서 언급한 두 API가 추가되었습니다.

해당 두 API를 활용하면 화면 레이아웃 중첩이나 기타 이슈로 인해 접근성 초점이 의도된 대로 이동하지 않을 때 초점 순서를 재조정할 수 있습니다.

isTraversalGroup true 속성은 무조건 해당 레이아웃 안의 모든 요소를 다 탐색한 다음 다음 레이아웃으로 초점이 이동하도록 강제하는 것이고 traversalIndex 는 한 레이아웃 또는 레이아웃 끼리 탐색 시 초점 순서를 재조정하는 것입니다. 

해당 API 사용 방법에 대해서는 조만간 널리 아티클로 게재할 예정이고 여기서는 해당 코드가 적용된 샘플 앱을 공유합니다.

아래 코드로 빌드를 하게 되면 두 개의 시계 레이아웃이 표시됩니다.

하나는 접근성 초점이 적용된 레이아웃이고 하나는 비적용된 레이아웃입니다.

비적용된 레이아웃의 경우 초점이 12시부터 이동하지 않고 10시부터 이동하며 초점 순서도 많이 틀어져 있습니다. 

package com.example.accessibilitydemo

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.traversalIndex
import androidx.compose.ui.unit.dp
import kotlin.math.cos
import kotlin.math.sin

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Box(modifier = Modifier.fillMaxSize()) {
                Column(modifier = Modifier.padding(16.dp)) {
                    ScreenTitle()

                    Column(modifier = Modifier.weight(1f)) {
                        // Accessible Clock Layout
                        SectionTitle("Accessible Clock Layout")
                        ClockFaceDemo(accessible = true)
                    }

                    Column(modifier = Modifier.weight(1f)) {
                        // Not Accessible Clock Layout
                        SectionTitle("Not Accessible Clock Layout")
                        ClockFaceDemo(accessible = false)
                    }
                }
            }
        }
    }

    @Composable
    fun ScreenTitle() {
        Column(modifier = Modifier
            .padding(bottom = 16.dp)
            .semantics(mergeDescendants = true) {
                // Additional semantic properties can be added here
            }
        ) {
            Text("Accessibility focus order demo", modifier = Modifier.padding(bottom = 8.dp))
            Text("Clock Layout")
        }
    }

    @Composable
    fun SectionTitle(title: String) {
        Text(
            title,
            modifier = Modifier.padding(vertical = 8.dp)
                .semantics { heading() }
        )
    }

    @Composable
    fun ClockFaceDemo(accessible: Boolean) {
        Box(
            modifier = Modifier
                .fillMaxSize()
                .semantics { isTraversalGroup = accessible },
            contentAlignment = Alignment.Center
        ) {
            CircularLayout {
                repeat(12) { hour ->
                    if (accessible) {
                        AccessibleClockText(hour)
                    } else {
                        NonAccessibleClockText(hour)
                    }
                }
            }
        }
    }

    @Composable
    private fun AccessibleClockText(value: Int) {
        Box(modifier = Modifier.semantics {
            isTraversalGroup = true
            traversalIndex = value.toFloat()
            heading()
        }) {
            Text((if (value == 0) 12 else value).toString())
        }
    }

    @Composable
    private fun NonAccessibleClockText(value: Int) {
        Box {
            Text((if (value == 0) 12 else value).toString())
        }
    }

    @Composable
    fun CircularLayout(
        modifier: Modifier = Modifier,
        radius: Int = 100, // Radius of the circle
        content: @Composable () -> Unit
    ) {
        Layout(
            content = content,
            modifier = modifier
        ) { measurables, constraints ->
            // Calculate the size of the layout
            val size = 2 * radius
            val layoutWidth = size.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth))
            val layoutHeight = size.coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))

            // Measure and place children
            val childConstraints = constraints.copy(minWidth = 0, minHeight = 0)
            val placeables = measurables.map { measurable ->
                measurable.measure(childConstraints)
            }

            layout(layoutWidth, layoutHeight) {
                val center = radius.toFloat()
                val angleStep = 2 * Math.PI / placeables.size

                placeables.forEachIndexed { index, placeable ->
                    // Subtract π/2 (90 degrees) to start from the top
                    val angle = angleStep * index - Math.PI / 2
                    val x = (center + radius * cos(angle) - placeable.width / 2).toInt()
                    val y = (center + radius * sin(angle) - placeable.height / 2).toInt()

                    placeable.placeRelative(x, y)
                }
            }
        }
    }
}

 

[android jetpack compose] 12월 7일 널리 세미나에서 발표할 드래그 데모 앱 예제 코드 선 공유

엔비전스 접근성 | 2023-11-17 16:42:49

2023년 12월 7일 널리 세미나 주제 중 안드로이드에서 커스텀 액션 접근성을 구현하는 코드를 설명할 예정입니다.

해당 세션에서 사용할 코드를 선 공유합니다.

아래 앱은 젯팩 컴포즈로 개발된 것으로 특정 과일의 순서를 변경하거나 삭제할 수 있도록 구현한 앱입니다.

해당 앱을 빌드하여 테스트 해 보시면 드래그 & 드롭, 삭제를 커스텀 액션으로 수행할 수 있으며 젯팩 컴포즈에서 특정 요소로 접근성 초점 보내기 또한 구현되어 있습니다.

발표 청취 전에 살펴 보시면 도움이 될 것 같습니다.

package com.example.dragdemo

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.paneTitle
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.roundToInt

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    FavoriteFruitArrangement()
                }
            }
        }
    }
}

@Composable
fun FavoriteFruitArrangement() {
    var fruits by remember {
        mutableStateOf(
            listOf(
                "Apple", "Orange", "Grape", "Banana", "Melon",
                "Lemon", "Mango", "Strawberry", "Pineapple", "Cherry"
            ).toMutableList()
        )
    }

    var focusAfterMove by remember { mutableStateOf<String?>(null) }

    // States for each "like" state
    var likesState by remember { mutableStateOf(List(fruits.size) { false }) }

    var accessibilityMessage by remember { mutableStateOf("") }

    fun deleteFruit(index: Int) {
        val deletedFruitName = fruits[index]  // Capture the fruit's name before deletion
        fruits = fruits.toMutableList().apply { removeAt(index) }
        likesState = likesState.toMutableList().apply { removeAt(index) }

        // Set the accessibility message
        accessibilityMessage = "$deletedFruitName has been deleted"

        // Determine which fruit to focus on after deletion
        if (fruits.isNotEmpty()) { // Ensure the list isn't empty
            focusAfterMove = if (index < fruits.size) fruits[index + 1] else fruits[index - 1]
        }
    }

    fun moveUp(index: Int): Boolean {
        if (index > 0) {
            val updatedFruits = fruits.toMutableList()
            val fruitToMove = updatedFruits.removeAt(index)
            // Get the name of the fruit above which the current fruit will be moved
            val fruitAbove = updatedFruits[index - 1]
            updatedFruits.add(index - 1, fruitToMove)
            fruits = updatedFruits

            val updatedLikesState = likesState.toMutableList()
            val likeStateToMove = updatedLikesState.removeAt(index)
            updatedLikesState.add(index - 1, likeStateToMove)
            likesState = updatedLikesState
            focusAfterMove = fruitToMove  // Set the focus to the moved fruit

            // Update the accessibility message using the saved fruitAbove
            accessibilityMessage = "$fruitToMove moved up above $fruitAbove"
            return true
        }
        return false
    }

    fun moveDown(index: Int): Boolean {
        if (index < fruits.size - 1) {
            val updatedFruits = fruits.toMutableList()
            val fruitToMove = updatedFruits.removeAt(index)
            // Get the name of the fruit below which the current fruit will be moved
            val fruitBelow = updatedFruits[index]
            updatedFruits.add(index + 1, fruitToMove)
            fruits = updatedFruits

            val updatedLikesState = likesState.toMutableList()
            val likeStateToMove = updatedLikesState.removeAt(index)
            updatedLikesState.add(index + 1, likeStateToMove)
            likesState = updatedLikesState
            focusAfterMove = fruitToMove  // Set the focus to the moved fruit

            // Update the accessibility message using the saved fruitBelow
            accessibilityMessage = "$fruitToMove moved down below $fruitBelow"
            return true
        }
        return false
    }
    Box(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            fruits.forEachIndexed { index, fruit ->
                var offsetY by remember { mutableStateOf(0f) }
                var isBeingDragged by remember { mutableStateOf(false) }
                val draggableState = rememberDraggableState { delta ->
                    offsetY += delta
                }
                LaunchedEffect(offsetY) {
                    if (!isBeingDragged) {
                        val newIndex =
                            (index + (offsetY / 100).roundToInt()).coerceIn(0, fruits.size - 1)
                        if (newIndex != index) {
                            val updatedFruits = fruits.toMutableList()
                            val draggedFruit = updatedFruits.removeAt(index)
                            updatedFruits.add(newIndex, draggedFruit)
                            fruits = updatedFruits
                            val updatedLikesState = likesState.toMutableList()
                            val draggedLikeState = updatedLikesState.removeAt(index)
                            updatedLikesState.add(newIndex, draggedLikeState)
                            likesState = updatedLikesState
                            offsetY = 0f  // Reset the offset
                        }
                    }
                }
                Box(
                    modifier = Modifier
                        .draggable(
                            orientation = Orientation.Vertical,
                            state = draggableState,
                            onDragStarted = {
                                isBeingDragged = true
                            },
                            onDragStopped = {
                                isBeingDragged = false
                            }
                        )
                        .fillMaxWidth()
                        .height(40.dp)
                        .background(Color.LightGray)
                        .semantics {
                            this.paneTitle = accessibilityMessage
                        }
                ) {
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .then(sendFocus(fruit == focusAfterMove))
                            .toggleable(
                                value = likesState[index],
                                onValueChange = { newValue ->
                                    likesState = likesState
                                        .toMutableList()
                                        .also {
                                            it[index] = newValue
                                        }
                                },
                                role = Role.Switch
                            )
                            .semantics {
                                val customActionsList = mutableListOf<CustomAccessibilityAction>()
                                if (index > 0) {  // If the fruit is not at the top, add the "Move Up" action
                                    customActionsList.add(
                                        CustomAccessibilityAction("Move Up") {
                                            moveUp(index)
                                            true
                                        }
                                    )
                                }
                                if (index < fruits.size - 1) {  // If the fruit is not at the bottom, add the "Move Down" action
                                    customActionsList.add(
                                        CustomAccessibilityAction("Move Down") {
                                            moveDown(index)
                                            true
                                        }
                                    )
                                }
                                // Add the "Delete" action
                                customActionsList.add(
                                    CustomAccessibilityAction("Delete") {
                                        deleteFruit(index)
                                        true
                                    }
                                )
                                customActions = customActionsList  // Set the list of custom actions
                            }
                    ) {
                        DragHandleIndicator(
                            modifier = Modifier.padding(
                                end = 8.dp,
                                start = 4.dp
                            )
                        )
                        Text(
                            text = fruit,
                            modifier = Modifier
                                .weight(1f)
                                .padding(8.dp)
                        )
                        // Like Representation: Icon + Switch
                        Row(
                            verticalAlignment = Alignment.CenterVertically,
                            horizontalArrangement = Arrangement.spacedBy(4.dp),
                        ) {
                            Icon(
                                imageVector = if (likesState[index]) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
                                contentDescription = "like",
                                tint = if (likesState[index]) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
                            )
                            Switch(checked = likesState[index], onCheckedChange = null)
                        }
                        Icon(
                            imageVector = Icons.Default.Delete,
                            contentDescription = "Delete",
                            modifier = Modifier
                                .clearAndSetSemantics { }
                            .clickable {
                            fruits = fruits
                                .toMutableList()
                                .apply { removeAt(index) }
                            likesState = likesState
                                .toMutableList()
                                .apply { removeAt(index) }
                        }
                            .padding(8.dp)
                        )
                    }
                }
            }

        }
        FloatingActionButton(
            onClick = {
                fruits = listOf(
                    "Apple", "Orange", "Grape", "Banana", "Melon",
                    "Lemon", "Mango", "Strawberry", "Pineapple", "Cherry"
                ).toMutableList()
                likesState = List(fruits.size) { false } // Resetting "like" states
                focusAfterMove = "Apple" // Set the focus to the first fruit
            },
            modifier = Modifier.align(Alignment.BottomEnd)
        ) {
            Icon(imageVector = Icons.Default.Refresh, contentDescription = "Reset")
        }
    }
}

@Composable
fun sendFocus(focusState: Boolean): Modifier {
    val focusRequester = remember { FocusRequester() }
    val coroutineScope = rememberCoroutineScope()

    LaunchedEffect(focusState) {
        if (focusState) {
            coroutineScope.launch {
                delay(1000) // Delay for 0.5 seconds
                focusRequester.requestFocus()
            }
        }
    }

    return Modifier
        .focusRequester(focusRequester)
        .focusable(focusState)
}

@Composable
fun DragHandleIndicator(modifier: Modifier = Modifier) {
    Canvas(
        modifier = modifier.size(24.dp, 24.dp)
    ) {
        val strokeWidth = 4f
        val startY = size.height / 3
        val endY = 2 * size.height / 3
        drawLine(
            Color.Black,
            Offset(strokeWidth / 2, startY),
            Offset(size.width - strokeWidth / 2, startY),
            strokeWidth = strokeWidth
        )
        drawLine(
            Color.Black,
            Offset(strokeWidth / 2, endY),
            Offset(size.width - strokeWidth / 2, endY),
            strokeWidth = strokeWidth
        )
    }
}

 

[android legacy view] Snackbar 구현 시 추가 접근성 구현 코드

엔비전스 접근성 | 2023-10-31 15:22:39

화면 하단에 추가 액션을 포함한 스낵바가 표시되었을 때 스낵바의 위치를 알리거나 스낵바로 접근성 초점을 보내야 하는 경우가 있습니다.

이 때는 

snackbar.addCallback(object : Snackbar.Callback() 메서드 안에서 announceForAccessibility 혹은 sendAccessibilityEvent 를 구현하면 됩니다.

다음은 예시입니다.

        val snackbar = Snackbar.make(view, message, Snackbar.LENGTH_INDEFINITE).setDuration(duration)
        snackbar.addCallback(object : Snackbar.Callback() {
            override fun onShown(sb: Snackbar?) {
                super.onShown(sb)
                if (message == "Vegetable button clicked") {
                    // Send an accessibility focus event to the Snackbar's view
                    snackbar.view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
                } else if (message == "Fruit button clicked") {
                    // Make an accessibility announcement
                    view.announceForAccessibility("Additional options at the bottom of the screen")
                }
            }
        })
        snackbar.show()
    }

 

[UIKit] UITextField 요소를 숨기면서 커스텀 텍스트필드를 구현할 때의 접근성 고려사항

엔비전스 접근성 | 2023-10-17 16:20:49

텍스트필드에서 보이스오버 사용자가 피드백을 받아야 하는 정보로는 다음과 같은 것들이 있습니다.

텍스트필드 요소 유형.

텍스트 필드가 현재 활성화 되었는지에 대한 상태 정보.

활성화 되었을 때 커서 위치.

입력 혹은 삭제 시 실시간 변경되는 글자에 대한 피드백.

 

그런데 네이티브 텍스트 필드를 숨겨 놓고 커스텀으로 텍스트필드를 구현하는 경우에는 이러한 모든 정보를 피드백 받을 수 없게 됩니다.

그런데 다행인 것은 커스텀 텍스트필드에 숨겨진 텍스트 필드의 accessibilityTraits, value, language 속성을 nameLabel.accessibilityTraits = nameTextField.accessibilityTraits 와 같이 참조하면 마치 네이티브 텍스트 필드처럼 읽어준다는 것입니다.

물론 accessibilityTraits 중에는 textField가 없지만 API가 공개되지 않았을 뿐 네이티브 요소 역시 accessibilityTraits를 가지고 있기 때문입니다.

따라서 화면이 구성되었을 때, 키보드가 표시되었을 때, 키보드가 숨겨졌을 때, 글자가 입력 혹은 삭제되었을 때 해당 밸류를 업데이트 해 주면 비교적 네이티브 텍스트필드처럼 피드백을 받을 수 있습니다. 

다만 한 가지 아쉬운 것은 키보드 입력 혹은 삭제 시 글자에 대한 피드백은 받지 못한다는 것입니다.

이를 해결하기 위해서 글자 입력 시 즉 밸류 값이 변경될 때마다 UIAccessibility.post(notification: .announcement, argument: text) 와 같이 어나운스 피드백을 해 주어야 합니다.

말할 필요도 없이 여기서의 text는 텍스트필드의 밸류 값입니다.

 

아래에 이와 관련된 UIKit 샘플 코드를 공유합니다. 스토리 보드 없는 뷰컨트롤러 클래스에서 테스트 해 보실 수 있습니다.

import UIKit


class ViewController: UIViewController, UITextFieldDelegate {
				
				private var nameLabel: UILabel!
				private var nameTextField: UITextField!
				
				override func viewDidLoad() {
								super.viewDidLoad()
								
								setupNavigationBar()
								setupTextField()
								setupNameLabel()
								
								let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleLabelTap))
								nameLabel.addGestureRecognizer(tapGesture)
								nameLabel.isUserInteractionEnabled = true
								
								NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidChange(_:)), name: UITextField.textDidChangeNotification, object: nameTextField)
				}
				
				private func setupNavigationBar() {
								navigationItem.title = "Hidden TextField example"
				}
				
				private func setupNameLabel() {
								nameLabel = UILabel(frame: CGRect(x: 20, y: 100, width: 200, height: 40))
								nameLabel.text = "Type your name"
								nameLabel.textColor = .black
								nameLabel.accessibilityLabel = "type your name"
								syncAccessibilityProperties()
								view.addSubview(nameLabel)
				}
				
				private func setupTextField() {
								nameTextField = UITextField(frame: CGRect(x: -1000, y: -1000, width: 200, height: 40))  // Position off-screen
								nameTextField.delegate = self
								nameTextField.isHidden = true
								view.addSubview(nameTextField)
				}
				
				@objc private func handleLabelTap() {
								nameTextField.becomeFirstResponder()
								syncAccessibilityProperties()
				}
				
				private func syncAccessibilityProperties() {
								nameLabel.accessibilityTraits = nameTextField.accessibilityTraits
								nameLabel.accessibilityValue = nameTextField.accessibilityValue
								nameLabel.accessibilityLanguage = nameTextField.accessibilityLanguage
				}
				
				@objc private func keyboardDidChange(_ notification: Notification) {
								if let text = nameTextField.text, !text.isEmpty {
												nameLabel.text = text
												UIAccessibility.post(notification: .announcement, argument: text)
								} else {
												nameLabel.text = "Type your name"
								}
								syncAccessibilityProperties()
				}
				
				func textFieldShouldReturn(_ textField: UITextField) -> Bool {
								nameTextField.resignFirstResponder()
								return true
				}
				
				func textFieldDidBeginEditing(_ textField: UITextField) {
								syncAccessibilityProperties()
				}
				
				func textFieldDidEndEditing(_ textField: UITextField) {
								syncAccessibilityProperties()
				}
}



 

[jetpack compose] sendFocus 모디파이어 업데이트

엔비전스 접근성 | 2023-10-03 10:18:54

지난 7월에 저는 젯팩 컴포즈에서 다른 요소로 접근성 초점을 보내기 위해 키보드 포커스를 보내는 방법에 대한 `sendFocus` 모디파이어를 소개했습니다. 이는 젯팩 컴포즈에는 접근성 초점을 다른 요소로 보내는 API가 없기 때문에 키보드 포커스를 보내는 방법을 사용해야 하기 때문입니다.

그런데 해당 모디파이어에 몇 가지 개선 사항을 추가하여 업데이트하게 되었습니다.
주요 변경사항:
1. 딜레이 추가: 포커스를 요청하기 전에 약 0.5초의 딜레이가 추가되었습니다. 이로 인해 혹시라도 초점을 받아야 하는 요소가 생성되기 전에 포커스를 보내고자 시도하여 초점이 이동하지 못하는 것을 방지할 수 있습니다.
2. 코루틴 스코프의 사용: 내부 로직에서 코루틴 스코프를 생성하여 포커스 이벤트를 더욱 유연하게 관리하도록 하였습니다.
업데이트된 `sendFocus` 모디파이어:
@Composable
fun sendFocus(focusState: Boolean): Modifier {
    val focusRequester = remember { FocusRequester() }
    val coroutineScope = rememberCoroutineScope()

    LaunchedEffect(focusState) {
        if (focusState) {
            coroutineScope.launch {
                delay(500) // Delay for 0.5 seconds
                focusRequester.requestFocus()
            }
        }
    }

    return Modifier
        .focusRequester(focusRequester)
        .focusable(focusState)
}


사용 예시:
포커스를 보내고자 하는 요소에 체인 형식으로 해당 모디파이어를 추가하고, 포커스를 보내야 하는 시점에 `true`로 반환할 변수를 파라미터에 넣어 주기만 하면 됩니다.


Text(
    text = "Hello, Compose!",
    modifier = Modifier.then(sendFocus(shouldFocus))
)

위 예시에서 보자면 shouldFocus 조건 변수가 트루로 변경되는 시점에 해당 텍스트로 포커스를 보내게 됩니다.

[react native] 탭 접근성 구현하기

엔비전스 접근성 | 2023-10-01 10:37:56

리액트 네이티브 accessibilityRole 중에는 tab이 있어서 탭과 관련된 요소 유형을 공식 지원하고 있습니다.

안드로이드에서는 해당 요소 유형이 AccessibilityNodeInfo.roleDescription 속성으로 렌더링되며 iOS에서는 대체 텍스트로 삽입됩니다.

여러 번 말씀드리지만 요소 유형이 대체 텍스트로 삽입되는 것은 보조기술의 호환성 측면에서 사용자에게 혼란을 줄 수 있기 때문에 탭 요소 유형을 구현할 때에는 다음과 같이 구현할 것을 추천드립니다.

1. accessibilityRole tab이 삽입된 요소에는 하위에 텍스트 컴포넌트가 있더라도 accessibilityLabel을 accessibilityRole이 있는 요소에 삽입해 주세요(안드로이드 호환성).

2. 플랫폼이 iOS이면 accessibilityRole을 button으로 변경합니다.

대신 상위 accessibilityRole tablist를 기준으로 각 accessibilityLabel에 탭의 현재 개수/총 개수를 함께 삽입해 주세요.

그러면 아래와 같이 읽어주게 됩니다.

안드로이드: 선택됨, 과일, 1/3 탭.

iOS: 선택됨, 과일, 1/3, 버튼.

이를 아래 예시와 같이 컴포넌트 형태로 만들어 구현하면 편리합니다.

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

const TabContext = createContext();

// Utility Functions
const gatherTextFromDescendants = (children) => {
  let texts = [];

  React.Children.forEach(children, child => {
    if (React.isValidElement(child)) {
      if (child.type === Text) {
        texts.push(child.props.children);
      } else if (child.props.children) {
        texts.push(...gatherTextFromDescendants(child.props.children));
      }
    }
  });

  return texts;
};

const Tab = ({ title, category, index, totalCount }) => {
  const { selectedCategory, setSelectedCategory } = useContext(TabContext);
  const isSelected = category === selectedCategory;

  const accessibilityRole = Platform.OS === 'ios' ? 'button' : 'tab';
  const allTextContents = gatherTextFromDescendants(<Text style={styles.tabText}>{title}</Text>);
  const accessibilityLabel = `${allTextContents.join(' ')}, ${index + 1}/${totalCount}`;

  return (
    <TouchableOpacity
      style={[styles.tab, isSelected && styles.selected]}
      onPress={() => setSelectedCategory(category)}
      accessibilityRole={accessibilityRole}
      accessibilityLabel={accessibilityLabel}
      accessibilityState={{ selected: isSelected }}
    >
      <Text style={styles.tabText}>{title}</Text>
    </TouchableOpacity>
  );
};

const TabList = ({ children }) => {
  const enhancedChildren = React.Children.map(children, (child, index) => {
    return cloneElement(child, { index, totalCount: children.length });
  });

  return <>{enhancedChildren}</>;
};

function App() {
  const [selectedCategory, setSelectedCategory] = useState('fruit');

  return (
    <TabContext.Provider value={{ selectedCategory, setSelectedCategory }}>
      <View style={styles.container} accessibilityRole="tablist">
        <TabList>
          <Tab title="Fruit" category="fruit" />
          <Tab title="Vegetable" category="vegetable" />
          <Tab title="Fish" category="fish" />
        </TabList>
      </View>
    </TabContext.Provider>
  );
}

// Styles
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  tab: {
    backgroundColor: '#ddd',
    padding: 10,
    margin: 10,
    borderRadius: 5,
  },
  tabText: {
    fontSize: 18,
  },
  selected: {
    backgroundColor: '#a5d6a7',
  },
});

export default App;

 

[스타일 고찰 시리즈] 스위치 컴포넌트의 방향

Joseph Roselli | 2023-09-26 12:30:29

아이폰이나 안드로이드 등, 우리 생활 속 오늘 날 모바일 OS 설정화면을 보면, PC에서 보지 못했던 컴포넌트를 찾게 됩니다.

바로 스위치(전환 버튼) 요소입니다. 스위치는 툭 튀어나온 둥근 단추가 어디에 있는지에 따라 상태를 나타내는 버튼입니다.

사실, 우리는 이런 형식의 인터페이스를 이미 물리적으로 접하고 있습니다. 불을 껐다 켜는 스위치라던가, 전자 기기의 설정, 전원 등을 토글하는 똑딱이 단추가 있겠습니다.

마우스에 있는 전원 스위치, 우측으로 밀면 블루투스, 왼쪽으로 밀면 2.4Ghz 무선 동글 모드, 가운대에 두면 전원을 끄는 스위치임.

이것은 제가 쓰고 있는 무선 마우스의 아랫면입니다. 자세히보면, 모바일에서 봐왔던 스위치와 비슷한 것이 보입니다. 아마도, 스위치는 이러한 모양에서 나온 것이 아닐까 싶습니다.

그런데 뭐가 문제에요?

 

  1. 방향의 문제: 
    우리는 글을 쓰거나 읽을 때 왼쪽에서 오른쪽으로 읽는 것이 당연합니다. 글자를 쓰는 체계가 왼쪽에서 오른쪽으로 쓰게끔 되어있기 때문인데요. 모든 나라가 왼쪽에서 오른쪽으로 읽는 문자체계를 갖고있진 않습니다.
  2. 상태의 기준:
    (일반적으로)스위치를 처음 보는 색맹, 색약 사용자는 오른쪽이 끔인지, 왼쪽이 끔인지 구분할 방법이 없습니다.

위 두가지 문제가 아주 대표적인 문제로, 주로 스위치 요소는 동그란 단추가 오른쪽으로 가면 켬, 왼쪽으로 가면 끔을 나타내며, 켬 상태에서는 강조 색상, 끔 상태에서는 그레이아웃시키는 것이 일반적입니다. 그런데, 스위치 요소에 익숙하지 않고, 색맹 또는 색약도 있다고 했을 때는 어떨까요? 혹은, 아랍 글자와 같이 오른쪽에서 왼쪽으로 읽는 인터페이스를 사용하는 다국어 사용자라면 어떨까요? 헷갈리지 않을까요?

그러면 어떻게 해야 해요?

위 마우스를 자세히보면, 글자의 대비가 좋진 않으나, 아랫쪽에, "단추를 여기로 옮기면 이 기능이 동작합니다"라고 아이콘이 표시된 것을 볼 수 있습니다. 이렇게, 레이블이 있으면, 방향과 관계없이, 색과 관계없이 스위치의 상태를 알 수 있게 됩니다.

스위치 애니메이션 스크린샷

위 사진에서는 스위치 단추에 전원 상태를 아이콘으로 표시하고 있습니다. O 상태이면 전원 신호 없음, - 상태이면 전원 신호 있음 아이콘입니다. 이렇게 상징적인 아이콘을 쓰거나, 혹은 단추에 "ON" 또는 "OFF"를 표시해주는 것이 훨씬 직관적인 스위치를 만들 수 있습니다.

 

[react-native] 커스텀 체크박스 접근성 적용하기

엔비전스 접근성 | 2023-09-25 11:45:49

커스텀 체크박스 구현 시 몇 가지 접근성 적용을 위한 고려 사항들을 아래에 정리하고 샘플 코드를 공유합니다.

1. 체크박스 클릭 이벤트를 가지고 있는 TouchableOpcacity 요소에 accessibilityRole & State를 주어야 하는데 accessibilityRole의 경우 안드로이드는 checkbox, iOS는 button 으로 구현합니다. 

이는 아이폰의 경우 체크박스라는 롤이 네이티브에 없기 때문에 같은 롤을 주게 되면 보이스오버에서 체크박스를 대체 텍스트로 추가하기 때문입니다.

게다가 대체 텍스트가 영어로 붙는 이슈가 있습니다.

2. 상태정보 즉 accessibilityState는 안드로이드는 checked true false, iOS는 selected true false로 구현합니다.

3. TouchableOpacity 에 accessibilityLabel 스트링을 주어서 안드로이드에서의 스크린 리더 호환성을 고려해 줍니다. 

4. 체크박스와 텍스트를 감싸는 곳에 TouchableOpacity를 주어서 초점이 하나로 합쳐질 수 있도록 합니다.

위와 같이 구현하면 커스텀 체크박스에 대한 접근성을 고려할 수 있게 됩니다.

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

코드 전체를 복사하여 빌드 및 테스트 해 보실 수 있습니다.

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

const CustomCheckbox = () => {
  const [checked, setChecked] = useState(false);

  const handleSubmit = () => {
    if (checked) {
      ToastAndroid.show('You checked!', ToastAndroid.SHORT);
    } else {
      ToastAndroid.show('You unchecked!', ToastAndroid.SHORT);
    }
  };

  const accessibilityRole = Platform.OS === 'ios' ? 'button' : 'checkbox';
  const accessibilityState = Platform.OS === 'ios' ? { selected: checked } : { checked };

  const buttonText = "Submit";

  return (
    <View style={styles.mainContainer}>
      <TouchableOpacity
        style={styles.container}
        onPress={() => setChecked(!checked)}
        accessibilityRole={accessibilityRole}
        accessibilityState={accessibilityState}
        accessibilityLabel="Agree to below conditions"
      >
        <View style={[styles.checkbox, checked ? styles.checked : styles.unchecked]}>
          {checked && <Text style={styles.checkMark}>✓</Text>}
        </View>
        <Text style={styles.text}>Agree to below conditions</Text>
      </TouchableOpacity>
      <Button 
        title={buttonText} 
        onPress={handleSubmit} 
        accessibilityLabel={buttonText}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  mainContainer: {
    flex: 1,
    justifyContent: 'center',
    padding: 20,
  },
  container: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 20,
  },
  checkbox: {
    width: 20,
    height: 20,
    borderWidth: 1,
    borderColor: 'black',
    marginRight: 10,
    justifyContent: 'center',
    alignItems: 'center',
  },
  checked: {
    backgroundColor: 'blue',
  },
  unchecked: {
    backgroundColor: 'transparent',
  },
  checkMark: {
    color: 'white',
    fontWeight: 'bold',
  },
  text: {
    fontSize: 16,
  },
});

export default CustomCheckbox;

 

[스타일 고찰 시리즈] 체크박스 스타일을 처음보는 사람도 어떻게 직관적으로 이해하게 할 수 있을까?

Joseph Roselli | 2023-09-22 12:26:28

체크박스(한국어: 확인란)는 아주 친숙한 폼 컨트롤 중 하나입니다.

검정색 테두리에 정사각형, 그안에 체크 표시를 하는 동의서 양식같은 곳에서 인터넷, 종이 문서 할 것 없이 많이 쓰이는 형식입니다.

그런데, 간혹 이런 생각이 들 때가 있습니다. 문서 서식에 익숙하지 않고, 인터넷에도 익숙하지 않은 사람은 체크박스 디자인이 달라도 요소를 "확인란"으로 인지할 수 있을까 라는 생각말입니다. 그래서 오늘은 피해야할 스타일 패턴이 무엇이 있는지 한번 정리해봅니다.

 

사용자가 이해하기 쉬운 체크박스 스타일은 무엇일까?

체크박스는 "박스"라는 표현이 있으므로, 정사각형 박스 모양 틀이 있는 것이 바람직합니다. 체크"박스"니까요. "박스" 모양은 체크박스의 상징과도 같습니다.

체크박스를 이해하기 쉽게 하기 위해서는 border는 반드시 넣습니다

체크박스 이미지, 오른쪽은 사각형으로 테두리가 있어 직관적이지만, 왼족은 그렇지 않음

"간혹 이렇게 체크 표시만 하면 되지" 하는 마인드로 만든 사이트를 볼 수 있습니다. 그러나 이는 웹 환경의 특성을 모르는 사용자로 하여금, 저 "체크 표시만 눌러야 하는구나" 라는 강박을 심습니다. 실제로는 그렇지 않더라도 말이죠. "에이 이건 너무 과도하게 신경 쓰는 것 아닌가요?"라고 할 수 있지만, 누가봐도 오른쪽이 더 직관적입니다.

체크박스와 라디오버튼을 혼동하여 디자인하지 마세요

물론, 자신의 브랜드, 사이트를 이쁘게 만들고 꾸미고 싶은 마음은 이해하지만, 라디오 버튼과 체크박스는 혼동하기 쉬운 디자인입니다.

특히, 현대에 와서는 체크박스의 모양이 사각형이 아닌 경우도 많습니다. 대표적으로 모바일 앱들을 보면 체크박스를 동그라미 안에 체크 표시를 넣어 표시하는 경우도 있습니다. 사용자 중에는 "라디오 버튼은 동그랗고, 체크박스는 네모낳다"라고 경험한 분들이 많습니다.

그런데, 간혹, 체크상자가 아닌 라디오 버튼에 이 체크표시를 넣는 경우를 볼 수 있습니다. 동그란 체크박스와 동그란 라디오 버튼에 똑같이 체크표시가 있다면, 어떻게 구분할 수 있을까요?

과일을 나열해놓은 라디오버튼 아래에 색상과 모양이 라디오버튼과 똑같은 체크상자가 있음.

margin-right 차이 뿐, 직접 조작하지 않고서 이 둘을 구분하는 것은 불가능합니다. 또한, 체크 마크는 "체크박스"에만 사용하는것이 바람직합니다. 라디오버튼에는 "작은 동그라미"를 넣어 구분해주세요.

레이블과 컨트롤 간격을 너무 떨어트려놓지 마세요

레이블 간격이 너무 넓어 보기힘든 모습

"이렇게 디자인하는 사람이 어디있느내"고 말하실 수 있지만, 아주 간혹이지만 이렇게 멀찍이 레이블과 조작 컨트롤을 떨어트려놓는 경우가 있습니다. 이럴 때, 시야가 좁은 사용자 입장에서, 눌러도 직관적으로 상태정보를 확인할 수 없고, 체크박스로 인식하는 데도 시간이 오래걸립니다.

input의 포커스 표시와 명도대비 질문 드려요

능소니 | 2023-09-21 14:22:10

안녕하세요~

 

input이 readony인 경우 키보드 탭이동이나 피씨&모바일 스크린리더 환경에서 input에 포커스가 되면 

포커스가 되었다고 디자인적(눈에 보이는 포커스 표시)으로 표시를 해주어야 접근성에 위배가 되지 않는걸까요?

 

그리고 input의 disabled 와 readony도 명도대비 3:1 또는 4.5:1을 맞춰야 하는거에요?

 

소중한 시간 내주셔서 감사합니다.

[javascript] setActiveDescendant 메서드 공유

엔비전스 접근성 | 2023-09-10 13:28:01

최근 검색어, 자동완성을 구현할 때 인풋에 텍스트가 들어오지 않고 화살표를 누를 때마다 선택된 리스트의 스타일을 업데이트 하는 경우 이에 대한 접근성을 조금 더 쉽게 구현할 수 있도록 setActiveDescendant 메서드를 만들어 공유합니다.

파라미터에는 자동완성 입력을 받는 편집창 인풋과 리스트를 가지고 있는 ul과 같은 컨테이너, 마지막으로 선택됨을 표시하는 클래스 이름입니다.

예시: 다큐먼트 및 자동완성 리스트가 로딩된 상태에서 사용자가 위 또는 아래 방향키를 누를 때 각 로직 안에   setActiveDescendant(searchInput, recentSearches, 'active');

이렇게 하면 다음이 자동 구현됩니다.

1. 지정한 클래스가 붙을 때: 해당 요소에 동적으로 아이디를 생성하고 이를 인풋에 aria-activedescendant로 추가합니다.

지정한 클래스는 role option이 있는 곳 혹은 상위 li 요소입니다.

2. 리스트 컨테이너 내부의 롤 옵션에 탭인덱스 -1이 없으면 이를 추가하여 탭키로는 해당 요소에 초점이 가지 않도록 합니다.

3. 선택한 클래스가 없으면 인풋의 aria-activedescendant 속성은 빈 값으로 표시합니다.

이 메서드를 사용하려면 다음 사항이 미리 정의되어 있어야 합니다.

1. role listbox, role option, role none(필요한 경우)이 컨테이너 및 각 리스트에 적절하게 마크업되어 있음.

2. 위 또는 아래 화살표를 누를 때마다 선택된 클래스가 변경되면서 자동완성 리스트가 선택됨.

3. 반드시 위 또는 아래 화살표 키 누를 때 setActiveDescendant 펑션이 함께 포함되도록 구현.

샘플 페이지에서 테스트하기

위 예제에서는 편집창에 포커스 하면 최근 검색어가 나타나며 특정 최근 검색어 리스트에서 딜리트 키를 누르면 삭제됩니다.

js 다운로드

[android view system] setCustomAction 메서드를 활용한 커스텀 액션 샘플 예제

엔비전스 접근성 | 2023-09-10 08:38:17

아래는 AccessibilityKotlin 유틸 클래스의 setCustomAction 메서드를 활용하여 커스텀 액션을 만든 예제입니다.

1부터 50까지의 숫자가 있고 각 숫자마다 삭제 버튼과 더보기 버튼이 있습니다.

삭제 버튼과 더보기 버튼에 커스텀 액션을 적용하였으며 따라서 삭제, 더보기는 접근성 초점에서 제거하였습니다.

필요 시 직접 빌드하여 테스트 해 보시기 바랍니다.

// value > actions.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- IDs for custom accessibility actions -->
    <item name="action_delete" type="id"/>
    <item name="action_more" type="id"/>
</resources>

 

// activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

// item_number.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp">

    <TextView
        android:id="@+id/tvNumber"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/btnDelete"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Delete" />

    <Button
        android:id="@+id/btnMore"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="More" />

</LinearLayout>

 

// MainActivity.kt

package com.example.customaction

import com.example.customaction.AccessibilityKotlin
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.customaction.databinding.ActivityMainBinding
import com.example.customaction.databinding.ItemNumberBinding

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.recyclerView.layoutManager = LinearLayoutManager(this)
        binding.recyclerView.adapter = NumberAdapter((1..50).toList(), this)
    }

    inner class NumberAdapter(private val numbers: List<Int>, private val context: MainActivity) :
        RecyclerView.Adapter<NumberAdapter.NumberViewHolder>() {

        inner class NumberViewHolder(val binding: ItemNumberBinding) : RecyclerView.ViewHolder(binding.root)

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NumberViewHolder {
            val binding = ItemNumberBinding.inflate(LayoutInflater.from(parent.context), parent, false)
            return NumberViewHolder(binding)
        }

        override fun getItemCount(): Int = numbers.size

        override fun onBindViewHolder(holder: NumberViewHolder, position: Int) {
            val currentNumber = numbers[position]
            holder.binding.tvNumber.text = currentNumber.toString()

            holder.binding.btnDelete.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
            holder.binding.btnMore.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO

            holder.binding.btnDelete.setOnClickListener {
                val updatedNumbers = numbers.toMutableList()
                updatedNumbers.removeAt(position)
                binding.recyclerView.adapter = NumberAdapter(updatedNumbers, context)
            }

            holder.binding.btnMore.setOnClickListener {
                Toast.makeText(context, "You clicked number $currentNumber more button", Toast.LENGTH_SHORT).show()
            }

            holder.binding.tvNumber.setOnClickListener {
                Toast.makeText(context, "You clicked number $currentNumber", Toast.LENGTH_SHORT).show()
            }
            AccessibilityKotlin.setCustomAction(
                holder.binding.tvNumber,
                AccessibilityKotlin.CustomAction(R.id.action_delete, "삭제") { holder.binding.btnDelete.performClick() },
                AccessibilityKotlin.CustomAction(R.id.action_more, "더 보기") { holder.binding.btnMore.performClick() }
            )


        }
    }
}

 

[android view system] AccessibilityKotlin 유틸 클래스에 setCustomAction 메서드 추가

엔비전스 접근성 | 2023-09-10 08:25:45

안드로이드 뷰 시스템에서 커스텀 액션을 쉽게 구현할 수 있도록 setCustomAction 메서드를 추가합니다.

해당 메서드를 사용하면 단 두세 줄 정도의 코드로 커스텀 액션을 구현할 수 있습니다.

예시: 

AccessibilityKotlin.setCustomAction(
    holder.binding.tvNumber,
    CustomAction(R.id.action_delete, "삭제") { holder.binding.btnDelete.performClick() },
    CustomAction(R.id.action_more, "더 보기") { holder.binding.btnMore.performClick() }
)
위 예시에서 알 수 있듯이 파라미터로는 액션이 들어가야 하는 뷰를 우선 지정하고 res > value > actions.xml 파일에서 지정해준 액션 아이디 및 표시될 액션 네임, 그리고 실행할 핸들러를 넣어 주면 됩니다.

위 예시에서는 holder.binding.tvNumber가 액션이 들어갈 뷰이고 액션 네임으로는 action_delete, action_more입니다.

즉 해당 뷰에는 두 개의 액션이 들어가게 되는 것입니다.

다음 팁에서는 해당 유틸 클래스를 이용하여 커스텀 액션을 구현한 예제 앱을 공유하도록 하겠습니다.

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

[jetpack compose] 커스텀 액션 구현하기

엔비전스 접근성 | 2023-09-08 13:41:33

컴포즈에서 커스텀 액션을 구현하기 위해서는 CustomAccessibilityAction 클래스를 다음과 같은 방법으로 사용합니다.

.semantics {
    customActions = listOf(
        CustomAccessibilityAction(deleteActionLabel) {
            onDelete()
            true
        }
    )
}
 

여러번 말씀드린 것처럼 컴포즈에서는 semantics 모디파이어를 사용해서 접근성을 구현하게 되는데 customActions 라는 속성 과 CustomAccessibilityAction 클래스를 활용해서 지정하고자 하는 액션을 구현해 주면 됩니다.

다만 커스텀 액션을 주는 이유가 비효율적인 여러 번의 초점 이동을 줄이거나 드래그와 같이 스크린 리더 사용자가 수행하기 어려운 기능을 매핑하는 것인만큼 커스텀 액션으로 대체하는 초점들은 초점이 가지 않도록 clearAndSetSemantics() 모디파이어를 지정합니다.

파라미터로는 액션 네임과 핸들러입니다.

위 예시에서는 삭제라는 액션 네임과 삭제를 실행하는 메서드가 들어가 있습니다.

아래에는 해당 커스텀 액션을 톡백에서 직접 테스트하실 수 있도록 관련 예제코드 전체를 공유합니다.

패키지만 변경하여 직접 빌드 후 테스트해 보실 수 있습니다.

// MainActivity.kt:

package com.example.customactiontest

import android.content.Context
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.ui.unit.dp
import androidx.compose.material3.*
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.customActions

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    NumberList(context = this@MainActivity)
                }
            }
        }
    }

    @Composable
    fun NumberList(context: Context) {
        var numbers by remember { mutableStateOf((1..50).toList()) }
        var announcement by remember { mutableStateOf("") }

        Column {
            LazyColumn(
                modifier = Modifier.weight(1f) // This ensures the LazyColumn occupies the maximum available space, leaving room for the reset button
            ) {
                itemsIndexed(numbers.chunked(2)) { index, pair ->
                    Row(
                        modifier = Modifier.fillMaxWidth(),
                        horizontalArrangement = Arrangement.SpaceBetween
                    ) {
                        pair.forEach { number ->
                            Box(
                                modifier = Modifier.weight(1f)
                            ) {
                                NumberItem(context, number) {
                                    numbers = numbers.filterNot { it == number }
                                    announcement = "You deleted number $number"
                                }
                            }
                        }
                    }
                }
            }

            Spacer(modifier = Modifier.height(16.dp)) // Give some space before the reset button

            Button(onClick = {
                numbers = (1..50).toList() // Reset the numbers
            }) {
                Text("Reset Numbers")
            }

            // Use announceForAccessibility composable to announce the message
            if (announcement.isNotEmpty()) {
                announceForAccessibility(announcement)
            }
        }
    }

    @Composable
    fun NumberItem(context: Context, number: Int, onDelete: () -> Unit) {
        // Get the string resource inside the composable context
        val deleteActionLabel = stringResource(id = R.string.delete_number, number)

        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
        ) {
            Text(
                text = number.toString(),
                modifier = Modifier
                    .clickable {
                        Toast.makeText(
                            context,
                            "You have selected $number",
                            Toast.LENGTH_SHORT
                        ).show()
                    }
                    .semantics {
                        customActions = listOf(
                            CustomAccessibilityAction(deleteActionLabel) {
                                onDelete()
                                true
                            }
                        )
                    }
            )
            Spacer(modifier = Modifier.width(8.dp)) // Spacer for some space between the Text and Button
            Button(onClick = onDelete, modifier = Modifier.clearAndSetSemantics { }) {
                Text("Delete")
            }
        }
    }

    @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
                }
            )
        }
    }

    @Preview(showBackground = true)
    @Composable
    fun DefaultPreview() {
        MaterialTheme {
            NumberList(context = this@MainActivity)
        }
    }
}

 

// strings.xml:

<string name="delete_number">Delete number %d</string>
 

[swiftUI] 커스텀 탭막대 accessibilityTraits 추가해주는 extension 공유

엔비전스 접근성 | 2023-08-27 13:59:45

현재 해당 팁을 작성하는 날짜를 기준으로 9월 말에 swiftUI에서 커스텀 탭을 구현할 때 보이스오버가 각 요소를 탭으로 읽어주는 동시에 탭바 영역임을 알려주도록 하는 기술에 대해 공유할 예정입니다.

해당 아티클의 연장선상에서 오늘은 커스텀 탭에 접근성 적용을 쉽게 할 수 있는 extension을 우선 팁을 통해 공유합니다.

자세한 원리에 대해서는 추후 발행될 아티클을 참고해 주시기 바랍니다.

swiftUI에는 UIKit에서 제공하는 tabbar 트레이트가 없기 때문에 ㅏ래 extension을 커스텀 탭을 구현하는 HSTACK과 같은 뷰에 다음 예시와 같이 구현하면 보이스오버가 각 요소를 탭으로 읽어주게 됩니다.

.addTabbarTrait(label: "Custom Tabs")

여기서 레이블은 탭바에 보이스오버가 접근했을 때 어떤 탭인지 읽어주도록 할 때 사용합니다. 특별한 영역 정보가 없다면 ""로 비워두면 됩니다.

다음은 extenssion 코드입니다.

// Extension
extension View {
    func addTabbarTrait(label: String) -> some View {
        self.modifier(TabBarTraitsModifier(label: label))
    }
}

struct CustomTabBarTrait<V: View>: UIViewRepresentable {
    typealias UIViewType = UIView
    var hostedView: UIHostingController<V>
    var label: String?
    
    init(_ hostedView: UIHostingController<V>, label: String? = "") {
        self.hostedView = hostedView
        self.label = label
    }
    
    func makeUIView(context: Context) -> UIViewType {
        let view = self.hostedView.view!
        self.hostedView.view.translatesAutoresizingMaskIntoConstraints = false
        view.accessibilityTraits = [.tabBar]
        view.accessibilityContainerType = .semanticGroup
        view.accessibilityLabel = "\(NSLocalizedString(label ?? "", comment: ""))"
        return view
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {}
}

struct TabBarTraitsModifier: ViewModifier {
    var label: String?
    
    @ViewBuilder
    func body(content: Content) -> some View {
        CustomTabBarTrait(UIHostingController(rootView: content), label: label)
    }
}

 

다음은 해당 extenssion을 적용한 간단한 예제 코드입니다.

과일과 채소 탭이 있으며 실제 탭은 버튼으로 구현하였고 addTabbarTrait 익스텐션을 적용한 것입니다.

import SwiftUI

enum TabSelection {
    case fruit
    case vegetables
}

struct ContentView: View {
    @State private var selectedTab: TabSelection = .fruit
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Custom Tab Example")
                .font(.largeTitle)
                .accessibilityAddTraits(.isHeader)
                .padding(.top, 20)
            
            HStack {
                Button(action: {
                    selectedTab = .fruit
                }) {
                    Text("Fruit")
                        .padding(.vertical, 10)
                        .padding(.horizontal, 20)
                        .background(selectedTab == .fruit ? Color.blue : Color.gray)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                        .accessibilityAddTraits(selectedTab == .fruit ? .isSelected : [])
                        .accessibilityRemoveTraits(selectedTab == .fruit ? [] : .isSelected)
                }
                
                Button(action: {
                    selectedTab = .vegetables
                }) {
                    Text("Vegetables")
                        .padding(.vertical, 10)
                        .padding(.horizontal, 20)
                        .background(selectedTab == .vegetables ? Color.blue : Color.gray)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                        .accessibilityAddTraits(selectedTab == .vegetables ? .isSelected : [])
                        .accessibilityRemoveTraits(selectedTab == .vegetables ? [] : .isSelected)
                }
            }
            .addTabbarTrait(label: "Custom Tabs")
            
            switch selectedTab {
            case .fruit:
                FruitView()
            case .vegetables:
                VegetablesView()
            }
            
            Spacer()
        }
        .padding()
    }
}

struct FruitView: View {
    let fruits = ["Apple", "Banana", "Cherry", "Grape", "Strawberry"]
    
    var body: some View {
        List(fruits, id: \.self) { fruit in
            Text(fruit)
        }
    }
}

struct VegetablesView: View {
    let vegetables = ["Carrot", "Broccoli", "Pepper", "Lettuce", "Spinach"]
    
    var body: some View {
        List(vegetables, id: \.self) { vegetable in
            Text(vegetable)
        }
    }
}

// Extension
extension View {
    func addTabbarTrait(label: String) -> some View {
        self.modifier(TabBarTraitsModifier(label: label))
    }
}

struct CustomTabBarTrait<V: View>: UIViewRepresentable {
    typealias UIViewType = UIView
    var hostedView: UIHostingController<V>
    var label: String?
    
    init(_ hostedView: UIHostingController<V>, label: String? = "") {
        self.hostedView = hostedView
        self.label = label
    }
    
    func makeUIView(context: Context) -> UIViewType {
        let view = self.hostedView.view!
        self.hostedView.view.translatesAutoresizingMaskIntoConstraints = false
        view.accessibilityTraits = [.tabBar]
        view.accessibilityContainerType = .semanticGroup
        view.accessibilityLabel = "\(NSLocalizedString(label ?? "", comment: ""))"
        return view
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {}
}

struct TabBarTraitsModifier: ViewModifier {
    var label: String?
    
    @ViewBuilder
    func body(content: Content) -> some View {
        CustomTabBarTrait(UIHostingController(rootView: content), label: label)
    }
}

 

[HTML-CSS] 다크모드에 맞는 기본 스타일 제공하기

엔비전스 접근성 | 2023-08-21 11:03:20

Windows 10 2016년 11월 업데이트, MacOS Mojave, Android Pi, iOS 13, 이 OS 버전은 다크모드를 처음 지원한 각 OS별 버전입니다. 위와 같이, 주요 OS에서 다크모드를 지원한 지 꽤 오래 되었습니다. 이에 맞춰서 웹 플랫폼에서도 CSS 미디어쿼리로 다크모드와 라이트모드를 확인하고 구현할 수 있는 prefers-color-scheme 미디어 속성이 추가되었습니다.

이렇게 다크모드를 지원하는 OS가 늘어났고, 오래됨에 따라 다크모드를 지원하는 웹사이트도 늘어나기 시작했습니다. 그런데, 아무런 커스터마이징이 없는 기본 폼 컨트롤을 사용할 때, 대체로 사람들이 잘 모르는 것이 있습니다 폼 컨트롤에도 기본 다크모드 스타일이 있고, 그것을 활성화할 수 있다는 것 말이지요.

CSS color-scheme 속성

CSS color-scheme 속성은 링크, 버튼, 체크박스, 라디오 버튼, 슬라이더, 텍스트 필드, 콤보박스, 스크롤 막대 등, 컨트롤 요소에 적용 시 테마에 맞는 네이티브 스타일을 제공하는 속성입니다. 차이를 보실까요?

마크업

<div class="horizontal">
    <div class="item light">
      <div>
        <label for="textfield1-light">
          TextField Light
          <input type="text" id="textfield1-light" placeholder="Light TextField">
        </label>
      </div>
      <div>
        <label for="textfield2-light">
          TextField Light
          <input type="text" id="textfield2-light" placeholder="Light TextField" value="It's Default Theme TextField">
        </label>
      </div>
      <div>
        <label for="checkbox-light1">
          <input type="checkbox" id="checkbox-light1">
          Checkbox Light
        </label>
        <label for="checkbox-light2">
          <input type="checkbox" id="checkbox-light2" checked>
          Checkbox Light (checked)
        </label>
      </div>
      <div>
        <fieldset>
          <legend>Radio Light</legend>
          <label for="radio-light1">
            <input type="radio" name="radio-light" id="radio-light1">
            Radio Light
          </label>
          <label for="radio-light2">
            <input type="radio" name="radio-light" id="radio-light2" checked>
            Radio Light Checked
          </label>
        </fieldset>
      </div>
      <div>
        <label for="select-light">
          Select Light
          <select name="select-light" id="select-light">
            <option value="0">Apple</option>
            <option value="1">Banana</option>
            <option value="2">Orange</option>
          </select>
        </label>
      </div>
      <div>
        <label for="slider-light">
          Slider light
          <input type="range" name="slider-light" id="slider-light">
        </label>
      </div>
      <div>
        <label for="color-picker-light">
          Color Picker Light
          <input type="color" value="#ff0000" name="color-picker-light" id="color-picker-light">
        </label>
      </div>
      <div>
        <button>Button Light</button> <a href="#">Link Light</a> <a href="https://www.naver.com">Link Light Visted</a>
      </div>
    </div>


    <div class="item dark">
      <div>
        <label for="textfield-dark">
          TextField Dark
          <input type="text" id="textfield-dark" placeholder="Dark TextField">
        </label>
      </div>
      <div>
        <label for="textfield2-dark">
          TextField Dark
          <input type="text" id="textfield2-dark" placeholder="Dark TextField" value="It's Dark Theme TextField">
        </label>
      </div>
      <div>
        <label for="checkbox-dark1">
          <input type="checkbox" id="checkbox-dark1">
          Checkbox Dark
        </label>
        <label for="checkbox-dark2">
          <input type="checkbox" id="checkbox-dark2" checked>
          Checkbox Dark Checked
        </label>
      </div>
      <div>
        <fieldset>
          <legend>Radio Dark</legend>
          <label for="radio-dark1">
            <input type="radio" name="radio-dark" id="radio-dark1">
            Radio Dark
          </label>
          <label for="radio-dark2">
            <input type="radio" name="radio-dark" id="radio-dark2" checked>
            Radio Dark Checked
          </label>
        </fieldset>
      </div>
      <div>
        <label for="select-dark">
          Select Dark
          <select name="select-dark" id="select-dark">
            <option value="0">Apple</option>
            <option value="1">Banana</option>
            <option value="2">Orange</option>
          </select>
        </label>
      </div>
      <div>
        <label for="slider-dark">
          Slider Dark
          <input type="range" name="slider-dark" id="slider-dark">
        </label>
      </div>
      <div>
        <label for="color-picker-dark">
          Color Picker Dark
          <input type="color" value="#ff0000" name="color-picker-dark" id="color-picker-dark">
        </label>
      </div>
      <div>
        <button>Button Dark</button> <a href="#">Link Dark</a> <a href="https://www.naver.com">Link Dark Visted</a>
      </div>
    </div>
  </div>

CSS

*{margin:0;padding: 0;box-sizing: border-box;}

.horizontal {
  display: flex; overflow: hidden; border-radius: 1em;
  width: fit-content;
  position: absolute; top:50%; left:50%;
  transform: translate(-50%,-50%);
}
.item {
  height: 250px;
  width: 450px;
  overflow: hidden;
  overflow-y: auto;
  padding: 0.75em;
}
.item > * {
  margin: 0.2em;
}

.dark {
  background-color: #252525;
  color: #efefdf;
  color-scheme: dark;
}
.light {
  background-color: #efefdf;
  color: #26211f;
  color-scheme: default;
}
fieldset {padding: 0.5em;}

 

왼쪽에 라이트모드, 오른쪽에 다크모드가 적용된 컨트롤 사진, 순서대로, placeholder가 보이는 빈 텍스트필드, 텍스트가 작성된 텍스트 필드, 체크박스, 라디오버튼, 콤보박스, 슬라이더, 컬러 픽커, 버튼과 링크가 있음.

color-scheme 속성외에 색상은 div에만 적용했고, 나머지는 레이아웃을 짜는 프로퍼티로만 구성했습니다. 보시는 것과 같이 color-scheme을 dark로 적용한 컨테이너 안에 있는 모든 요소는 다크모드에 맞는 요소 색상으로 기본 스타일이 변경된 것을 볼 수 있습니다.

이것을 어떻게 활용할 수 있을까?

별도로 이쁘게 꾸밀 이유가 없고, 네이티브 컨트롤의 모양을 그대로 쓸 때 이 color-scheme 속성을 사용할 수 있는데, html 태그에 적용하여 하위에 있는 모든 요소에 속성값이 상속되도록 세팅하면 됩니다.

밝은 테마, 어두운 테마에 따라 전달되게 하려면 아래처럼 :root 선택자 규칙에 CSS-Variable을 활용하여 미디어쿼리로 추가해주면됩니다.

:root {
  /*...(생략)*/
  --color-scheme: default;
}
@media (prefers-color-scheme:dark){
  :root {
    /*...(생략)*/
    --color-scheme: dark;
  }
}

html,body {
 /*...(생략)*/
 color-scheme:var(--color-scheme);
}

 

[android view system] isTalkBackOn 유틸 클래스를 적용한 예제 코드 공유

엔비전스 접근성 | 2023-08-20 21:25:45

바로 아래에서 공유한 코틀린에 적용된 isTalkBackOn 메서드를 적용한 간단한 앱 코드를 공유합니다.

새로운 프로젝트를 만들고 패키지만 변경한 후 코드를 그대로 복사하여 실행해볼 수 있습니다.

과일, 채소, 생선 3개의 페이지가 있으며 톡백이 켜졌을 때에는 3페이지로, 그렇지 않을 때는 무한 스크롤됩니다.

안드로이드 뷰 시스템이므로 레이아웃 파일과 액티비티 클래스 파일이 필요합니다.

따라서 아래에 각 파일별 코드를 붙여 놓겠습니다.

당연한 이야기이지만 AccessibilityKotlin 파일도 추가되어 있어야 합니다.

// activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/screenTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="ViewPager with TalkBack demo"
        android:textSize="20sp"
        android:textStyle="bold"
        android:layout_gravity="center_horizontal"
        android:layout_marginBottom="16dp"/>

    <TextView
        android:id="@+id/talkBackStatus"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textSize="18sp"
        android:layout_marginBottom="16dp"/>

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

</LinearLayout>

 

// page_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/item1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:textSize="16sp" />

    <TextView
        android:id="@+id/item2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:textSize="16sp" />

    <TextView
        android:id="@+id/item3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:textSize="16sp" />

    <TextView
        android:id="@+id/item4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:textSize="16sp" />

    <TextView
        android:id="@+id/item5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:textSize="16sp" />

</LinearLayout>

 

//MainActivity.kt

package com.example.myapplicationtalkbackdemo

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2

class MainActivity : AppCompatActivity() {

    private lateinit var viewPager: ViewPager2

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewPager = findViewById(R.id.viewPager)

        AccessibilityKotlin.isTalkBackOn(this) { isTalkBackTurnOn ->
            val talkBackStatus = findViewById<TextView>(R.id.talkBackStatus)

            if (isTalkBackTurnOn) {
                talkBackStatus.text = "You are running TalkBack"
                viewPager.adapter = FinitePagerAdapter()
            } else {
                talkBackStatus.text = "You are not running TalkBack"
                viewPager.adapter = InfinitePagerAdapter()
                viewPager.setCurrentItem(Int.MAX_VALUE / 2, false)
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        AccessibilityKotlin.removeTalkBackStateListener(this)
    }

    private val pages = listOf(
        PageData("Fruits", listOf("Apple", "Banana", "Cherry", "Grape", "Mango")),
        PageData("Vegetables", listOf("Broccoli", "Cabbage", "Carrot", "Lettuce", "Spinach")),
        PageData("Fish", listOf("Salmon", "Trout", "Mackerel", "Tuna", "Sardine"))
    )

    inner class FinitePagerAdapter : RecyclerView.Adapter<PageViewHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
            val view = LayoutInflater.from(parent.context).inflate(R.layout.page_layout, parent, false)
            return PageViewHolder(view)
        }

        override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
            holder.bind(pages[position])
        }

        override fun getItemCount(): Int = pages.size
    }

    inner class InfinitePagerAdapter : RecyclerView.Adapter<PageViewHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
            val view = LayoutInflater.from(parent.context).inflate(R.layout.page_layout, parent, false)
            return PageViewHolder(view)
        }

        override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
            holder.bind(pages[position % pages.size])
        }

        override fun getItemCount(): Int = Int.MAX_VALUE
    }

    inner class PageViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        private val item1: TextView = view.findViewById(R.id.item1)
        private val item2: TextView = view.findViewById(R.id.item2)
        private val item3: TextView = view.findViewById(R.id.item3)
        private val item4: TextView = view.findViewById(R.id.item4)
        private val item5: TextView = view.findViewById(R.id.item5)

        fun bind(page: PageData) {
            item1.text = page.items[0]
            item2.text = page.items[1]
            item3.text = page.items[2]
            item4.text = page.items[3]
            item5.text = page.items[4]
        }
    }

    data class PageData(val title: String, val items: List<String>)
}

 

[android view system] isTalkBackOn 메서드 업데이트

엔비전스 접근성 | 2023-08-20 21:04:18

오랜만에 안드로이드 유틸 클래스를 업데이트 합니다.

isTalkBackOn 메서드를 처음 공유할 당시에는 액티비티가 실행될 당시에만 톡백이 켜졌는지 꺼졌는지를 체크했었습니다.

그러나 이번에 업데이트 하는 isTalkBackOn은 중간에 톡백이 실행되거나 해제되더라도 이를 감지하여 원하는 UI를 변경하거나 기능을 변경할 수 있도록 기능을 추가했습니다.

파라미터 값으로는 context 즉 영향을 받는 액티비티(대부분 this가 될 것입니다), 조건문을 가진 콜백 함수입니다.

코틀린 예시: 

AccessibilityKotlin.isTalkBackOn(this) { isTalkBackTurnOn ->

여기서 콜백 함수는 isTalkBackTurnOn이므로 해당 함수가 true/false일 때 각각의 기능을 구현해 주기만 하면 됩니다.

자바 예시: 

AccessibilityUtil.isTalkBackOn(this, new AccessibilityUtil.TalkBackCallback() {
    @Override
    public void onResult(boolean isTalkBackOn) {
        if (isTalkBackOn) {
            // 톡백이 켜지면 실행할 코드
        } else {
            // TalkBack이 꺼지면 실행할 코드
        }
    }
});

 

 

예를 들어보겠습니다.

좌우 스크롤되는 콘텐츠를 구현할 때 ViewPager 클래스를 많이 사용합니다.

광고 페이지가 총 10개인데 옆으로 무한 스크롤되게 구현하고 싶을 수 있습니다.

그런데 그렇게 구현하면 톡백에서는 페이지 개수를 모든 스크롤되는 페이지를 다 계산하므로 2천만 페이지가 넘는 페이지가 있다고 말할 것이고 페이지가 변경될 때마다 2300만 페이지 중 350페이지와 같이 읽어줄 것입니다.

이를 해결하려면 ViewPager 어댑터를 두 개 만들어 놓고 톡백이 켜지면 실제 10페이지가 있는 어댑터를 실행하고 꺼지면 무한 스크롤되는 어댑터를 실행하도록 콜백 조건문 안에서 실행하면 되는 것입니다.

다만 해당 액티비티가 destory될 때에는 유틸 클래스에 함께 추가되어 있는 

removeTalkBackStateListener를 넣어서 불필요한 메모리 낭비를 줄여야 합니다.

해당 removeTalkBackStateListener의 파라미터는 context입니다.

 

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

자바 유틸 클래스 다운로드

[UIKit] 특정 요소로 포커스를 보내는 extension 공유

엔비전스 접근성 | 2023-08-15 16:31:35

안드로이드 뷰 시스템에서 사용하는 sendAccessibilityEvent 메서드와 마찬가지로 UIKit에서는 layoutChanged 노티피케이션을 통해 접근성 초점을 다른 곳으로 보낼 수 있습니다. 

이때도 항상 고생하는 것 중 하나가 딜레이를 적용하지 않아 초점이 가지 않는 경우가 많다는 것입니다.

레이아웃이 변경되고 내부적으로 요소들이 다시 그려지고 있는 상황에서 초점 보내기 액션이 실행되면 실패할 확률이 높기 때문에 항상 어느정도 딜레이를 주곤 합니다.

그래서 딜레이와 초점 보내는 것을 조금 더 간단한 코드로 구현할 수 있도록 sendFocusTo 라는 익스텐션을 만들어 공유하게 되었습니다.

인자 값으로는 초점을 보내고자 하는 요소만 넣어 주면 됩니다.

예시: self?.view.sendFocusTo(view: sender ?? UIView())

이렇게 하면 약 0.5초의 딜레이 후에 지정된 뷰로 초점을 보냅니다.

아래는 확장 익스텐션 코드입니다.

extension UIView {
 

    func sendFocusTo(view: UIView) {
        Task {
            // Delay the task by 500 milliseconds
            try await Task.sleep(nanoseconds: UInt64(0.5 * Double(NSEC_PER_SEC)))
            
            // Send the VoiceOver focus to the specific view
            UIAccessibility.post(notification: .layoutChanged, argument: view)
        }
    }
}

 

해당 초점 보내기 예시는 바로 아래에 올려져 있는 팁의 예제 앱을 테스트해 보시면 됩니다.

5개의 과일 버튼이 있고 각 버튼을 누르면 얼럿 창이 표시됩니다.

그런데 확인을 누르면 초점이 항상 마지막 요소로 이동하는 이슈가 있습니다.

그래서 바로 위 익스텐션을 적용해서 기존 클릭한 뷰로 초점을 보내도록 한 예제입니다.

[UIKit] 보이스오버 실행 여부 감지하는 extension 추가

엔비전스 접근성 | 2023-08-15 15:32:20

얼마 전 작성한 하나의 콜렉션뷰로 여러 페이지를 나누어 구현할 때 보이스오버 초점 해결방법 팁에서 언급한 것처럼 어쩔 수 없이 보이스오버가 실행되는지의 여부를 탐지하여 접근성을 구현해야 하는 경우가 있습니다. 

이때 흔히 isVoiceOverRunning 조건문만 오버라이드 해서 구현하면 된다고 생각할 수 있습니다.

그러나 그렇게만 구현하면 해당 화면이 열린 상태에서 보이스오버를 끄거나 반대로 보이스오버가 꺼진 채로 해당 화면을 실행하고 중간에 보이스오버를 켜는 경우 접근성을 적용받을 수 없게 됩니다.

따라서 보이스오버를 켜고 끌 때마다 이를 캐치해서 조건문에 맞도록 구현을 해 주어야 합니다.

이때 사용할 수 있는 extension이 바로 아래에서 소개할 observeVoiceOverState입니다.

인자값으로는 보이스오버 실행 true false를 체크하는 변수와 핸들러 즉 변수가 트루, 폴스일 때 실행되는 메서드입니다.

다음과 같이 사용할 수 있습니다.

        observeVoiceOverState { isRunning in
            self.updateVoiceOverStatusLabel(isRunning)
        }
extension을 적용하니 코드가 너무나도 간단해졌습니다.

여기서는 isRunning이라는 변수가 보이스오버 캐치하는 트루, 폴스 변수로 사용되었고 트루, 폴스에 따라 self.updateVoiceOverStatusLabel 핸들러, 즉 메서드가 실행되게 한 것입니다.

아래는 extension 코드입니다.

extension UIViewController {

    func observeVoiceOverState(using handler: @escaping (Bool) -> Void) {
        NotificationCenter.default.addObserver(forName: UIAccessibility.voiceOverStatusDidChangeNotification, object: nil, queue: OperationQueue.main) { _ in
            handler(UIAccessibility.isVoiceOverRunning)
        }
    }

    var isVoiceOverEnabled: Bool {
        return UIAccessibility.isVoiceOverRunning
    }
}
 

 

아래는 해당 익스텐션을 적용한 예제 앱입니다. 

스토리보드가 없는 뷰 컨트롤러를 만들고 코드 전체를 복사하여 실행해볼 수 있습니다.

화면 하단에 보이스오버 실행중, 혹은 보이스오버 실행중 아님 메시지가 보이스오버 상태에 따라 출력되는 예제입니다.

import UIKit

class ViewController: UIViewController {
    
    let fruits = ["Apple", "Banana", "Cherry", "Orange", "Kiwi"]
    var statusLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.title = "Focus Test"
        
        setupUI()
        
        observeVoiceOverStates { isRunning in
            self.updateVoiceOverStatusLabel(isRunning)
        }
    }
    
    func setupUI() {
        let buttonHeight: CGFloat = 50
        let spacing: CGFloat = 15
        
        for (index, fruit) in fruits.enumerated() {
            let button = UIButton(frame: CGRect(x: 20, y: CGFloat(index) * (buttonHeight + spacing) + 100, width: view.bounds.width - 40, height: buttonHeight))
            button.setTitle(fruit, for: .normal)
            button.backgroundColor = .blue
            button.addTarget(self, action: #selector(fruitButtonTapped(_:)), for: .touchUpInside)
            view.addSubview(button)
        }
        
        // VoiceOver status label
        let statusLabelY = CGFloat(fruits.count) * 65 + 150
        statusLabel = UILabel(frame: CGRect(x: 20, y: statusLabelY, width: view.bounds.width - 40, height: 40))
        statusLabel.textAlignment = .center
        statusLabel.font = UIFont.systemFont(ofSize: 24)
        statusLabel.textColor = .white
        statusLabel.backgroundColor = .darkGray
        statusLabel.layer.cornerRadius = 5
        statusLabel.clipsToBounds = true
        view.addSubview(statusLabel)
        updateVoiceOverStatusLabel(isVoiceOver)
    }

    @objc func fruitButtonTapped(_ sender: UIButton) {
        var message = "You selected \(sender.titleLabel?.text ?? "")"
        
        if sender.Focused() {
            message += " with VoiceOver"
        }
        
        let alert = UIAlertController(title: "Selection", message: message, preferredStyle: .alert)
        
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak self, weak sender] _ in
            self?.view.sendFocusTo(view: sender ?? UIView())
        }))
        
        self.present(alert, animated: true, completion: nil)
    }
    
    func updateVoiceOverStatusLabel(_ isRunning: Bool) {
        statusLabel.text = isRunning ? "You are running VoiceOver" : "You are not using VoiceOver"
    }
}

// Extensions
extension UIViewController {

    func observeVoiceOverState(using handler: @escaping (Bool) -> Void) {
        NotificationCenter.default.addObserver(forName: UIAccessibility.voiceOverStatusDidChangeNotification, object: nil, queue: OperationQueue.main) { _ in
            handler(UIAccessibility.isVoiceOverRunning)
        }
    }

    var isVoiceOverEnabled: Bool {
        return UIAccessibility.isVoiceOverRunning
    }
}

extension UIView {
    var Focused: Bool {
        get {
            guard let focusedElement = UIAccessibility.focusedElement(using: .notificationVoiceOver) as? UIView else {
                return false
            }
            return focusedElement == self
        }
    }

    func sendFocusTo(view: UIView) {
        Task {
            // Delay the task by 500 milliseconds
            try await Task.sleep(nanoseconds: UInt64(0.5 * Double(NSEC_PER_SEC)))
            
            // Send the VoiceOver focus to the specific view
            UIAccessibility.post(notification: .layoutChanged, argument: view)
        }
    }
}

 

[UIKit] 특정 테이블 셀을 탭으로 구현할때

엔비전스 접근성 | 2023-08-13 13:54:26

탭 콘텐츠를 구현할 때 특정 테이블 셀에 탭 개수만큼 UILabel을 만들고 각 탭을 눌러 아래 셀들의 콘텐츠가 변경되도록 구현하는 경우가 있습니다.

이런 경우 보이스오버 사용자에게 커스텀 탭을 탭으로 읽어주도록 하기 위한 구현 방법을 정리했습니다.

1. 우선 각 텍스트 레이블의 accessibilityTraits를 button으로 줍니다.

그렇지 않으면 각 탭으로 초점이 분리되지 않고 하나의 탭으로 인식하여 다른 탭으로 전환 자체가 불가능합니다.

2. 해당 텍스트들을 가진 셀에 accessibilityTraits .tabbar로 줍니다.

이렇게 하면 하위 텍스트는 버튼 트레이트를 가졌지만 상위가 탭바 트레이트 이므로 보이스오버에서 각 탭을 실제 탭처럼 요소 유형을 변경하여 읽어주게 됩니다.

3. 마지막으로 선택된 탭에 따라 selected 트레이트를 insert/remove 합니다.

 

아래는 관련 예제 코드를 만들어본 것입니다.

첫 번째 행의 셀에는 과일, 채소 탭이 있습니다.

그리고 선택된 탭에 따라 아래 셀의 콘텐츠가 표시됩니다.

스토리보드가 없는 뷰컨트롤러를 만들고 아래 코드를 넣어 테스트해볼 수 있습니다.

import UIKit

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    
    let tableView = UITableView()
    
    var items = ["Apple", "Banana", "Orange", "Pineapple", "Strawberry"]
    let fruits = ["Apple", "Banana", "Orange", "Pineapple", "Strawberry"]
    let vegetables = ["Broccoli", "Carrot", "Spinach", "Tomato", "Pepper"]
    
    var isFruitSelected = true

    override func viewDidLoad() {
        super.viewDidLoad()
        
        title = "Tab Example"
        
        setupTableView()
    }
    
    func setupTableView() {
        tableView.frame = view.bounds
        tableView.delegate = self
        tableView.dataSource = self
        view.addSubview(tableView)
        
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
    }
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return 2
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return section == 0 ? 1 : 5
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        
        // Reset the default label of the cell
        cell.textLabel?.text = nil
        
        // Clean cell for reuse
        for view in cell.contentView.subviews {
            view.removeFromSuperview()
        }
        
        if indexPath.section == 0 {
            let fruitsLabel = UILabel(frame: CGRect(x: 20, y: 10, width: 100, height: 30))
            fruitsLabel.text = "Fruits"
            fruitsLabel.textColor = isFruitSelected ? .blue : .black
            fruitsLabel.accessibilityTraits = .button
            if isFruitSelected {
                fruitsLabel.accessibilityTraits.insert(.selected)
            }
            cell.contentView.addSubview(fruitsLabel)
            
            let vegetablesLabel = UILabel(frame: CGRect(x: 140, y: 10, width: 100, height: 30))
            vegetablesLabel.text = "Vegetables"
            vegetablesLabel.textColor = isFruitSelected ? .black : .blue
            vegetablesLabel.accessibilityTraits = .button
            if !isFruitSelected {
                vegetablesLabel.accessibilityTraits.insert(.selected)
            }
            cell.contentView.addSubview(vegetablesLabel)
            
            let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.toggleSelection))
            cell.addGestureRecognizer(tapGesture)
            
            // Set the accessibility traits for this cell to indicate it's a tab bar
            cell.accessibilityTraits = .tabBar
        } else {
            cell.textLabel?.text = items[indexPath.row]
        }
        
        return cell
    }
    
    @objc func toggleSelection() {
        isFruitSelected.toggle()
        items = isFruitSelected ? fruits : vegetables
        tableView.reloadData()
    }
}


 

[UIKit] 하나의 콜렉션뷰로 여러 페이지를 나누어 구현할 때 보이스오버 초점 해결방법

엔비전스 접근성 | 2023-08-12 17:22:09

각각의 페이지마다 별도의 뷰가 표시되는 형태이면 이슈가 없으나 하나의 콜렉션뷰 내에 콘텐츠 전체를 넣어 놓고 스크롤되는 페이지에 따라 일정 양만큼만 페이지가 표시되게끔 구현하는 경우가 있습니다.

이런 경우 보이스오버 사용자가 겪게 되는 크리티컬한 이슈가 있는데 첫 번째 페이지 콘텐츠 끝에 있는 페이지 컨트롤, 즉 보이스오버에서 조절 가능이라고 읽어주는 컨트롤에서 거꾸로 돌아오면 사용자의 의도와 상관 없이 무조건 마지막 페이지로 화면이 스크롤된다는 것입니다.

마찬가지로 2페이지 이상으로 페이지가 스크롤된 상태에서 페이지 콜렉션뷰 이전 콘텐츠를 탐색하다가 페이지 콜렉션뷰셀 쪽으로 초점을 이동하는 순간 역시나 사용자의 의도와 상관 없이 무조건 1페이지로 스크롤됩니다.

이것은 위에서 설명한대로 페이지마다 보여지는 콘텐츠는 다르지만 결국에는 하나의 콜렉션뷰이기 때문입니다.

따라서 이를 해결하기 위해서는 콜렉션뷰가 스크롤되었을 때 보이스오버가 실행중이면 스크롤되는 바운더리를 현재 선택된 페이지 내부에서만 스크롤되게 하라 라는 구현을 해 주어야 합니다.

대신 이렇게 하였을 때의 단점은 세 손가락으로 좌 우 스와이프 하여 페이지를 넘길 수 없습니다.

그러나 페이지를 구현하면 페이지 컨트롤을 함께 구현하므로 페이지 컨트롤을 통해 페이지를 넘기면 그만입니다.

아래의 코드와 같이 구현합니다.

 

func scrollViewDidScroll(_ scrollView: UIScrollView) {

if UIAccessibility.isVoiceOverRunning {

scrollView.contentOffset = CGPoint(x: CGFloat(pageControl.currentPage) * scrollView.bounds.width, y: -100)

}

}

 

여기서 y를 -100으로 준 이유는 0으로 설정할 경우 상단의 첫 번째 셀 아이템이 화면에 나타나지 않을 수 있기 때문입니다. 이 부분은 위치에 따라 조정해야 할 수 있습니다.

위와 같이 적용하면 세 손가락 스크롤은 할 수 없지만 페이지 컨트롤을 통해 페이지를 조정하면서 콘텐츠를 평소와 같이 탐색할 수 있게 됩니다.

아래는 위 코드가 적용된 샘플 앱으로 과일, 채소, 생선 3페이지로 구선된 하나의 콜렉션뷰가 있습니다.

스토리보드가 없는 뷰 컨트롤러를 만들고 아래 코드를 붙여 넣어 테스트해볼 수 있습니다.

import UIKit
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
				
				let pages = [
								["Apple", "Mango", "Banana", "Orange", "Grape"],
								["Carrot", "Broccoli", "Lettuce", "Spinach", "Tomato"],
								["Salmon", "Tuna", "Mackerel", "Trout", "Catfish"]
				]
				
				var pageControl: UIPageControl!
				var collectionView: UICollectionView!
				let cellSpacing: CGFloat = 10.0
				
				override func viewDidLoad() {
								super.viewDidLoad()
								self.navigationItem.title = "Page Control Example"
								
								let layout = UICollectionViewFlowLayout()
								layout.scrollDirection = .horizontal
								layout.minimumInteritemSpacing = cellSpacing
								
								collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
								collectionView.register(ItemListCell.self, forCellWithReuseIdentifier: "cell")
								collectionView.isPagingEnabled = true
								collectionView.backgroundColor = .white
								collectionView.delegate = self
								collectionView.dataSource = self
								view.addSubview(collectionView)
								
								setupPageControl()
				}
				
				private func setupPageControl() {
								pageControl = UIPageControl(frame: CGRect(x: 0, y: view.frame.height - 50, width: view.frame.width, height: 50))
								pageControl.numberOfPages = pages.count
								pageControl.currentPage = 0
								pageControl.addTarget(self, action: #selector(pageControlChanged(_:)), for: .valueChanged)
								view.addSubview(pageControl)
				}
				@objc func pageControlChanged(_ sender: UIPageControl) {
								let offset = CGPoint(x: collectionView.bounds.width * CGFloat(sender.currentPage), y: 0)
								collectionView.setContentOffset(offset, animated: true)
				}
				func numberOfSections(in collectionView: UICollectionView) -> Int {
								return 1
				}
				func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
								return pages.count
				}
				func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
								let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ItemListCell
								cell.items = pages[indexPath.item]
								cell.innerCollectionView.reloadData()
								return cell
				}
				func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
								return CGSize(width: view.frame.width - cellSpacing, height: view.frame.height - 50)
				}
				func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
								let page = Int(round(scrollView.contentOffset.x / (scrollView.bounds.width + cellSpacing)))
								pageControl.currentPage = page
				}
				func scrollViewDidScroll(_ scrollView: UIScrollView) {
								if UIAccessibility.isVoiceOverRunning {
												scrollView.contentOffset = CGPoint(x: CGFloat(pageControl.currentPage) * scrollView.bounds.width, y: -100)
								}
				}
}
class ItemListCell: UICollectionViewCell, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
				var items: [String] = []
				
				fileprivate let innerCollectionView: UICollectionView = {
								let layout = UICollectionViewFlowLayout()
								let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
								collectionView.translatesAutoresizingMaskIntoConstraints = false
								collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "innerCell")
								return collectionView
				}()
				
				override init(frame: CGRect) {
								super.init(frame: frame)
								
								contentView.addSubview(innerCollectionView)
								
								NSLayoutConstraint.activate([
												innerCollectionView.topAnchor.constraint(equalTo: contentView.topAnchor),
												innerCollectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
												innerCollectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
												innerCollectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
								])
								
								innerCollectionView.dataSource = self
								innerCollectionView.delegate = self
								innerCollectionView.backgroundColor = .white
				}
				
				required init?(coder: NSCoder) {
								fatalError("init(coder:) has not been implemented")
				}
				
				func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
								return items.count
				}
				
				func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
								let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "innerCell", for: indexPath)
								cell.contentView.subviews.forEach { $0.removeFromSuperview() }
								
								let label = UILabel()
								label.text = items[indexPath.item]
								label.textAlignment = .center
								cell.contentView.addSubview(label)
								label.frame = cell.bounds
								return cell
				}
				
				func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
								return CGSize(width: collectionView.bounds.width, height: 50)
				}
}

 

[UIKit] VoiceOver 커서 초점이 View에 있는지 확인하는 속성 공유

엔비전스 접근성 | 2023-08-08 18:35:38

보이스오버가 실행되고 있을 때 롤링되고 있는 배너를 정지시키거나 UISlider 내의 드래그 동작 없이도 값을 조절하도록 해야 할 때 isVoiceOverRunning 메서드를 오버라이드 해서 보이스오버가 실행되고 있는 동안에만 이를 적용할 수도 있지만 보이스오버가 해당 뷰에 포커스 했을 때를 캐치하여 접근성을 적용하는 것이 더욱 안정적입니다.

그래서 UIView를 포함한 UISlider와 같은 뷰에 보이스오버가 현재 포커스를 하고 있는지 그렇지 않은지를 간단하게 체크할 수 있는 extension을 만들어 공유하게 되었습니다.

메서드 이름은 Focused이며 사용 방법은 너무나 간단합니다.

아래 extension을 추가한 다음 다음 예시와 같이 사용할 수 있습니다. 

            if !self.accessibleView.Focused() {
                self.nextSlide()

            }

아래는 extension 코드입니다.

extension UIView {
    var Focused: Bool {
        get {
            guard let focusedElement = UIAccessibility.focusedElement(using: .notificationVoiceOver) as? UIView else {
                return false
            }
            return focusedElement == self
        }
    }
}

 

아래는 해당 메서드를 적용한 배너 정지 재생하기 예시입니다. 

보이스오버가 배너 캐러셀에 포커스 되면 배너가 정지되는 예시이며 스토리보드가 없는 뷰 컨트롤러를 만들고 해당 코드를 붙여 넣으면 그대로 빌드하여 테스트해 보실 수 있습니다.

import UIKit
extension UIView {
    var Focused: Bool {
        get {
            guard let focusedElement = UIAccessibility.focusedElement(using: .notificationVoiceOver) as? UIView else {
                return false
            }
            return focusedElement == self
        }
    }
}

class ViewController: UIViewController {
				let fruits = ["Apple", "Banana", "Cherry", "Grape", "Orange"]
				var index = 0
				let label = UILabel()
				let accessibleView = AccessibleView()
				let titleLabel = UILabel()
				var timer: Timer?
				
				override func viewDidLoad() {
								super.viewDidLoad()
								
								titleLabel.text = "Carousel Test"
								titleLabel.textAlignment = .center
								titleLabel.translatesAutoresizingMaskIntoConstraints = false
								view.addSubview(titleLabel)
								
								NSLayoutConstraint.activate([
												titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
												titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor)
								])
								
								label.text = fruits[index]
								label.textAlignment = .center
								label.translatesAutoresizingMaskIntoConstraints = false
								accessibleView.addSubview(label)
								
								NSLayoutConstraint.activate([
												label.centerXAnchor.constraint(equalTo: accessibleView.centerXAnchor),
												label.centerYAnchor.constraint(equalTo: accessibleView.centerYAnchor)
								])
								
								accessibleView.isAccessibilityElement = true
								accessibleView.accessibilityLabel = "Carousel"
								accessibleView.accessibilityValue = label.text
								accessibleView.accessibilityTraits = .adjustable
								accessibleView.viewController = self
								accessibleView.translatesAutoresizingMaskIntoConstraints = false
								view.addSubview(accessibleView)
								
								NSLayoutConstraint.activate([
												accessibleView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
												accessibleView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
												accessibleView.widthAnchor.constraint(equalToConstant: 100),
												accessibleView.heightAnchor.constraint(equalToConstant: 100)
								])
								
								timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { _ in
												if !self.accessibleView.isFocused() {
																self.nextSlide()
												}
								}
				}
				
				func nextSlide() {
								index = (index + 1) % fruits.count
								label.text = fruits[index]
								accessibleView.accessibilityValue = label.text
				}
				
				func previousSlide() {
								index = (index - 1 + fruits.count) % fruits.count
								label.text = fruits[index]
								accessibleView.accessibilityValue = label.text
				}
}
class AccessibleView: UIView {
				weak var viewController: ViewController?
				
				override func accessibilityIncrement() {
								super.accessibilityIncrement()
								
								viewController?.nextSlide()
				}
				
				override func accessibilityDecrement() {
								super.accessibilityDecrement()
								
								viewController?.previousSlide()
				}
}

 

[jetpack compose] 커스텀 모달 대화상자 구현 시 clearAndSetSemantics 사용하기 및 초점

엔비전스 접근성 | 2023-08-07 06:45:08

대화상자 모달을 표시할 때 기존 콘텐츠가 백그라운드로 화면 상에서만 숨겨지는 경우 당연하게도 접근성 서비스에서 기존 콘텐츠가 접근되지 못하게 구현을 해 주어야 합니다.

이때 사용할 수 있는 메서드가 semantics 모디파이어 내의 clearAndSetSemantics 입니다.

예시: 

Column(
    modifier = Modifier.fillMaxSize()
        .then(if (showModal) Modifier.clearAndSetSemantics {} else Modifier),
즉 백그라운드 콘텐츠를 담고 있는 컨테이너가 숨겨질 때를 조건문으로 설정하여 해당 메서드를 사용하는 것입니다. 

안드로이드 뷰 시스템에서 android:importantForAccessibility="no-hide-descendants" 속성과 같다고 보시면 되니다.

또한 레이어를 닫으면 초점을 기존 레이어를 여는 버튼으로 되돌려 주어야 하는데 이때는 지난 번 소개한 아티클의 sendFocus 모디파이어를 사용할 수 있습니다.

아래는 이 두 가지 접근성을 적용한 샘플 앱입니다.

필요시 젯팩 컴포즈 프로젝트에 복사하여 테스트해볼 수 있습니다.

화면에는 5개의 과일이 있습니다.

각 과일을 누르면 간단한 과일에 대한 설명이 모달 형태로 표시되고 닫기를 누르면 기존 화면으로 돌아오는 구조입니다.

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            var showModal by remember { mutableStateOf(false) }
            var selectedFruit by remember { mutableStateOf("") }
            var focusState by remember { mutableStateOf(false) }

            Column(
                modifier = Modifier.fillMaxSize()
                    .then(if (showModal) Modifier.clearAndSetSemantics {} else Modifier),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text("Select a fruit to learn more")
                Spacer(modifier = Modifier.height(16.dp))
                FruitButton("Apple", selectedFruit == "Apple" && focusState) {
                    selectedFruit = "Apple"
                    showModal = true
                }
                FruitButton("Banana", selectedFruit == "Banana" && focusState) {
                    selectedFruit = "Banana"
                    showModal = true
                }
                FruitButton("Orange", selectedFruit == "Orange" && focusState) {
                    selectedFruit = "Orange"
                    showModal = true
                }
                FruitButton("Mango", selectedFruit == "Mango" && focusState) {
                    selectedFruit = "Mango"
                    showModal = true
                }
                FruitButton("Pineapple", selectedFruit == "Pineapple" && focusState) {
                    selectedFruit = "Pineapple"
                    showModal = true
                }
            }

            if (showModal) {
                MyCustomModal(
                    fruit = selectedFruit,
                    onClose = {
                        showModal = false
                        focusState = true
                    }
                )
            }

            val scope = rememberCoroutineScope()

            SideEffect {
                if (!showModal && focusState) {
                    scope.launch {
                        delay(500)
                        focusState = false
                    }
                }
            }
        }
    }
}

@Composable
fun FruitButton(fruit: String, focusState: Boolean, onClick: () -> Unit) {
    Button(
        onClick = onClick,
        modifier = sendFocus(focusState)
    ) {
        Text(fruit)
    }
}

@Composable
fun MyCustomModalSurface(fruit: String, onClose: () -> Unit) {
    Surface(
        shape = MaterialTheme.shapes.medium,
        modifier = Modifier.padding(16.dp)
    ) {
        Column(
            modifier = Modifier.padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(fruit)
            Text(getFruitDescription(fruit))
            Spacer(modifier = Modifier.height(16.dp))
            Button(onClick = onClose) {
                Text("Close")
            }
        }
    }
}

@Composable
fun MyCustomModal(fruit: String, onClose: () -> Unit) {
    Box(
        modifier = Modifier.fillMaxSize()
            .background(Color.Black.copy(alpha = 0.5f)),
        contentAlignment = Alignment.Center,
    ) {
        MyCustomModalSurface(fruit, onClose)
    }
}

fun getFruitDescription(fruit: String): String {
    return when (fruit) {
        "Apple" -> "An apple is a sweet, edible fruit produced by an apple tree."
        "Banana" -> "A banana is an elongated, edible fruit produced by several kinds of large herbaceous flowering plants."
        "Orange" -> "The orange is the fruit of various citrus species. It is a hybrid between pomelo and mandarin."
        "Mango" -> "A mango is a juicy stone fruit produced from numerous species of tropical trees."
        "Pineapple" -> "The pineapple is a tropical plant with an edible fruit and the most economically significant plant in the family Bromeliaceae."
        else -> ""
    }
}

@Composable
fun sendFocus(focusState: Boolean): Modifier {
    val focusRequester = remember { FocusRequester() }

    LaunchedEffect(focusState) {
        if (focusState) {
            focusRequester.requestFocus()
        }
    }

    return Modifier
        .focusRequester(focusRequester)
        .focusable(focusState)
}

 

[HTML-CSS-Javascript] 모바일에서 IR로 숨겨진 체크박스의 문제점과 해결방안

엔비전스 접근성 | 2023-07-25 12:22:20

HTML 표준 태그만으로 요소를 만들다보면 기업에서 원하는 디자인과는 거리가 먼 경우가 대부분입니다.

요소 모양을 수정할 수 있으면 가장좋겠지만 디자인을 고칠 수 있는 방법을 제공하지 않거나 크로스 브라우징 문제 등, 발목을 잡는 요소가 많습니다. 그래서, 직접 요소를 수정하는 방법보다는 커스텀 요소를 만들거나 원레 요소를 쓰되 IR기법으로 숨기는 방법이 주로 사용되고 있습니다.

그 중, 가장 간단한 방법으로 사용되는 것이 위에서 얘기한 IR기법으로 input 요소를 숨기고 label 태그에 요소 유형을 제공하는 것입니다. 레이블에 for만 잘 연결해 준다면, 마우스 사용 시 input의 상태는 잘 반영되기 때문에 스타일을 변경해야 할 때 오래전부터 쓰이던 방식입니다.

오래전부터 써오던 방식인 만큼, PC 환경만 고려했기 때문에 모바일 환경에서의 UI 사용자 경험이 떨어진다는 단점이 있습니다. IR기법 특성상 초점이 매우 작고 엉뚱한 곳에 표시되거나 아예 보이지 않는 이슈가 대표적입니다. 이는 PC 브라우저에서도 비슷한 양상을 보입니다.

또 한가지는 터치 크기와 요소 유형 이슈가 있습니다. label 태그이 for 속성으로 레이블과 체크박스를 올바르게 짝지어놓더라도 별도의 객체이기 때문에 초점이 분리되며, 겉으로 보이는 요소는 label 요소이므로, 임의로 탐색 시 상태정보와 유형정보를 못 들을 가능성이 생깁니다.

이 팁에서는 label 안에 checkbox를 숨겼을 때 생기는 이런 문제를 가장 쉽게 해결하는 CSS, 자바스크립트 기법을 설명하고자 합니다.

가급적 IR기법을 누를 수 있는 컨트롤 요소 자신에게 사용하지 말 것

IR 기법의 IR은 Image Replacement의 약자로, 눈에는 보이지만 스크린리더로 볼 수 없는 방법으로 표시된 이미지에 대체텍스트를 숨겨서 다는 기법입니다. 링크나, 버튼 내부에 IR기법을 사용하는 요소를 쓸 수는 있지만, input 태그나 button, a태그 자체를 IR로 숨기는 것은 바람직하지 않습니다. 그러면 가장 좋은 방법은 무엇일까요?

WAI-ARIA를 남용하는 것은 바람직하지 않지만, 올바르게만 사용한다면 웹접근성에 있어서 그 어떤 방법보다 강력합니다. 스마트폰  환경이 대세가 된 후, button 태그나, a태그에 텍스트 없이 아이콘만 넣는 사례가 늘었습니다. 화면 가로 폭이 좁으니, 가로로 긴 버튼보단, 가로 폭이 좁은 정사각형이나, 세로로 긴 직사각형 버튼을 선호하게 된 것이 그 이유라고 봅니다. 이럴 때 aria-label은 IR기법의 좋은 대안이 될 수 있습니다.

aria-label을 잘못썼을 때 무서운 점은 적용된 요소의 하위 요소에 어떤 텍스트가 있든, 덮어씌워버린다는 점입니다. 때문에 레이블을 제공할 때, 꼼꼼히 눈에 보이는 텍스트 중 빠진 내용이 없는지 확인할 필요가 있습니다.

또 한가지 주의할 점은, WAI-ARIA 명세에 따라 aria-label이 사용 가능한 태그나 role이 정해져 있다는 겁니다. IR기법은 설명이 복잡한 이미지(예:조직도)나, background-image에 대체텍스트를 제공할 목적으로 사용되는데, 컨트롤 요소 대부분은 aria-label을 지원하므로 링크나, 버튼, 체크박스 등은 aria-label을 활용하는 것이 아주 깔끔하고 좋은 대안이 될 수 있습니다.

2: 불가피하게 사용해야 한다면 초점 시각화를 고려할 것

1번과 겹치는 내용이나, IR기법을 컨트롤 요소에 쓰고, 디자인을 별도로 하게 되면 브라우저나 스크린리더 기본 초점이 제 역할을 할 수 없게 됩니다. 기본 요소를 숨기는 경우, 반드시 초점을 받았을 때 outline을 재정의해줘야만 합니다. 방법은 아래와 같습니다.

label:has(input[type=checkbox].blind):focus-within {
  outline:auto;
}

초점이 overflow에 의해 잘리는 경우는 요소가 크다면, outline-offset을 음수값으로 지정해서 해결할 수 있습니다.

3: 모바일 페이지라면 label에 role 사용과 스크립트 적용을 고려해볼 것

모바일, 특히 iOS에서는 초점 분리 이슈가 매우 많이 발생합니다. 또한, 초점 시각화 문제와 터치 크기 이슈도 같이 발생하게 되는데, 그럴 때, 간단하게 체크박스의 터치 크기를 키우고, 초점을 하나로 합치는 기법이 있습니다. IR로 숨긴 체크박스를 display:none으로 숨기고, label에 상태정보, 유형, 초점을 제공하는 것입니다.

체크박스를 display:none으로 숨기는 것은 기본적으로 안 되지만, label을 WAI-ARIA 체크박스로 만든다면 그 문제가 말끔히 해결됩니다. 물론 label과 input이 for 속성으로 잘 연결되어 있다는 것을 조건으로 합니다. 또한, 레이블에 도움말 같은 컨트롤이 포함되어있지 않아야 합니다.

<label for="chk-1">
    <input type="checkbox" id="chk-1">
    <div class="checkmark material-icons" aria-hidden="true">check</div>
    Give me disposable forks, spoons, and chopsticks
  </label>
  <label>
    <input type="checkbox">
    <div class="checkmark material-icons" aria-hidden="true">check</div>
    Give me disposable forks, spoons, and chopsticks
  </label>
  <script>

    function generateUUID() {
      let d = new Date().getTime();

      if (window.performance && typeof window.performance.now === "function") {
        d += performance.now(); // 추가적인 고유성을 위해 performance.now() 사용
      }

      const uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
        const r = (d + Math.random() * 16) % 16 | 0;
        d = Math.floor(d / 16);
        return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16);
      });

      return uuid;
    }

    const checkboxes = document.querySelectorAll('label:has(input[type="checkbox"])');
    for(const checkbox of checkboxes){
      const nativeCheckbox = checkbox.querySelector('input[type="checkbox"]');
      const generatedID = generateUUID();
      if(!checkbox.id) {
        checkbox.id = `customCheckbox-${generatedID}`;
      }
      if(!nativeCheckbox.id) {
        nativeCheckbox.id = `nativeCheckbox-${generatedID}`;
      }
      if(nativeCheckbox.id && !checkbox.htmlFor) {
        checkbox.htmlFor = nativeCheckbox.id;
      }

      checkbox.setAttribute('role','checkbox');
      checkbox.setAttribute('aria-labelledby',checkbox.id);

      const updateAriaChecked=()=>{
        checkbox.setAttribute('aria-checked',nativeCheckbox.checked);
      }
      const updateAriaDisabled=()=>{
        checkbox.setAttribute('aria-disabled',nativeCheckbox.disabled);
        checkbox.tabIndex = nativeCheckbox.disabled === "true" ? -1 : 0;
      }
      updateAriaDisabled();
      updateAriaChecked();
      new MutationObserver((records)=>records.forEach(record=>{
        updateAriaDisabled();
      })).observe(nativeCheckbox,{attributes:true,attributeFilter:["disabled"]})
      
      // events
      checkbox.addEventListener('keydown',(evt)=>{if ( evt.key == " " ) {evt.preventDefault();nativeCheckbox.click();}})
      nativeCheckbox.addEventListener("change",(evt)=>{checkbox.setAttribute('aria-checked',nativeCheckbox.checked);});
    }
  </script>

display가 none이여도 label-for로 잘 연결만 되어있다면 레이블을 눌렀을 때, 눈에는 보이지 않고 스크린리더로 들을 수는 없지만, 실제로는 체크박스가 눌리게 됩니다. 레이블 안에 체크박스만 있는 경우, 이렇게 change 이벤트와 keydown 이벤트만으로 label와 input[type=checkbox] 태그를 하나의 checkbox로 만들 수 있습니다.

이 스크립트는 레이블과 체크박스 사이에 도움말 버튼 등이 있는 사례에는 적합하지 않으나, 충분히 이런 방식으로 응용하여 기존에 IR기법으로 숨겨놓은 체크박스의 모바일 UX를 향상시킬 수 있습니다.

[jetpack compose] stateDescription 사용하여 직관적인 상태정보 제공해주기

엔비전스 접근성 | 2023-07-23 10:40:48

안드로이드 뷰 시스템을 다룰 때 언급한 적이 있지만 읽지 않음, 읽음과 같은 조금 더 직관적인 상태정보를 제공해 주기 위해서는 stateDescription을 사용할 수 있습니다.

젯팩 컴포즈에서는 roleDescription은 지원하지 않지만 stateDescription은 지원하기에 아래에 해당 예제를 만들어 보았습니다.

구현 방법은 간단합니다.

해당 상태정보가 필요한 요소에 semantics 모디파이어를 주고 그 안에서 읽음, 읽지 않음과 같은 상태정보를 제공해 주기만 하면 됩니다.

.semantics { stateDescription = if (unread) "Unread" else "Read" },

아래는 이와 관련된 샘플 앱입니다.

1번부터 5번까지의 알림이 있으며 기본은 읽지 않음 상태이고 한번 누르면 읽음 상태로 변경됩니다.

그리고 초기화 버튼을 누르면 다시 읽지 않음 상태로 변경됩니다.

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.unit.dp
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val context = LocalContext.current
            var notifications = remember { mutableStateListOf(true, true, true, true, true) }
            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.SpaceBetween,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                notifications.forEachIndexed { index, unread ->
                    Notification(
                        text = "Notification ${index + 1}",
                        unread = unread,
                        onClick = {
                            notifications[index] = false
                            Toast.makeText(context, "You read notification ${index + 1}", Toast.LENGTH_SHORT).show()
                        }
                    )
                }
                Button(onClick = { notifications.fill(true) }) {
                    Text("Reset")
                }
            }
        }
    }

    @Composable
    fun Notification(text: String, unread: Boolean, onClick: () -> Unit) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
                .clickable(onClick = onClick)
                .semantics { stateDescription = if (unread) "Unread" else "Read" },
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Text(text)
            if (unread) {
                Box(
                    modifier = Modifier
                        .size(10.dp)
                        .background(MaterialTheme.colorScheme.secondary)
                )
            } else {
                Box(
                    modifier = Modifier
                        .size(10.dp)
                        .background(Color.Gray)
                )
            }
        }
    }
}

 

[jetpack compose] 커스텀 라디오버튼, 탭 등 구현 시 몇 개 중 몇 개 읽어주게 하기

엔비전스 접근성 | 2023-07-22 18:43:24

네이티브 라디오버튼이나 탭의 경우 현재 포커스 하고 있는 요소가 총 개수 중 몇 번째인지를 톡백에서 자동으로 읽어줍니다.

그런데 semantics 모디파이어를 통해서 라디오버튼, 탭 구현 시에도 톡백에서 이를 읽어줄 수 있도록 할 수 있습니다.

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

커스텀 라디오버튼이나 탭을 감싸고 있는 Row와 같은 컨테이너에 selectableGroup semantics 모디파이어를 추가해 주기만 하면 됩니다.

예시:

Row(
    modifier = Modifier.semantics { selectableGroup()
    contentDescription = "tab control"}아래에 이와 관련된 샘플 앱을 공유합니다. 

패키지를 추가한 다음 빌드하여 톡백에서 바로 테스트해 보실 수 있습니다.

화면은 간단합니다.

커스텀 과일, 채소 탭이 있고 선택된 탭에 따라 콘텐츠가 변경됩니다.

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.selectableGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            var selectedTab by remember { mutableStateOf("Fruit") }
            val fruits = listOf("Apple", "Banana", "Orange", "Strawberry", "Grapes")
            val vegetables = listOf("Carrot", "Broccoli", "Cauliflower", "Spinach", "Peas")

            MaterialTheme {
                Column(
                    modifier = Modifier.fillMaxSize(),
                    horizontalAlignment = Alignment.CenterHorizontally,
                    verticalArrangement = Arrangement.Center
                ) {
                    Row(
                        modifier = Modifier.semantics { selectableGroup()
                        contentDescription = "tab control"}
                    ) {
                        Button(
                            onClick = { selectedTab = "Fruit" },
                            colors = if (selectedTab == "Fruit") ButtonDefaults.buttonColors() else ButtonDefaults.outlinedButtonColors(),
                            modifier = Modifier.semantics {
                                role = Role.Tab
                                this.selected = (selectedTab == "Fruit")
                            }
                        ) {
                            Text("Fruit")
                        }
                        Spacer(modifier = Modifier.width(8.dp))
                        Button(
                            onClick = { selectedTab = "Vegetable" },
                            colors = if (selectedTab == "Vegetable") ButtonDefaults.buttonColors() else ButtonDefaults.outlinedButtonColors(),
                            modifier = Modifier.semantics {
                                role = Role.Tab
                                this.selected = (selectedTab == "Vegetable")
                            }
                        ) {
                            Text("Vegetable")
                        }
                    }
                    Spacer(modifier = Modifier.height(16.dp))
                    when (selectedTab) {
                        "Fruit" -> fruits.forEach { Text(it) }
                        "Vegetable" -> vegetables.forEach { Text(it) }
                    }
                }
            }
        }
    }
}

 

[jetpack compose] 커스텀 체크박스 또는 스위치 구현

엔비전스 접근성 | 2023-07-22 17:46:54

네이티브가 아닌 커스텀 체크박스나 스위치를 구현할 때에는 semantics 모디파이어 안에서 role = Role.Checkbox 또는 role = Role.Switch를 사용하여 요소 유형을 줄 수 있습니다.

그리고 상태정보는 선택과 해제를 할 수 있는 토글 속성을 가지므로 semantics가 아닌 .toggleable 모디파이어를 사용하면 됩니다.

해당 toggleable 모디파이어 안에서 체크 해제 혹은 체크됨 조건 변수를 다음 예시와 같이 대입해줍니다.

.toggleable(
    value = vegetableChecked,
    onValueChange = { vegetableChecked = it }
)
이렇게 하면 지정된 밸류 값의 true, false 상태정보에 따라서 체크됨, 체크 안 됨 또는 켜짐, 꺼짐 상태정보를 출력하게 됩니다.

아래는 커스텀 체크박스와 스위치 적용 예제입니다.

화면에는 Box 위젯을 사용하여 구현된 과일 체크박스와 채소 스위치가 있습니다.

톡백으로 해당 요소들을 빌드하여 테스트 해 보실 수 있습니다.

 

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import android.widget.Toast
import androidx.compose.foundation.selection.toggleable
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            var fruitChecked by remember { mutableStateOf(false) }
            var vegetableChecked by remember { mutableStateOf(false) }

            MaterialTheme {
                Column(
                    modifier = Modifier.fillMaxSize(),
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {
                    Box(
                        modifier = Modifier
                            .semantics { role = Role.Checkbox }
                            .toggleable(
                                value = fruitChecked,
                                onValueChange = { fruitChecked = it }
                            )
                    ) {
                        Text("Fruit", style = if (fruitChecked) MaterialTheme.typography.bodyMedium else MaterialTheme.typography.bodySmall)
                    }
                    Box(
                        modifier = Modifier
                            .semantics { role = Role.Switch }
                            .toggleable(
                                value = vegetableChecked,
                                onValueChange = { vegetableChecked = it }
                            )
                    ) {
                        Text("Vegetable", style = if (vegetableChecked) MaterialTheme.typography.bodyMedium else MaterialTheme.typography.bodySmall)
                    }
                    Button(onClick = {
                        val message = when {
                            fruitChecked && vegetableChecked -> "You checked both fruit and vegetable"
                            fruitChecked -> "You only checked fruit"
                            vegetableChecked -> "You only checked vegetable"
                            else -> "You did not check both"
                        }
                        Toast.makeText(this@MainActivity, message, Toast.LENGTH_SHORT).show()
                    }) {
                        Text("Submit")
                    }
                }
            }
        }
    }
}

 

[jetpack compose] 확장 축소 상태정보 제공해주기

엔비전스 접근성 | 2023-07-22 12:06:09

안드로이드 뷰 시스템에서는 저희가 제공한 라이브러리 메서드 중 expandCollapseButton을 통해서 접힘, 펼쳐짐 상태 정보를 톡백에서 읽어줄 수 있도록 구현하는 방법을 공유했었습니다.

컴포즈에서는 semantics 속성 중 expand, collapse action을 통해서 톡백에게 접힘, 펼쳐짐 상태 정보를 전달할 수 있습니다.

인자값은 사용자가 해당 액션을 수행했을 때의 동작 및 리턴 트루 값입니다.

즉 확장되었을 때에는 축소되는 액션 동작을, 축소되었을 때에는 확장하는 액션 동작을 추가해 주면 되는 것입니다.

왜냐하면 접힘 펼쳐짐 상태정보는 단순한 상태값 속성으로 들어가지 않고 접근성 액션 형태로 추가되기 때문에 해당 액션을 사용자가 수행할 때의 동작을 정의해 주어야 하기 때문입니다.

그래서 다음과 같이 적용할 수 있습니다.

if (expandedFruits) {
    this.collapse(action = { expandedFruits = false; true })
} else {
    this.expand(action = { expandedFruits = true; true })
}위와 같이 적용하면 스크린 리더 사용자는 두 번 탭을 해서 확장 또는 축소할 수도 있고 접근성 개액션이 추가되었으므로 톡백에서 추가된 액션 리스트를 열어서도 확장 축소를 실행할 수 있게 됩니
다.
해당 액션 동작은 심지어 웹에서의 aria-expanded true false 적용 시에도 동일하게 적용되지만 웹에서는 클릭 액션이 확장, 축소 액션으로 자동 적용됩니다.

아래는 젯팩 컴포즈에서 확장 축소가 구현된 전체 코드 예제입니다.

패키지를 제외한 전체 코드를 복사하여 직접 실행해볼 수 있습니다.

본 코드에서는 과일, 채소 버튼이 있어 확장 축소가 가능하게 되어 있으며 제출 버튼을 누르면 두 버튼 중 어떤 버튼이 확장되었는지를 토스트 메시지로 알려주게끔 구현되어 있습니다.

import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.collapse
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.expand
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                ExpandableList()
            }
        }
    }
}

@Composable
fun ExpandableList() {
    val fruits = listOf("Apple", "Banana", "Mango", "Pineapple", "Strawberry")
    val vegetables = listOf("Carrot", "Broccoli", "Spinach", "Cucumber", "Tomato")
    var expandedFruits by remember { mutableStateOf(false) }
    var expandedVegetables by remember { mutableStateOf(false) }
    val context = LocalContext.current

    Column {
        Text(
            text = "Fruits",
            modifier = Modifier
                .fillMaxWidth()
                .clickable { expandedFruits = !expandedFruits }
                .semantics {
                    role = Role.Button
                    if (expandedFruits) {
                        this.collapse(action = { expandedFruits = false; true })
                    } else {
                        this.expand(action = { expandedFruits = true; true })
                    }
                },
            textAlign = TextAlign.Center
        )
        if (expandedFruits) {
            fruits.forEach { fruit ->
                Text(text = fruit, modifier = Modifier.padding(start = 24.dp))
            }
        }
        Spacer(modifier = Modifier.height(16.dp))
        Text(
            text = "Vegetables",
            modifier = Modifier
                .fillMaxWidth()
                .clickable { expandedVegetables = !expandedVegetables }
                .semantics {
                    role = Role.Button
                    if (expandedVegetables) {
                        this.collapse(action = { expandedVegetables = false; true })
                    } else {
                        this.expand(action = { expandedVegetables = true; true })
                    }
                },
            textAlign = TextAlign.Center
        )
        if (expandedVegetables) {
            vegetables.forEach { vegetable ->
                Text(text = vegetable, modifier = Modifier.padding(start = 24.dp))
            }
        }

        Spacer(modifier = Modifier.height(16.dp))
        Box(
            modifier = Modifier.fillMaxWidth().clickable {
                val message =
                    when {
                        expandedFruits && expandedVegetables -> "Both fruits and vegetables are expanded"
                        expandedFruits -> "Only fruits are expanded"
                        expandedVegetables -> "Only vegetables are expanded"
                        else -> "Neither fruits nor vegetables are expanded"
                    }
                Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
            }.semantics { role = Role.Button },
        ) {
            Text(
                text = "Submit",
                textAlign = TextAlign.Center,
                modifier = Modifier.fillMaxWidth(),
            )
        }
    }
}

 

[jetpack compose] selected semantics modifier 특징 정리

엔비전스 접근성 | 2023-07-15 17:55:49

접근성 정보를 추가할 때 사용하는 semantics modifier 속성 중 selected 속성에 대해 아래와 같이 정리합니다.

1. 선택됨 상태 정보를 접근성 노드에 전달해야 할 때 사용할 수 있습니다. 

2. true 상태일 때에는 힌트 메시지를 전달하지 않으며 false 일 때에는 전환하려면 또는 활성화 하려면 두 번 탭하라는 힌트 메시지를 전달합니다.

selected 객체를 가진 요소의 accessibility role 즉 요소 유형이 없는 경우 및 라디오버튼은 전환하려면 두 번 탭하세요, 요소 유형이 탭일 때에는 활성화 하려면 두 번 탭하라는 힌트 메시지를 출력합니다.

3. 요소 유형이 없거나 라디오버튼일 경우에는 선택안됨 상태 정보를 함께 출력합니다. 그러나 요소 유형이 탭일 때에는 선택안됨 정보를 출력하지 않습니다.

 

아래에 참고하실 수 있도록 이와 관련된 드랍다운 예제를 만들어 보았습니다.

1. 과일, 채소 리스트 드랍다운을 누르면 과일 혹은 채소 리스트가 나타납니다.

드랍다운 메뉴의 경우 드랍다운 요소 유형이 없기 때문에 onClickLabel 속성을 통해 힌트 메시지를 추가하였습니다.

2. semantics modifier 내에서 특정 리스트를 누르면 해당 리스트가 선택됨 상태로 변경되는데 이를 접근성 노드에 알려주기 위해서 selected 속성을 사용하였습니다. 

3. 또한 여러 요소 중 하나만 선택되므로 요소 유형은 라디오버튼을 사용하였습니다.

예를 들어 과일 리스트를 누르면 사과, 바나나와 같은 요소를 라디오버튼으로 읽어주며 선택안됨, 선택됨 상태 정보를 함께 출력합니다.

아래 코드를 복사하여 상단의 package 부분만 추가하면 톡백으로 바로 테스트 하실 수 있습니다.

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.selectableGroup
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Column(
                    modifier = Modifier.fillMaxSize(),
                    horizontalAlignment = Alignment.CenterHorizontally,
                    verticalArrangement = Arrangement.Center
                ) {
                    // Fruit dropdown list
                    var fruitExpanded by remember { mutableStateOf(false) }
                    var selectedFruit by remember { mutableStateOf("Fruit") }

                    Text(
                        text = selectedFruit,
                        modifier = Modifier.clickable(
                            onClick = { fruitExpanded = true },
                            onClickLabel = "Open fruit dropdown menu"
                        )
                    )
                    DropdownMenu(
                        expanded = fruitExpanded,
                        onDismissRequest = { fruitExpanded = false }
                    ) {
                        listOf("Apple", "Banana", "Orange", "Grapes", "Mango").forEach { fruit ->
                            DropdownMenuItem(
                                text = {
                                    Text(
                                        text = fruit,
                                        style = if (fruit == selectedFruit) MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.primary) else MaterialTheme.typography.bodyLarge
                                    )
                                },
                                onClick = {
                                    selectedFruit = fruit
                                    fruitExpanded = false
                                },
                                modifier = Modifier.semantics {
                                    this.selected = (fruit == selectedFruit)
                                    this.role = Role.RadioButton
                                }
                            )
                        }
                    }

                    // Vegetable dropdown list
                    var vegetableExpanded by remember { mutableStateOf(false) }
                    var selectedVegetable by remember { mutableStateOf("Vegetable") }

                    Text(
                        text = selectedVegetable,
                        modifier = Modifier.clickable(
                            onClick = { vegetableExpanded = true },
                            onClickLabel = "Open vegetable dropdown menu"
                        )
                    )
                    DropdownMenu(
                        expanded = vegetableExpanded,
                        onDismissRequest = { vegetableExpanded = false }
                    ) {
                        listOf("Carrot", "Broccoli", "Spinach", "Peas", "Corn").forEach { vegetable ->
                            DropdownMenuItem(
                                text = {
                                    Text(
                                        text = vegetable,
                                        style = if (vegetable == selectedVegetable) MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.primary) else MaterialTheme.typography.bodyLarge
                                    )
                                },
                                onClick = {
                                    selectedVegetable = vegetable
                                    vegetableExpanded = false
                                },
                                modifier = Modifier.semantics {
                                    this.selected = (vegetable == selectedVegetable)
                                    this.role = Role.RadioButton
                                }
                            )
                        }
                    }
                }
            }
        }
    }
}

 

[jetpack compose] 포커스를 다른 요소로 보낼 수 있는 모디파이어, fun sendFocus(focusState: Boolean): Modifier 공유

엔비전스 접근성 | 2023-07-12 06:46:57

지난 번 젯팩 컴포즈에서 다른 요소로 포커스를 보내는 방법에 대해 다룬 적이 있습니다.

오늘은 해당 기능을 조금 더 간단하게 구현할 수 있도록 관련 모디파이어를 만들어 공유하게 되었습니다.

해당 모디파이어를 사용하면 스테이트 변수를 만들어서 포커스를 보내고자 하는 대상을 조금 더 간단하게 설정할 수 있습니다.

포커스를 보내고자 하는 요소가 하나라면 focusState와 같은 변수 초기 값은 false로 설정하고 포커스를 보내야 하는 시점에, 즉 해당 벼누가 트루가 되는 시점에 modifier = sendFocus(focusState) 와 같이 만들어 주기만 하면 됩니다.

포커스를 보내야 하는 대상이 여러 개라면 FocusState와 같은 enum class를 만들어 놓고 초기 컨스턴트를 none으로 설정한 다음 초점을 보내야 하는 각 대상에 

modifier = sendFocus(focusState == FocusState.Banana) 와 같이 사용할 수 있습니다.

그리고 대상은 설정되어 있으므로 초점을 보내야 하는 시점에는 onClick = { setFocusState(FocusState.Orange) } 와 같이 사용할 수 있습니다.
 

@Composable
fun sendFocus(focusState: Boolean): Modifier {
    val focusRequester = remember { FocusRequester() }

    LaunchedEffect(focusState) {
        if (focusState) {
            focusRequester.requestFocus()
        }
    }

    return Modifier
        .focusRequester(focusRequester)
        .focusable(focusState)
}

 

 

아래는 해당 모디파이어를 적용한 샘플 앱입니다. 

사과, 바나나 버튼과 오렌지 텍스트가 있습니다.

사과를 누르면 바나나로, 바나나를 누르면 오렌지로 초점이 이동됩니다.

package com.example.talkbackdetection

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                MainContent()
            }
        }
    }
}

enum class FocusState {
    None,
    Banana,
    Orange
}

@Composable
fun MainContent() {
    val (focusState, setFocusState) = remember { mutableStateOf(FocusState.None) }

    Column {
        Button(
            onClick = { setFocusState(FocusState.Banana) }
        ) {
            Text("Apple")
        }
        Button(
            onClick = { setFocusState(FocusState.Orange) },
            modifier = sendFocus(focusState == FocusState.Banana)
        ) {
            Text("Banana")
        }
        Text(
            "Orange",
            modifier = sendFocus(focusState == FocusState.Orange)
        )
    }
}

@Composable
fun sendFocus(focusState: Boolean): Modifier {
    val focusRequester = remember { FocusRequester() }

    LaunchedEffect(focusState) {
        if (focusState) {
            focusRequester.requestFocus()
        }
    }

    return Modifier
        .focusRequester(focusRequester)
        .focusable(focusState)
}

 

[python qt] 키보드 이벤트 적용하기

엔비전스 접근성 | 2023-07-10 19:39:15

접근성에서 항상 중요한 것 중 하나가 키보드 이벤트입니다.

이는 앱이 개발될 때 마우스 클릭에 대해서만 구현하는 경우가 많으며 커스텀 컨트롤의 경우 여러 번 말씀드렸지만 키보드 이벤트가 기본적으로 시스템 상에서 구현되어 있지 않기 때문입니다.

python qt에서 키보드 이벤트를 적용할 때에는 keypress event type을 사용합니다.

예를 들어 엔터키에 대한 이벤트를 적용한다면 if event.type() == QEvent.KeyPress and event.key() == Qt.Key_Return: 과 같이 적용할 수 있습니다.

아래는 샘플 코드 예제입니다.

리스트박스로 10개의 과일을 표시하고 있습니다.

특정 과일을 마우스로 클릭하면 선택한 과일을 얼럿으로 띄우는 아주 간단한 코드입니다.

그런데 리스트 위젯에서는 위 아래 화살표키로 다른 리스트 아이템을 선택할 수는 있지만 기본적으로 엔터키에 대한 키보드 이벤트가 없습니다.

그래서 키보드 이벤트를 적용해 주지 않으면 키보드 사용자는 각 리스트 아이템을 실행할 수 없게 되므로 엔터 키 이벤트를 적용한 것입니다.

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QListWidget, QListWidgetItem, QVBoxLayout, QMessageBox
from PyQt5.QtCore import Qt, QEvent

fruits = ["Apple", "Banana", "Orange", "Watermelon", "Pineapple", "Strawberry", "Mango", "Peach", "Grapes", "Blueberry"]

class MyApp(QWidget):
    def __init__(self):
        super().__init__()
        self.list_widget = QListWidget()
        for fruit in fruits:
            item = QListWidgetItem(fruit)
            self.list_widget.addItem(item)

        layout = QVBoxLayout()
        layout.addWidget(self.list_widget)
        self.setLayout(layout)

        self.setWindowTitle("Fruit List")
        self.resize(300, 200)

        self.list_widget.itemClicked.connect(self.on_item_clicked)

    def on_item_clicked(self, item):
        fruit = item.text()
        QMessageBox.information(self, "Fruit Selected", "You selected the " + fruit + " fruit.")

    def event(self, event):
        if event.type() == QEvent.KeyPress and event.key() == Qt.Key_Return:
            selected_item = self.list_widget.currentItem()
            if selected_item:
                self.on_item_clicked(selected_item)
            return True
        return super().event(event)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MyApp()
    window.show()
    sys.exit(app.exec_())

 

[python qt] 네이티브 체크박스와 setAccessibleName, setAccessibleDescription 관련

엔비전스 접근성 | 2023-07-08 18:47:57

앞으로 틈틈이 윈도 응용 프로그램의 접근성 구현에 대해서도 팁을 작성해 보려고 합니다.

당분간 저희 팀에서 연구를 시작한 python qt를 기준으로 팁을 작성해 보도록 하겠습니다.

첫 번째로 다루고 싶은 것은 체크박스 입니다.

모든 접근성이 그렇듯이 네이티브 체크박스를 사용하면 탭키를 사용하여 초점이 가고 스페이스로 체크 또는 체크를 해제할 수 있습니다.

그리고 체크박스에 대한 레이블을 기본적으로 self.like_fruits_checkbox = QCheckBox("Like") 와 같이 줄 수 있는데 이렇게 하면 탭키를 눌렀을 때 체크박스와 함께 레이블을 읽어주게 됩니다.

그런데 여기서 살펴보고자 하는 것은 체크박스에 대한 접근성 레이블을 별도로 주거나 체크박스에 대한 접근성 힌트 메시지를 줄 때 어떻게 하느냐 하는 것입니다.

이때 사용할 수 있는 것이 setAccessibleName, setAccessibleDescription 입니다.

네임을 사용하면 스크린 리더가 기존 체크박스 레이블을 무시하고 지정한 스트링으로 읽어줍니다. 

디스크립션을 사용하면 마치 웹에서 title 속성을 사용한 것처럼 요소 유형 뒤에 디스크립션에서 지정한 힌트 메시지를 읽습니다.

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

참고: 윈도 응용 프로그램에는 웹과 같이 가상 커서라는 것이 없습니다. 따라서 초점이 가지 않는 일반 텍스트의 경우에는 스크린 리더가 지원하는 객체 탐색 기능을 사용하여 탐색합니다.

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QCheckBox, QPushButton, QMessageBox, QLabel


class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Fruit Preference")
        self.setup_ui()

    def setup_ui(self):
        layout = QVBoxLayout()

        # Create label
        label = QLabel("Do you like fruits?")
        layout.addWidget(label)

        # Create checkbox
        self.like_fruits_checkbox = QCheckBox("Like")
        self.like_fruits_checkbox.setAccessibleDescription("Do you like fruits?")
        self.like_fruits_checkbox.setAccessibleName("like fruit")
        layout.addWidget(self.like_fruits_checkbox)

        # Create OK button
        ok_button = QPushButton("OK")
        ok_button.clicked.connect(self.show_message)
        layout.addWidget(ok_button)

        self.setLayout(layout)

    def show_message(self):
        if self.like_fruits_checkbox.isChecked():
            QMessageBox.information(self, "Message", "You checked 'Like Fruits'.")
        else:
            QMessageBox.information(self, "Message", "You did not check 'Like Fruits'.")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

 

[jetpack compose] 톡백 실행 여부 탐지하여 특정 메서드 적용 또는 제거하기

엔비전스 접근성 | 2023-07-06 07:04:29

젯팩 컴포즈에서 톡백 실행 여부를 탐지할 때에는 안드로이드 뷰 시스템에 있는 AccessibilityManager class 의 

exploreByTouchEnabled 조건문 변수를 그대로 사용합니다.

또한 액티비티가 실행된 상태에서 중간에 저시력 사용자가 톡백을 켜고 끄거나 스크린 리더 사용자가 특정 조건에서 톡백을 켜는 상황을 대비하여 

accessibilityManager.addTouchExplorationStateChangeListener, 
accessibilityManager.removeTouchExplorationStateChangeListener 메서드를 활용합니다.

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

talkBackEnabled 와 같은 특정 변수를 

private var talkBackEnabled by mutableStateOf(false) 와 같이 만들어 놓고 exploreByTouchEnabled 조건문을 따르도록 한 다음 true, false 일 때 관련 코드를 만들어 주기만 하면 됩니다.

아래는 관련 예제입니다. 

톡백이 켜져 있을 때와 꺼져 있을 때 화면에 표시되는 텍스트를 달리하였습니다.

아래 코드를 패키지만 바꾸어서 바로 테스트 해 볼 수 있습니다.

package com.example.talkbackdetection

import android.content.Context
import android.os.Bundle
import android.view.accessibility.AccessibilityManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.talkbackdetection.ui.theme.TalkBackDetectionTheme

class MainActivity : ComponentActivity() {
    private lateinit var accessibilityManager: AccessibilityManager
    private var talkBackEnabled by mutableStateOf(false)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        accessibilityManager = getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager

        setContent {
            TalkBackDetectionTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    MyScreen()
                }
            }
        }
    }

    override fun onStart() {
        super.onStart()
        accessibilityManager.addTouchExplorationStateChangeListener(touchExplorationStateChangeListener)
    }

    override fun onStop() {
        super.onStop()
        accessibilityManager.removeTouchExplorationStateChangeListener(touchExplorationStateChangeListener)
    }

    private val touchExplorationStateChangeListener =
        AccessibilityManager.TouchExplorationStateChangeListener { _ ->
            // Update the talkBackEnabled variable based on the TalkBack status
            val exploreByTouchEnabled = accessibilityManager.isTouchExplorationEnabled
            talkBackEnabled = exploreByTouchEnabled
        }

    @Composable
    fun MyScreen() {
        val context = LocalContext.current
        val accessibilityManager = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
        val exploreByTouchEnabled = accessibilityManager.isEnabled && accessibilityManager.isTouchExplorationEnabled
        talkBackEnabled = exploreByTouchEnabled

        Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            Column(horizontalAlignment = Alignment.CenterHorizontally) {
                Text(
                    text = "Hello, this screen is demonstrating TalkBack detection with Jetpack Compose",
                    modifier = Modifier.padding(16.dp)
                )
                Text(
                    text = if (talkBackEnabled) {
                        "You are running TalkBack"
                    } else {
                        "You are not running TalkBack"
                    },
                    modifier = Modifier.padding(16.dp)
                )
            }
        }
    }

    @Preview(showBackground = true)
    @Composable
    fun MyScreenPreview() {
        TalkBackDetectionTheme {
            MyScreen()
        }
    }
}

 

[android view system] onTouchListener 적용시 더욱 간편한 접근성 적용방법

엔비전스 접근성 | 2023-07-01 17:42:48

톡백에서 두 번 탭을 했을 때 발생하는 이벤트는 클릭입니다. 

물론 디폴트 클릭 리스너 자체가 없는 이미지뷰, 텍스트뷰와 같은 곳에 온터치 리스너를 적용하면 클릭 리스너 자체가 없어서 두 번 탭해서 온터치 이벤트를 실행시킬 수 있기는 하지만 클릭에 대한 힌트 메시지도 들을 수 없고 블루투스 키보드에서도 액션이 불가능한 이슈가 있습니다.

그렇다고 슬라이드를 해서 특정 도구모음을 닫는다든지 하는 액션이 있는 경우 온터치 이벤트가 들어가야 하기 때문에 대부분 클릭 이벤트로 구현하기는 하지만 온터치 이벤트를 전혀 사용하지 않을 수는 없습니다.

그래서 지난 번 온터치 이벤트 적용 시에 performClick 메서드의 디폴트 동작을 오버라이드하여 온터치 이벤트가 발생할 때 클릭 이벤트도 함께 구현하도록 가이드를 했었습니다.

그런데 이 performClick 구현보다 조금 더 간편한 방법이 있어 오늘 예제와 함께 공유하려고 합니다.

바로 지난 번 소개해 드린 replaceAccessibilityAction 메서드를 활용하는 것입니다.

해당 액션 가운데 클릭 액션이 있고 클릭 액션에는 힌트만 재정의할 수 있는게 아니라 접근성 서비스로 클릭을 했을 때 수행될 동작도 정의할 수 있습니다.

따라서 온터치 이벤트로 구현한 펑션을 해당 클릭 액션 안에 넣어주면 접근성 서비스로는 클릭을 해도 해당 이벤트가 발생하게 되는 것입니다.

이렇게 하면 클릭 액션도 쉽게 정의할 수 있고 온터치와 퍼폼 클릭만으로 지원되지 않는 실행하려면 두 번 탭하라는 힌트 메시지도 자연스럽게 적용시켜줄 수 있습니다.

아래는 코드 예시입니다.

참고로 가장 마지막에 클릭 접근성 이벤트도 함께 포함시켜 주었습니다. 이유는 기본적으로 네이티브 클릭 이벤트는 자체적으로 접근성 클릭 이벤트를 가지고 있어서 클릭했을 때 선택됨 상태 정보가 변경되면 이를 자동으로 읽어주는데 온터치에 클릭 이벤트를 커스텀으로 적용했을 때 접근성 클릭 이벤트를 시스템에서 보내주지 않아서 이를 피드백하지 못하기 때문입니다.

// Add click actions to the views
        ViewCompat.replaceAccessibilityAction(
            tab1TextView,
            AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
            null
        ) { _, _ ->
            selectedTab = 0
            updateTabSelection()
            updateContents()
            tab1TextView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED)
            true
        }

 

아래는 전체 코드입니다. 

안드로이드 뷰 프로젝트를 만들고 MainActivity.kt, activity_main.xml 파일에 각각 코드 붙여넣기 후 실행하면 톡백으로 바로 테스트해볼 수 있습니다.

단 MainActivity 클래스의 패키지는 수정하셔야 합니다.

//레이아웃 xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:orientation="vertical"
        android:gravity="center">

        <TextView
            android:id="@+id/fruits_text_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Fruits" />

        <TextView
            android:id="@+id/vegetables_text_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Vegetables" />

        <TextView
            android:id="@+id/fishes_text_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Fishes" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/tab1_text_view"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="Fruits" />

        <TextView
            android:id="@+id/tab2_text_view"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="Vegetables" />

        <TextView
            android:id="@+id/tab3_text_view"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="Fishes" />

    </LinearLayout>

</LinearLayout>

 

//MainActivity

package com.example.talkbacktest

import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import com.example.talkbacktest.R

class MainActivity : AppCompatActivity() {

    private lateinit var fruitsTextView: TextView
    private lateinit var vegetablesTextView: TextView
    private lateinit var fishesTextView: TextView
    private lateinit var tab1TextView: TextView
    private lateinit var tab2TextView: TextView
    private lateinit var tab3TextView: TextView

    private val fruits = listOf("Apple", "Banana", "Orange", "Watermelon", "Grapes")
    private val vegetables = listOf("Potato", "Carrot", "Onion", "Cucumber", "Tomato")
    private val fishes = listOf("Salmon", "Tuna", "Mackerel", "Halibut", "Sardines")

    private var selectedTab = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        fruitsTextView = findViewById(R.id.fruits_text_view)
        vegetablesTextView = findViewById(R.id.vegetables_text_view)
        fishesTextView = findViewById(R.id.fishes_text_view)
        tab1TextView = findViewById(R.id.tab1_text_view)
        tab2TextView = findViewById(R.id.tab2_text_view)
        tab3TextView = findViewById(R.id.tab3_text_view)

        fruitsTextView.text = fruits[0]
        vegetablesTextView.text = vegetables[0]
        fishesTextView.text = fishes[0]

        updateTabSelection()

        tab1TextView.setOnTouchListener { _, event ->
            if (event.action == MotionEvent.ACTION_UP) {
                selectedTab = 0
                updateTabSelection()
                updateContents()
            }
            true
        }

        tab2TextView.setOnTouchListener { _, event ->
            if (event.action == MotionEvent.ACTION_UP) {
                selectedTab = 1
                updateTabSelection()
                updateContents()
            }
            true
        }

        tab3TextView.setOnTouchListener { _, event ->
            if (event.action == MotionEvent.ACTION_UP) {
                selectedTab = 2
                updateTabSelection()
                updateContents()
            }
            true
        }
// Add click actions to the views
        ViewCompat.replaceAccessibilityAction(
            tab1TextView,
            AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
            null
        ) { _, _ ->
            selectedTab = 0
            updateTabSelection()
            updateContents()
            tab1TextView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED)
            true
        }

        ViewCompat.replaceAccessibilityAction(
            tab2TextView,
            AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
            null
        ) { _, _ ->
            selectedTab = 1
            updateTabSelection()
            updateContents()
            tab2TextView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED)
            true
        }

        ViewCompat.replaceAccessibilityAction(
            tab3TextView,
            AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
            null
        ) { _, _ ->
            selectedTab = 2
            updateTabSelection()
            updateContents()
            tab3TextView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED)
            true
        }
    }

    private fun updateTabSelection() {
        tab1TextView.isSelected = selectedTab == 0
        tab2TextView.isSelected = selectedTab == 1
        tab3TextView.isSelected = selectedTab == 2
    }

    private fun updateContents() {
        when (selectedTab) {
            0 -> {
                fruitsTextView.text = fruits[0]
                vegetablesTextView.text = fruits[1]
                fishesTextView.text = fruits[2]
            }
            1 -> {
                fruitsTextView.text = vegetables[0]
                vegetablesTextView.text = vegetables[1]
                fishesTextView.text = vegetables[2]
            }
            2 -> {
                fruitsTextView.text = fishes[0]
                vegetablesTextView.text = fishes[1]
                fishesTextView.text = fishes[2]
            }
        }
    }
}

 

[swift UI] systemName image checkmark와 접근성

엔비전스 접근성 | 2023-06-24 16:48:15

iOS 네이티브 앱에는 체크박스라는 뷰 혹은 접근성 트레이트가 없기 때문에 무엇인가를 선택 또는 해제하는 체크박스의 경우 button, selected accessibilityTraits를 조합하여 사용합니다.

그런데 swiftUI에서 지원하고 있는 systemName 이미지 중 checkmark라는 이미지를 선택되었다는 용도로 사용하면 selected 트레이트를 사용하지 않아도 보이스오버에서 선택됨으로 읽어줍니다. 즉 해당 checkmark가 마치 선택됨의 네이티브처럼 작동되는 것입니다.

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


import SwiftUI

struct ContentView: View {
  @State private var age = ""
  @State private var displayAge = false
  @State private var showAlert = false

  var body: some View {
    VStack {
      Button(action: {
        displayAge.toggle()
      }) {
        Text("Display Age")
        Image(systemName: displayAge ? "checkmark" : "")
      }
      .padding()
      
      TextField("Age", text: $age)
        .textFieldStyle(RoundedBorderTextFieldStyle())
        .keyboardType(.numberPad) // Display number keyboard
        .padding()
      
      Button("Submit") {
        showAlert = true
      }
      .disabled(age.isEmpty) // This line of code will disable the submit button if the age text field is empty.
      .alert(isPresented: $showAlert) {
        if displayAge {
          return Alert(title: Text("Your age"), message: Text(age), dismissButton: .default(Text("OK")))
        } else {
          return Alert(title: Text("Error"), message: Text("You did not turn on the display age switch"), dismissButton: .default(Text("OK")))
        }
      }
    }
  }
}

 

[react native] 보이스오버, 톡백으로 두 번 탭하는 동작 재정의하기

엔비전스 접근성 | 2023-06-14 07:44:58

상황에 따라 두 번탭하는 디폴트 액션을 재정의하거나 클릭 이벤트가 없는 특정 요소를 보이스오버, 톡백으로 두 번 탭하여 실행 시 특정 이벤트가 실행되게 해야 하는 경우가 있을 수 있습니다.

이때 우리는 accessibilityActions, onAccessibilityAction을 사용하여 이를 구현할 수 있습니다.

accessibilityActions 안에는 activate, increment, decrement와 같은 디폴트 접근성 액션들이 있습니다. 이러한 액션을 사용하게 되면 두 번 탭, 슬라이더 등의 접근성 액션을 재정의할 수 있습니다.

만약 디폴트 액션 네임이 아닌 커스텀 네임을 사용하게 되면 커스텀 액션으로 구현됩니다.

이렇게 구현한 액션네임은 onAccessibilityAction 안에서 각 액션 네임에 대한 이벤트를 정의합니다.

액션이 두 개 이상 등록되었다면 조건문을 통해 a 액션 실행시 수행해야 할 동작, b 액션 실행 시 수행해야 할 동작과 같은 형식으로 코드를 만들어 주어야 합니다.

참고: activate 사용 시 action label은 필수가 아닙니다. 

그러나 label을 사용하게 되면 안드로이드에서 활성화 하려면 두 번 탭하세요 대신에 레이블을 가지고 와서 힌트로 출력하게 됩니다.

아래는 두 번탭을 오버라이드 하는 코드 예시입니다.

아래 코드에는 증가, 감소 버튼이 있습니다.

증가 버튼을 탭하면 원래 1씩 숫자가 증가합니다.

그러나 스크린 리더로 두 번탭을 재정의하였기 때문에 스크린 디러를 실행한 채로 증가 버튼을 두 번 탭하면 2씩 증가합니다.

import React, { useState } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';

const App = () => {
  const [number, setNumber] = useState(0);

  const incrementNumber = () => {
    setNumber(number + 1);
  };

  const decrementNumber = () => {
    setNumber(number - 1);
  };

  const onIncrementAccessibilityAction = () => {
    setNumber(number + 2);
  };

  return (
    <View style={styles.container}>
      <Text style={styles.numberText}>{number}</Text>
      <Button
        title="Increment"
        onPress={incrementNumber}
        accessibilityLabel="Increment"
        onAccessibilityAction={onIncrementAccessibilityAction}
        accessibilityActions={[{ name: 'activate', label: 'Activate' }]}
      />
      <Button
        title="Decrement"
        onPress={decrementNumber}
        accessibilityLabel="Decrement"
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  numberText: {
    fontSize: 24,
    marginBottom: 20,
  },
});

export default App;

 

[jetpack compose] 버튼 텍스트가 클릭 시마다 변경될 때

엔비전스 접근성 | 2023-06-06 11:47:24

안드로이드 뷰 시스템에서는 버튼 텍스트가 변경될 때 변경된 텍스트를 바로 읽어줄 수 있도록 하기 위해서 접근성 이벤트중 하나인  sendAccessibilityEvent 를 활용하였습니다.

그러나 아직 jetpack compose 에는 강제로 접근성 이벤트를 보낼 수 있는 방법이 없습니다.

따라서 버튼 텍스트가 반복재생, 한번 재생, 렌덤재생과 같이 클릭 시마다 변경되는 경우 liveRegion 시맨틱스 모디파이어를 활용하여 접근성을 적용해줄 수 있습니다.

다만 처음부터 버튼에 해당 모디파이어를 추가하게 되면 해당 화면이 실행되자마자 무조건 버튼 텍스트를 읽게 됩니다.

우리가 원하는 것은 클릭을 할 때 변경되는 텍스트를 읽도록 하는 것이기 때문에 liveRegion 속성을 컨트롤할 수 있는 조건문 boolean 변수를 하나 만들고 기본은 false 로 설정한 다음 클릭했을 때 true 가 되고 true 가 되었을 때 버튼에 liveRegion 시맨틱스가 설정되게 하는 것이 현재로서는 최선인 것으로 보여집니다.

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

@Composable
fun TalkBackTest() {
    var currentText by remember { mutableStateOf("Fruit") }
    var hasClicked by remember { mutableStateOf(false) }

    Column {
        Text(
            text = "Sample App",
            modifier = Modifier.semantics {heading() }
        )

        Button(
            onClick = {
                currentText = if (currentText == "Fruit") "Vegetable" else "Fruit"
                hasClicked = true
            },
            modifier = Modifier.semantics {
                if (hasClicked) {
                    liveRegion = LiveRegionMode.Polite
                }
            }
        ) {
            Text(text = currentText)
        }
    }
}

 

[jetpack compose] paneTitle 코드 예시

엔비전스 접근성 | 2023-06-06 10:23:29

안드로이드 뷰 시스템을 다룰 때 언급한 바와 같이 화면 전환에 대한 접근성 이벤트를 구현하는 가장 쉬운 방법은 accessibilityPaneTitle 속성을 활용하는 것입니다.

컴포즈에서도 시맨틱스 안의 paneTitle 모디파이어를 활용하면 이를 쉽게 구현할 수 있습니다.

여러 화면들이 나뉘어져 있다고 가정하고 특정 화면 컴포저블이 있다면 해당 화면의 상위 레이아웃에 다음 예시와 같이 panetitle 속성을 추가하면 됩니다.

@Composable
fun VegetableScreen(onButtonClick: (Screen) -> Unit, currentScreen: Screen) {
    val vegetables = listOf("Potato", "Carrot", "Broccoli", "Cauliflower", "Cucumber")

    Column(
        Modifier.fillMaxWidth().padding(8.dp)
            .semantics() {
                this.paneTitle = "Vegetable Screen"
            }
    ) {
        vegetables.forEach { vegetable ->
            Text(vegetable)
        }

        Button(onClick = { if (currentScreen is Screen.Vegetable) onButtonClick(Screen.Fruit) }) {
            Text("Change to fruit")
        }
    }
}

 

[javascript] passiveRadio(radioGroup) 메서드 추가

엔비전스 접근성 | 2023-06-04 21:59:31

라디오버튼의 경우 기본적으로 화살표를 누르면 같은 name 속성을 가진 라디오버튼들 사이에서 이전 또는 다음 라디오버튼으로 포커스 되면서 포커스된 라디오버튼이 자동으로 체크됩니다. 

그런데 라디오버튼이 체크되는 동시에 라디오버튼이 포함된 요소들의 구조가 변경되거나 얼럿이 표시되는 경우 라디오버튼을 전환할 때마다 초점을 잃어버리거나 메시지가 출력되므로 키보드 사용을 통한 라디오버튼 조작 자체가 어렵게 됩니다.

지난번에 이를 해결하고자 라디오버튼 선택시 동적으로 페이지가 변경되는 경우 라디오버튼과 연결되어 있는 레이블 태그를 마치 버튼처럼 만들어 사용할 수 있는 radioAsButton 메서드를 공유한 적이 있습니다.

이번에는 조금 다른 접근법으로 라디오버튼은 유지하되 화살표를 움직여 라디오버튼 사이를 이동할 때 라디오버튼이 자동으로 체크되지 않도록 하는 메서드를 만들어 공유합니다.

passiveRadio(radioGroup) 인자 값으로는 라디오버튼들을 품고 있는 라디오그룹을 참조합니다.

이렇게 하면 적용된 라디오그룹 안의 모든 라디오버튼들에 대해서는 화살표 이동시 이전 혹은 다음 라디오버튼으로 포커스는 되지만 자동으로 선택되지 않으며 선택은 스페이스로 할 수 있습니다.

따라서 라디오버튼이 선택되자마자 동적으로 라디오버튼을 포함한 요소들이 업데이트 되거나 얼럿이 표시될 때 자동 체크되는 것을 막는 목적으로 사용할 수 있습니다.

js 다운로드

[javascript] afterDeleteFocusManage(container, buttonClassName) 메서드 추가

엔비전스 접근성 | 2023-06-04 21:45:37

최근 검색어리스트, 즐겨찾기 리스트, 장바구니 리스트 등에는 각 리스트마다 삭제 버튼이 표시되는 경우가 많습니다.

문제는 삭제 버튼을 눌러 특정 리스트를 삭제하면 그 버튼 자체가 사라지게 되므로 스크린 리더에 따라 초점 자체를 잃어버리는 경우가 많다는 것입니다.

이렇게 되면 초점이 초기화 되고 포커스가 웹페이지 상단으로 튀어버리게 되므로 연속적인 탐색이 상당히 어렵습니다.

이를 조금이나마 해결하고자 function afterDeleteFocusManage(container, buttonClassName) 메서드를 만들어 공유하게 되었습니다.

인자 값으로는 삭제 버튼들이 들어 있는 div, ul 과 같은 컨테이너와 삭제버튼이 가지고 있는 클래스 이름 입니다.

예시: afterDeleteFocusManage(deleteContainer, deleteButton)

해당 메서드를 적용하게 되면 다음과 같이 동작합니다.

1. 인자 값으로 지정한 클래스 네임을 가진 요소를 클릭하게 되면 다음 리스트에 해당 클래스를 가진 요소가 있을 경우 해당 요소로 포커스 시킵니다. 

2. 만약 없으면 이전의 동일한 클래스를 가진 요소가 있을 경우 해당 요소로 포커스 시킵니다.

이렇게 하면 일일이 삭제 버튼 동작 시 초점 관리를 구현하지 않아도 초점 관리에 대한 접근성 적용이 가능합니다.

js 다운로드

[jetpack compose] Jetpack Compose에서 View를 활용한 announceForAccessibility 구현하기

엔비전스 접근성 | 2023-05-29 15:40:38

 

스크린 리더 사용자에게 화면에 없지만 중요한 메시지를 전달하는 것은 Jetpack Compose에서 직접 `announceForAccessibility` API가 없어 어려울 수 있습니다. 그러나 레거시 View 시스템을 Compose와 결합하여 이를 가능하게 할 수 있습니다. 

단계 1: Composable 함수 생성
먼저 UI 구성 요소를 설정할 Composable 함수를 만듭니다. 이 Composable 내에서 `LocalView.current`를 사용하여 현재 View에 대한 참조를 얻습니다.

@Composable
fun ScreenReaderTestScreen() {
    val localView = LocalView.current
    // Your UI components go here
}

단계 2: announce 함수 정의
Composable 범위 외부에서 View와 메시지를 매개변수로 받는 함수를 정의합니다. 이 함수는 `announceForAccessibility`를 사용하여 메시지를 스크린 리더에게 전달하는 역할을 합니다.

fun announce(view: View, message: String) {
    view.announceForAccessibility(message)
}

단계 3: 함수 트리거
Composable 내에서 메시지를 전달해야 할 때 `announce` 함수를 사용하여 `localView와 원하는 메시지를 호출합니다. 이는 버튼 클릭과 같은 View 기반 컨텍스트 내에서 수행되어야 합니다.
Button(
    onClick = {
        announce(localView, "You clicked the screen")
    }
) {
    Text("Test Button")
}
 

[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 다운로드

[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 태그에 직접 바뀐 타이틀을 전달해야 합니다.

 

참고:

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

[javascript] improveAccessibility.js 파일에 joinSplitedTexts 메서드 추가

엔비전스 접근성 | 2023-05-22 15:42:17

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

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

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

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

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

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

js 다운로드

[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>
  )
};

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

 

[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 더보기와와 같이 하나의 쌍으로 존재해야 오류가 없습니다.

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에 다크모드가 적용된 모습

 

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>

 

[HTML] <select> 태그의 레이블이 label for, title 또는 aria-label로 제공되는 경우

엔비전스 접근성 | 2023-05-05 11:16:43

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

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

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

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

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

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

[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
      importantForAccessibility="no"
    >
      <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;

 

[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,
  },
});

 

[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}
        >

 

[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;

 

[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;

 

[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;

 

[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;

 

[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;

 

톡백에서 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>

혹시 방법이 있을까요?

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

[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(), 잘 알고 사용합시다.

[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에 마크업한 슬라이더에 적용

결과물:


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

자바스크립트 없이 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를 제외한 국내에서 많이 쓰거나 무료인 스크린 리더에서는 읽는 방식은 다르지만 잘 읽는 것을 볼 수 있습니다.

[기업 서비스 접근성 구현 사례 공유] 기획전 a11y 개선 프로젝트

Webacc NV | 2023-01-17 14:57:04

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

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

[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)

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

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

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

[android view system] 접근성 적용 유틸 클래스에 viewAsRoleDescription 메서드 추가

Webacc NV | 2023-01-14 17:31:30

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

요소 유형이 지정되어 있든 그렇지 않든 메뉴항목과 같은 새로운 요소 유형으로 변경해야 할 때 사용할 수 있습니다.

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

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

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

accessibilityUtil.viewAsRoleDescription(buttonView, "목록항목")

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

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

[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와 달리 제목을 제외한 버튼 슬라이드와 같은 요소 유형 두 개는 추가할 수 없습니다.

[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")); // 이벤트를 발생시켜 페이지에 접속했을 때 멈춤 적용

 

[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 속성 체험하기

[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]

[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로 설정한 것과 같은 상황이 됩니다.

[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),
                            ),

 

[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: () {
                    },
                  ),
                );

 

[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() }

 

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

태그가 잘못 닫혀있어요

틀렸어요 | 2022-12-13 10:50:08

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

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

 

잘못된 태그

[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을 사용하면 여러 알림을 사용하더라도 큐 위치가 겹치지 않기 때문에 오류 없이 무난하게 사용자에게 스크린리더 알림을 줄 수 있습니다.

피씨에서 폼 요소에 마우스 클릭 시 포커스 효과 있어야 할까요?

능소니 | 2022-11-07 16:12:58

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

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

[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 객체에 텍스트만을 교체하는 방식을 사용해야 합니다.

[swift UI] 보이스오버에서의 이중탭 액션 재정의하기

Webacc NV | 2022-10-30 10:49:26

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

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

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

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

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

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

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

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

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

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

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

				.accessibilityAction {
					self.showCharge(true)
				}

 

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

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

[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 모디파이어 사용시 필요 없음).

[android view system] 동일한 클릭 속성을 가진 스위치, 체크박스와 텍스트뷰의 초점 합치기

Webacc NV | 2022-10-26 21:18:10

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

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

checkBox.isFocusable = false

checkBox.isClickable = false

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

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

[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
			}
		}

 

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

[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)
        }

 

[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)
  }
}

//...

 

[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
      }
    }
  }
}

 

[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.")
    }
}

 

[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 속성이 자동으로 붙도록 한 것입니다.

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

[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
            }
        }

 

[swift UI] 장식용 이미지 보이스오버 초점에서 제거하기

Webacc NV | 2022-09-03 14:33:02

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

방법은 두 가지입니다. 

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

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

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

 

[swiftUI] 보이스오버에서의 이미지 처리에 관하여

Webacc NV | 2022-09-02 16:36:18

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

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

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

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

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

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

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

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

웹 접근성 직군별 교육 수정 요구

malangdidoo | 2022-08-14 02:17:54

https://nuli.navercorp.com/education

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

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

 

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

 

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

수정 부탁드립니다!

[센스리더] 최근 업데이트 된 크롬에서의 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. 가상커서 초점이 브라우저 초점과 동기화되지 않는 이슈가 대부분 수정되었습니다. 

 

[android 공통] 접근성 진단은 TalkBack으로

Webacc NV | 2022-08-04 16:55:26

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

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

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

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

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

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

[UIKit] UIAccessibility announcement 간단히 사용할 수 있는 extension 공유

Webacc NV | 2022-07-26 12:35:20

보이스오버에서 특정 

알림을 어나운스 형태로 제공해야 할 때 UIAccessibility.post(notification: .announcement, argument: announcementString) 과 같은 형식으로 구현을 합니다.

그런데 사용자가 특정 요소를 누르는 것과 어나운스가 발생하는 시간이 중첩되는 경우 이러한 어나운스들은 무시되는 경우가 많아 0.1초 이상의 딜레이를 주게 됩니다.

그래서 이를 조금 더 간단하게 구현할 수 있도록 announceForAccessibility extension을 만들어 공유하게 되었습니다.

넣어 주어야 할 값은 스트링 문자입니다.

따라서 특정 뷰의 텍스트나 accessibilityLabel 혹은 
announceForAccessibility("\(counterValue)") 와 같이 추가만 하면 끝입니다.

아래는 extension 코드입니다.

extension UIViewController {
    func announceForAccessibility(_ string: String) {
        Task {
            // Delay the task by 100 milliseconds
            try await Task.sleep(nanoseconds: UInt64(0.1 * Double(NSEC_PER_SEC)))
            
            // Announce the string using VoiceOver
            let announcementString = NSAttributedString(string: string, attributes: [.accessibilitySpeechQueueAnnouncement : true])
            UIAccessibility.post(notification: .announcement, argument: announcementString)
        }
    }

아래는 이를 적용한 간단한 예제입니다.

수량 증가 및 감소 버튼이 있고 증가나 감소 버튼을 누를 때마다 화면에 있는 증감된 숫자를 자동으로 읽어주도록 한 것입니다.

스토리보드가 없는 뷰컨트롤러를 만들고 테스트해볼 수 있습니다.

import UIKit

extension UIViewController {
    func announceForAccessibility(_ string: String) {
        Task {
            // Delay the task by 100 milliseconds
            try await Task.sleep(nanoseconds: UInt64(0.1 * Double(NSEC_PER_SEC)))
            
            // Announce the string using VoiceOver
            let announcementString = NSAttributedString(string: string, attributes: [.accessibilitySpeechQueueAnnouncement : true])
            UIAccessibility.post(notification: .announcement, argument: announcementString)
        }
    }
}

class ViewController: UIViewController {
    
    var counterLabel: UILabel!
    var counterValue: Int = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Create increase button
        let increaseButton = UIButton(frame: CGRect(x: 50, y: 100, width: 100, height: 50))
        increaseButton.setTitle("Increase", for: .normal)
        increaseButton.setTitleColor(.blue, for: .normal)
        increaseButton.addTarget(self, action: #selector(increaseCounter), for: .touchUpInside)
        view.addSubview(increaseButton)
        
        // Create decrease button
        let decreaseButton = UIButton(frame: CGRect(x: 200, y: 100, width: 100, height: 50))
        decreaseButton.setTitle("Decrease", for: .normal)
        decreaseButton.setTitleColor(.blue, for: .normal)
        decreaseButton.addTarget(self, action: #selector(decreaseCounter), for: .touchUpInside)
        view.addSubview(decreaseButton)
        
        // Create counter label
        counterLabel = UILabel(frame: CGRect(x: 150, y: 200, width: 50, height: 50))
        counterLabel.text = "\(counterValue)"
        counterLabel.textAlignment = .center
        view.addSubview(counterLabel)
    }
    
    @objc func increaseCounter() {
        counterValue += 1
        counterLabel.text = "\(counterValue)"
        
        // Announce changed counter value using VoiceOver with a delay of 100 milliseconds
        announceForAccessibility("\(counterValue)")
    }
    
    @objc func decreaseCounter() {
        counterValue -= 1
        counterLabel.text = "\(counterValue)"
        
        // Announce changed counter value using VoiceOver with a delay of 100 milliseconds
        announceForAccessibility("\(counterValue)")
    }
}


 

[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의 종료를 계기로 앞으로는 웹환경에 맞춘 보다 좋은 최신 기술로 코드 작성이 가능해질 것입니다.

[swiftUI] accessibilityRepresentation API 소개

Webacc NV | 2022-07-07 15:12:30

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

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

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

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

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

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

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

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

 

[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("편집 영역"))
    }

 

[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) 를 사용하면 되겠습니다.

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

[android] 안드로이드 디버깅 시에 사용할 수 있는 ui automator 파일 다운로드 제공 관련의 건

Webacc NV | 2022-06-24 19:07:22

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

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

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

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

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

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

[iOS native] 미디어 재생 버튼 구현시 startMediaSeesion trait 적용 권고

Webacc NV | 2022-06-21 16:16:40

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

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

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

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

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

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

[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" 과 같이 속성을 변경하면 퍼센트가 해당 값만큼씩 증감하게 됩니다.

[android view system] 접근성 포커스가 진입하면 경과되는 시간 더 이상 읽지 못하게 하기

Webacc NV | 2022-06-13 16:23:15

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

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

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

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

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

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

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

아래 예시를 참고합니다.

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

 

[mobile web] TalkBack에서의 라디오버튼 지원 업데이트

Webacc NV | 2022-06-06 15:46:36

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

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

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

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

[android view system] 접근성 유틸 클래스에 setAsDropdownWithHint 메서드 추가

Webacc NV | 2022-06-02 12:38:08

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

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

 

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

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

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

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

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

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

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

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

유틸 클래스 자바 다운로드

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

[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
    )
}

 

[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)
        )
    }
}

 

[jetpack compose] clearAndSetSemantics 메서드에 관하여

Webacc NV | 2022-05-28 11:16:42

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

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

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

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

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

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

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

modifier = Modifier.clearAndSetSemantics { }

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

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

[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
    ) { .... }

 

[ARIA 예제] 편집창에 바로 입력되지 않는 자동완성 예제 체험해보기

Webacc NV | 2022-05-24 12:03:38

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

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

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

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

[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 = "읽지 않음")
    }

 

[iOS native] UITextView 요소를 보이스오버가 읽을 시 쉼표가 붙는 이슈 수정 관련

Webacc NV | 2022-05-20 17:12:43

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

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

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

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

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

[javascript] improveAccessibility.js에 announceForAutoComplete 메서드 추가

Webacc NV | 2022-05-19 11:08:30

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

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

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

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

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

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

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

improveAccessibility.js 다운로드

[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 요소의 레이블을 정의합니다.

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

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

샘플 페이지 가기

 

[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으로 확인하는 것이 더 쉽습니다.

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

[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 다운로드

[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,
          ),
        ));
      },
    );
  }

 

[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="" 형태로 마크업하고 자동완성 리스트 중 하나를 가리키고 있을 경우에는 해당 아이디를 연결해줍니다.

[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 다운로드

[javascript] 드롭다운 팝업메뉴 접근성 적용을 위한 popupMenu js 추가

Webacc NV | 2022-04-18 15:52:08

옵션 더보기와 같은 버튼을 눌렀을 때 팝업 메뉴가 표시되는 경우 이를 적용하기 위한 js를 만들어 공유합니다. 

해당 펑션의 파라미터는 팝업 메뉴가 포함되어 있는 컨테이너(예: div)입니다.

해당 펑션을 옵션 메뉴가 표시되었을 때 적용하면 다음과 같은 것들이 적용됩니다.

1. 적용된 파라미터에 role menu 속성이 없으면 추가합니다.

2. 하위에 role menuitem 항목이 있으면 이에 대한 키보드 접근성을 자동 적용하여 위 또는 아래 화살표로 메뉴를 탐색할 수 있도록 합니다. 만약 하위에 role menuitem 요소가 하나도 없고 a href 또는 button 요소가 있으면 해당 요소에 자동으로 role menuitem 속성을 추가한 다음 키보드 접근성을 적용합니다.

3. 탭이나 쉬프트 탭은 기본 동작을 막아 메뉴 바깥으로 초점이 이동하지 못하도록 합니다.

4. 만약 초점이 메뉴 영역에 없으면 메뉴 요소의 첫 번째 요소로 초점을 보내줍니다.

 

사용법: 위에서 언급한 것처럼 메뉴가 표시되었을 때 메뉴를 포함하고 있는 요소를 파라미터 값으로 지정해 주기만 하면 됩니다.

 

추가로 적용해야 하는 것들:

1. 메뉴 아이템들은 적어도 첫 번째 요소는 탭키를 눌러 포커스 할 수 있어야 합니다. 따라서 메뉴 요소들이 li, span과 같은 커스텀 요소라면 tabindex 0 속성을 반드시 지정해 주세요.

2. 메뉴가 사라질 때 초점 관리는 직접 해 주셔야 합니다. 즉 메뉴가 닫히면 초점을 기존 메뉴가 열리는 버튼에 보내주거나 혹은 메뉴가 다른 팝업을 호출하는 경우 해당 팝업으로 초점을 보내 주어야 합니다.

3. 메뉴가 열린 상태에서 ESC 즉 취소 키를 누르면 메뉴가 닫히게 적용해 주세요. 이는 메뉴를 닫는 것에 대한 기본적인 키보드 조작 방법이기 때문입니다.

4. 메뉴를 여는 버튼에는 aria-haspopup menu 속성을 추가해 주세요.

js 다운로드

[flutter] CheckBox 위젯은 VoiceOver에서 스위치버튼 역할로 정의됨

Webacc NV | 2022-04-11 19:39:35

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

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

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

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

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

[flutter] 화면에 보여지는 일반 텍스트의 레이블을 대체 텍스트로 대체할 경우

Webacc NV | 2022-04-10 11:52:32

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

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

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

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

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

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

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

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

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

[android wear] 워치에서의 최신 TalkBack 업데이트에 따른 클릭 리스너가 포함된 ImageView 처리방식 수정 관련

Webacc NV | 2022-04-02 12:17:29

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

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

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

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

[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 함수에 대한 자세한 설명은 관련 팁을 참고하시면 됩니다.

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 컨퍼런스 홈페이지를 참고해 주시기 바랍니다.

[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 다운로드

[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 다운로드

[javascript] improveAcessibility.js 파일에 추가된 focusTogetherForMobile 메서드 소개

Webacc NV | 2022-03-08 15:13:10

아이폰에서 여러 텍스트 스타일이 하나의 링크로 구성된 페이지를 VoiceOver로 탐색할 때 수많은 초점 분리로 인해 탐색이 비효율적인 경험을 해 보셨을 줄로 압니다.

해당 이슈는 VoiceOver에서 개선을 해 주 어야 하는 이슈이지만 임시 방편으로나마 해당 유형의 링크 탐색 시 스크린 리더 사용성을 개선할 수 있도록 focusTogetherForMobile(selector) 메서드를 만들어 공유하게 되었습니다.

인자값으로는 초점이 분리되는 링크들을 품고 있는 요소를 넣어 주시면 됩니다.

예: focusTogetherForMobile('.test_div');

improveAccessibility.js 를 추가한 상태에서 해당 메서드를 실행해 주기만 하면 됩니다.

특징은 다음과 같습니다.

1. 아이폰 모바일 디바이스에서만 동작합니다.

2. <a href 속성이 있는 링크 안의 모든 텍스트와 이미지 alt 속성은 다 aria-label 안에 넣고 대신 실제 텍스트 노드들은 aria-hidden true로 숨깁니다.

improveAccessibility.js 다운로드

[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 다운로드

[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 다운로드

[javascript] screenReaderLive() 소개

Webacc NV | 2022-02-25 15:20:35

버튼을 누를 때 상태 정보가 아닌 레이블 자체가 변경되는 경우 스크린 리더에 따라 처리하는 방식이 다 다르다보니 특정 스크린 리더에서는 변경된 레이블 자체를 읽어주지 않습니다. 

예를 들어 피씨 웹 NVDA의 경우는 버튼 레이블이 aria-label 속성으로 마크업 되어 있으면 변경되는 텍스트를 자동으로 읽어주지만 버튼이나 하위 스팬과 같은 곳에 텍스트 자체로 마크업되어 있고 레이블이 변경되면 이를 읽어주지 않습니다. 

그래서 피씨, 모바일을 고려하여 버튼이 변경될 때 자동으로 변경된 레이블을 스크린 리더가 읽도록 하는 screenReaderLive 메서드를 공유하게 되었습니다.

파라미터는 없으며 사용 방법은 다음과 같습니다.

1. 버튼이 클릭 되었을 때 변경되는 레이블을 읽어야 하는 버튼들에 screen-reader-live 라는 속성을 추가합니다. 

2. 웹페이지가 로딩된 상태에서 screenReaderLive() 펑션을 실행하면 끝입니다.

js 다운로드

[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 다운로드

[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 다운로드

[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 다운받기

[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 다운로드

[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 를 다운받아 사용할 수 있습니다.

[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]

[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 다운받기

[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 조건문을 사용해야만 합니다.

[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())

유틸클래스 자바 다운로드

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

[android native] java 및 kotlin util class에 적용된 setAsRadioButton 접근성 업데이트

Webacc NV | 2022-01-21 09:40:50

오랜만에 android util class 업데이트를 진행합니다.

현재 TalkBack에서는 라디오버튼에 초점을 두고 있으면 선택한 라디오버튼이든 선택이 안 된 라디오버튼이든 간에 접근성 힌트 메시지가 다 '전환하려면 이중탭하세요.'입니다.

그런데 사실상 선택한 라디오버튼은 전환을 할 수 없습니다.

왜냐하면 라디오버튼은 토글이 아니기 때문입니다.

즉 힌트 메시지를 잘못 출력하고 있는 것입니다.

그래서 setAsRadioButton을 적용하면 선택한 라디오버튼에 초점을 두었을 때는 아무런 힌트도 읽지 않도록 했습니다.

사용법은 기존에 설명한 것과 동일합니다.

자세한 것은 관련 팁을 참고해 주세요.

VoiceOver에서 HTML title, aria-describedby 속성 처리에 관하여

Webacc NV | 2022-01-20 09:28:44

iOS 네이티브앱에서는 대체 텍스트를 제공할 경우 accessibilityLabel을 사용하며 힌트 정보가 필요할 경우 accessibilityHint 속성을 사용하여 실행 결과에 대한 메시지 등을 함께 읽도록 하고 있습니다.

그런데 VoiceOver에 한정적이긴 하지만 웹에서도 title, aria-describedby 속성은 네이티브 앱에서의 accessibilityHint와 같이 처리를 하고 있습니다.

예를 들어보겠습니다.

1. 레이블: 인기순.

2. 힌트: 정렬 방식을 변경하려면 이중탭하세요.

3. 마크업: <button title="정렬방식을 변경하려면 이중탭하세요.">인기순</button>

위와 같이 마크업할 경우 iOS 브라우저에서는 VoiceOver 힌트 읽기 기능이 켜져 있는 경우에만 title 속성으로 부여된 메시지를 읽게 됩니다.

해당 기능은 TalkBack에서는 적용되지 않으며 TalkBack에서는 힌트 메시지 온/오프와 관계 없이 해당 속성을 다 읽습니다.

[javascript] improveAccessibility.js 파일에 ariaButton 메서드 추가

Webacc NV | 2022-01-19 09:48:42

링크는 다른 목적지로 이동할 때, 버튼은 제출 또는 확장 축소와 같은 무언가의 액션을 한다는 의미를 가집니다.

그런데 버튼의 경우 부득이하게 버튼 태그를 사용하지 못하고 role button 속성을 주어 버튼으로 읽도록 하는 경우가 있을 수 있습니다.

이때는 버튼에 대한 키보드 적용을 반드시 해 주어야 합니다. 만약 <a href 요소에 롤 버튼을 주었다면 엔터로만 실행되므로 스페이스까지 실행되게 해야 할 것이고 div, span 과 같은 커스텀 태그에 롤 버튼을 주었다면 탭인덱스 0을 주는 것을 포함하여 키보드 접근성을 구현해야 할 것입니다.

따라서 ariaButton 메서드를 적용하면 지정한 컨테이너 내부의 모든 롤 버튼을 찾아 키보드 접근성 및 div, span 태그이면서 탭인덱스 0이 없는 경우 이에 대한 접근성까지 구현하게 됩니다.

기본은 document.body가 적용되어 있어서 전체 문서를 기준으로 하지만 파라미터에 특정 엘리먼트를 넣으면 그 안에서만 적용하게 됩니다.

이는 리액트나 뷰와 같은 라이브러리에서는 컴포넌트 단위로 개발이 되게 되는데 항상 문서 전체에서 적용될 경우 성능 이슈가 있을 수 있어 두 경우를 다 포함할 수 있도록 제작한 것입니다.

js 다운로드

[javascript] aria checkbox 추가 관련

Webacc NV | 2022-01-18 09:54:28

체크, 체크해제 기능을 구현할 때 input type checkbox 요소를 사용하면 접근성과 관련해서 특별한 구현 자체가 필요치 않습니다.

그러나 인풋을 디스플레이 난으로 숨기고 커스텀으로 체크박스를 구현하거나 하게 되면 키보드 사용자나 스크린 리더 사용자는 체크박스에 대한 상태 정보도 알 수 없을 뿐만 아니라 체크, 체크 해제 자체를 할 수 없게 됩니다.

이때 적용할 수 있는 ariaCheckbox() 펑션을 만들어서 추가하게 되었습니다.

1. 클릭 이벤트가 있는 요소에 role checkbox, aria-checked 초기 상태를 마크업합니다.

2. aria-label 속성을 사용하여 체크박스에 대한 레이블을 줍니다. 예: 약관 전체 동의

그 후 ariaCheckbox() 펑션을 적용하면 탭키로 커스텀 체크박스에 접근될 뿐만 아니라 스페이스로 체크 또는 체크 해제를 할 수 있게 됩니다.

js 다운로드

[자바스크립트] TalkBack 최신버전에서의 aria-pressed 속성 추가지원에 따른 ariaPressed() 함수 추가 업데이트

Webacc NV | 2021-12-31 15:43:17

2022년 2월 정도에 널리 아티클을 통해 다루겠지만 TalkBack 최신 버전을 이용하면 기존과 달리 aria-pressed 속성이 있는 요소에 포커스 해도 현재 상태, 즉 on/off 상태를 음성 출력해 줍니다.

따라서 ariaPressed 메서드가 다음과 같이 변경/업데이트 되었습니다.

1. 안드로이드에서도 aria-pressed 속성을 사용하도록 변경: 기존에는 TalkBack에서 aria-pressed 속성을 이중탭하여 속성 값이 변경되었을 때만 지원하여 role="switch", aria-checked="true/false" 속성으로 변경하였으나 해당 기능은 삭제했습니다.

2. 안드로이드에서 토글 버튼을 이중탭하여 값이 변경되는 경우 속성에 따라 켜짐, 꺼짐 음성 피드백하도록 추가: 현재는 이중탭하여 속성이 변경되면 TalkBack 자체적으로는 변경된 값을 바로 읽어주지 못하여 스크립트를 통해 읽어주도록 처리했습니다.

안드로이드에서만 적용되며 iOS 및 pc에서는 스크린 리더 자체에서 지원하므로 추가하지 않았습니다.

 

 

이 외의 업데이트 된 내용에 대해서는 지난 팁을 참고합니다.

사용 방법은 ariaPressed() 함수를 적용하는 시점에 마크업된 모든 aria-pressed 속성을 가지고 오게 되며

각 버튼을 클릭했을 때 스타일의 변화가 있으면 값이 변경됩니다.

improveAccessibility.js 다운로드

[iOS native] 같은 뷰컨트롤러에서 화면 콘텐츠 전체가 다른 콘텐츠로 교체되었을 때의 초점 관리가 중요한 이유

Webacc NV | 2021-12-30 15:30:47

접근성 테스트를 하다보면 어느순간 한 손가락 쓸기로 다음 요소를 탐색하려고 하면 화면에는 분명히 다른 요소들이 있음에도 불구하고 바로 직전의 사라진 텍스트를 읽으며 이전 혹은 다음 요소로 이동하지 못하는 것을 발견하는 경우가 있습니다.

뷰컨트롤러가 실행되어 화면에 콘텐츠가 표시되면 보이스오버는 접근성 API 에서 정보를 받아 접근성 초점이 가능한 객체들을 수집하고 이를 바탕으로 초점이 가능한 여러 요소들을 생성하게 됩니다. 

이 정보들을 기준으로 여러 제스처를 사용하여 화면의 내용을 음성으로 들을 수 있게 되는 것입니다.

그런데 화면 변경과 같은 이벤트 없이 한 화면의 전체 콘텐츠가 다른 콘텐츠로 대체되었을 때 보이스오버는 화면이 변경되었음을 인지하지 못할 수 있습니다.

그래서 스크린 리더 사용자가 화면이 변경되었음을 알지 못하는 것도 문제이지만 한 손가락 쓸기로 화면을 탐색하면 마치 아무 것도 없는 것처럼 퉁퉁 하는 보이스오버 특유의 소리만 출력하고 마지막으로 초점을 가진 요소만 반복해서 읽는 경우가 발생합니다. 

이런 경우 임의로 액정의 특정 부분을 손으로 터치하면 그때서야 보이스오버는 전체 변경된 콘텐츠 정보를 다시 생성합니다.

이런 문제가 발생하지 않게 하려면 뷰컨트롤러에서 전체 레이아웃이 변경되었을 경우에는 반드시 screenChanged 혹은 layoutChanged 이벤트를 주어서 보이스오버가 화면이 변경되었음을 인식할 수 있도록 해 주는 것이 중요합니다.

이것은 웹페이지에서도 동일합니다.

레이어 팝업이 표시되었을 때 aria-hidden 으로 기존 백그라운드 콘텐츠를 숨겼다고 가정합시다.

그리고 레이어 팝업쪽으로 초점을 보내주지 않으면 액정을 다시 터치하지 않는 이상 초점이 갇히는 증상이 발생할 수 있습니다.

[android native] 자바 및 코틀린 유틸 클래스에 커스텀 키패드 터치하여 입력 메서드 추가

Webacc NV | 2021-12-12 12:51:53

우리 나라에서는 커스텀 키패드를 사용하는 경우가 종종 있습니다.

안드로이드 11 이상에서 톡백의 설정에 따라 톡백의 고급 설정에 있는 한번 터치하여 입력하기를 활성화 한 경우 커스텀 키패드 역시 손가락을 떼면 바로 입력되도록 하는 메서드를 자바 및 코틀린 유틸 클래스 및 객체에 추가하게 되었습니다.

메서드 이름은 setAsKeyboardKey 입니다.

인자 값으로는 키보드로 사용되는 뷰 객체만 넣어 주면 됩니다.

예: a11y.setAsKeyboardKey(keyButton)

그러면 안드로이드 버전이 11 이상인지를 체크한 다음 11 이상이면 톡백의 설정에 따라 한 번 입력하는 것을 활성화 하였다면 사용자가 키보드에서 손가락을 떼면 바로 입력이 됩니다.

[자바스립트] 모바일 스크린리더의 호환성 대응을 위한 mobile.js 추가

Webacc NV | 2021-12-10 09:23:34

1. iOS 보이스오버에서 특정 상황에서의  <ul> 인식 불가 해결:

웹페이지에서 <ul> 요소를 만나면 스크린 리더 나름대로 목록 그룹으로 인식하고 이에 대한 정보를 음성 출력합니다.

그런데 iOS 보이스오버에서는 특정 상황에서 <ul> 요소가 있어도 목록으로 인식하지 못하는 경우가 있습니다.

대표적인 것이 목록 스타일이 none 으로 설정되어 있는 경우입니다.

스크린 리더 사용자 입장에서는 목록 역시 하나의 영역이며 여러 목록이 있다면 목록 단위로 이동함으로써 웹페이지에 대한 레이아웃을 조금 더 쉽게 이해할 수 있는 정보가 됩니다.

게다가 특정 목록에 aria-label 속성을 함께 주면 현재 탐색하는 목록이 어떤 영역인지도 스크린 리더 나름대로 지원해 주기도 하므로 큰 도움이 됩니다.

해당 js 를 사용하면 보이스오버에서도 어떤 페이지에서나 <ul> 요소를 목록으로 인식하며 특정 목록에 aria-label 속성을 주면 목록 간 이동 시 혹은 해당 영역을 터치할 시에 어떤 목록인지를 자동으로 읽어주게 됩니다.

2. iOS 보이스오버에서의 타이머와 같이 텍스트가 스크립트에 의해 동적으로 변경되는 요소 대응:

몇분 혹은 며칠이 남았는지를 스크립트로 구현하고 남은 시간의 텍스트가 수시로 변경되는 경우 보이스오버에서 해당 요소로 초점을 보낼 수 없거나 보내기 어려운 이슈가 있습니다.

따라서 해당 요소에 timer 라는 속성을 마크업하고 mobile.js 파일을 추가하면 자동으로 timer 라는 속성이 있는 곳에 role="progressbar" aria-valuetext 속성을 추가하여 수시로 변경되는 텍스트 쪽으로 초점을 보낼 수 있도록 할 수 있습니다.

사용법은 간단합니다.

아래 js 파일을 <head> 에 <script> 형태로 추가만 해 주시면 됩니다.

mobile.js 다운로드

[자바스크립트] ariaHidden 메서드 추가

Webacc NV | 2021-12-07 09:38:25

웹페이지 접근성을 구현할 때 아리아 히든 속성을 사용해야 하는 대표적인 경우는 모달 레이어가 표시되었을 때 화면에서 가려지는 부분을 숨기는 것입니다.

그런데 이 외에도 확장 축소되는 콘텐츠 역시 디스플레이 난, 블록 속성을 사용하지 않을 때에도 아리아 히든 속성을 사용해야 합니다.

그렇지 않으면 분명히 버튼은 축소되었는데 스크린 리더에서는 확장 축소와 상관 없이 하위 콘텐츠를 읽게 되고 그렇게 되면 페이지 구조를 파악하기 어렵기 때문입니다.

또한 하위 콘텐츠가 초점을 가지고 있는 버튼, 링크 등으로 구성되어 있다면 탭인덱스 -1 속성을 함께 주어야 합니다.

그래서 ariaHidden 메서드를 만들어 해당 팁을 통해 공유를 하게 되었습니다.

형식은 ariaHidden() 이며 다음과 같이 마크업을 합니다.

1. 아리아 히든 트루 혹은 폴스가 적용되어야 하는 컨테이너에 아이디를 부여하고 초기 값을 aria-hidden="true" 와 같이 마크업을 합니다. 페이지 로딩 시 초기 값은 반드시 존재해야 하며 해당 영역은 화면에서 보여지거나 숨겨질 때 반드시 스타일의 변화가 있는 곳이어야 합니다.

2. 해당 영역을 보이거나 숨기는 버튼에 screen-reader-hidden="적용한 id" 형식으로 연결을 합니다.

그러면 다음과 같이 동작을 합니다.

1. 페이지가 처음 로딩 되었을 때 screen-reader-hidden 과 연결된 아이디 영역이 아리아 히든 트루라면 하위 모든 초점이 가능한 요소에 탭인덱스 -1 속성이 붙어서 초점이 가지 않게 됩니다.

2. screen-reader-hidden 속성이 있는 버튼을 누르면 screen-reader-hidden 속성과 연결된 영역 혹은 클릭한 버튼 바로 상위 요소의 스타일 변경이 감지될 경우 아리아 히든 폴스 혹은 트루로 변경되고 탭인덱스 역시 트루일 경우 -1이 붙습니다.

아리아 히든 트루 폴스의 변경은 연결된 버튼을 클릭하기 전과 비교해서 판단합니다.

3. 아리아 히든 속성이 있는 컨테이너 내에서 닫기와 같은 요소를 눌러 다시 스타일이 감지되면 반대로 아리아 히든 속성이 다시 트루로, 탭인덱스 -1 속성이 추가되고 초점은 해당 영역과 연결된 기존 버튼으로 돌아오게 됩니다.

improveAccessibility.js 다운받기

[자바스크립트] aria-expanded.js 파일에 type="checkbox" role="checkbox" 확장/축소 기능 추가

Webacc NV | 2021-12-06 09:32:12

기존까지 aria-expanded.js 파일에서는 버튼, 롤 버튼에서의 확장/축소 기능에 대한 스크립트를 지원하였습니다. 

이와 더불어서 이번 업데이트에서는 체크박스에 대한 확장축소 기능을 지원하게 되었습니다.

특정 체크박스의 경우 체크되면서 하위 요소들이 표시되거나 체크 해제되면서 하위 요소들이 접히는 UI 에서의 접근성을 적용하기 위함입니다.

따라서 해당 js 파일이 포함된 상태에서 기존 체크박스에 aria-expanded 속성이 포함되어 있고 aria-controls 속성을 통해 표시되거나 숨겨지는 요소와 아이디로 연결된 경우에는 aria-controls 와 연결된 id 요소가 display none 혹은 aria-hidden true 속성으로 변경되면 aria-expanded false, 반대이면 true 로 설정되게 됩니다.

[자바스크립트] ariaPressed 메서드 기능 업데이트

Webacc NV | 2021-12-03 14:59:41

널리에서 배포한 WAI-ARIA UI example 항목 중  ariaPressed js 기능을 업데이트하여 공유하게 되었습니다.

업데이트된 내용은 다음과 같습니다.

1. 토글 버튼을 클릭했을 때 버튼이나 롤 버튼이 포함된 요소에 아무런 변화가 없을 경우에는 aria-pressed true 혹은 false 값이 변경되지 않도록 했습니다.

이는 좋아요 등의 버튼을 눌렀을 때 상황에 따라서는 로그인 등의 조건을 만족하지 않을 경우 좋아요 자체가 안 될 수 있기 때문입니다.

2. 안드로이드 디바이스 사용시에는 버튼이나 role="button" 속성이 무조건 role="switch" 로 변경되고 aria-pressed 속성 대신에 스위치 롤에서 사용해야 하는 aria-checked ture /false 값으로 변경되도록 했습니다.

이는 안드로이드에서는 톡백의 이슈로 인해 버튼에 aria-pressed 속성을 사용하면 이중탭하여 aria-pressed 값이 변경되었을 때에는 값을 읽어주지만 초점만 갔을 경우에는 현 상태를 알려 주지 못하고 있기 때문입니다. 

하지만 스위치 롤로 변경하면 전환 선택됨, 전환 해제됨 으로 읽어주게 됩니다.

사용방법은 간단합니다.

우선 마크업단에서 버튼, 타입 버튼 롤 버튼 중 하나에 페이지 로딩 시 기본으로 들어가야 하는 aria-pressed true 혹은 false 값을 넣어주고 ariaPressed 메서드를 실행하기만 하면 됩니다.

improveAccessibility.js 다운로드

[javascript] PopupFocusRepeater 스크립트 소개

Webacc NV | 2021-12-02 09:14:48

팝업이 열렸을 때 팝업 내부로 초점을 보내주고 팝업을 닫았을 때 다시 초점을 기존 팝업을 여는 버튼으로 되돌려보내주는 것을 자동화 할 수 있는 스크립트를 만들어 공유합니다.

사용 조건:

1. 팝업을 여는 버튼은 반드시 button, role button 또는 input type button이어야 합니다.

2. 모든 팝업을 여는 버튼에는 aria-haspopup dialog, aria-expanded false, aria-controls 속성이 포함되어 있어야 합니다. 대화상자 콘텐츠가 동적으로 생성되더라도 해당 콘텐츠가 표시될 때 버튼과 연결된 aria-controls 아이디 값이 포함되어 있다면 동적으로 생성되는 대화상자에도 사용할 수 있습니다.

3. 대화상자를 여는 버튼을 클릭하였을 때 반드시 해당 버튼의 aria-expanded 속성이 true로 변경되어야 하고 대화상자를 닫으면 해당 버튼의 aria-expanded 속성을 다시 false로 업데이트해 주어야 합니다.

이렇게만 해주면 나머지는 해당 js에서 처리합니다.

js가 처리하는 부분은 다음과 같습니다.

1. 버튼을 클릭할 때 aria-expanded 속성이 true로 변경되고 초점이 해당 버튼에 머물러 있으면 대화상자 영역으로 초점을 보냅니다. 이때 대화상자 영역에 tabindex 속성이 없으면 tabindex -1 속성을 주고 초점을 보냅니다.

2. 대화상자 영역에 role dialog 속성이 없으면 이를 추가합니다.

3. 안드로이드 디바이스만 제외하고 대화상자 영역에 aria-label이 없으면 버튼 레이블 또는 버튼 aria-label 속성을 가지고 와서 이를 추가합니다. 이는 안드로이드 톡백에서 대화상자 내에 aria-label, tabindex -1 속성이 있으면 대화상자 콘텐츠를 읽지 못하는 버그가 있기 때문입니다.

4. 대화상자를 닫았을 때 또는 사라졌을 때 기존 대화상자를 여는 버튼의 aria-expanded 속성이 false로 변경되면 초점을 다시 해당 버튼으로 보내줍니다.

사용의 한계: 대화상자를 닫았을 때 또는 사라졌을 때 기존 버튼 자체가 사라졌다 다시 생겨나는 경우에는 초점을 다시 되돌려줄 수 없습니다.

js 다운로드

[자바스크립트] announceForAccessibility 메서드 소개

Webacc NV | 2021-11-30 17:02:38

웹페이지에서 특정 콘텐츠가 업데이트 될 때 이를 스크린 리더 사용자에게 알려야 한다면 우리는 aria-live 또는 role="alert" 속성을 사용합니다.

하지만 특정 상황에서는 화면에 없는 메시지를 스크린 리더 사용자에게 알림 형태로 제공해 주어야 하는 경우가 있습니다.

페이지 전체가 변경되지 않는 상황에서 화면이 변경되었거나 사용자가 입력한 그 무언가가 설정되었을 때 등입니다.

iOS, android 네이티브앱에서는 화면에 보이지 않는 알림을 스크린 리더 사용자에게 제공하는 공식적인 메서드를 지원하지만 웹에는 공식적인 메서드가 없습니다.

그래서 화면에 보이지 않는 스크린 리더용 알림 메시지를 제공하기 위한 announceForAccessibility 메서드를 만들어 공유하게 되었습니다.

아래에서 js 파일을 다운받아 실제 사이트에서 호출한 다음 스크립트에서 해당 메시지를 알려야 할 시점에 announceForAccessibility("날짜 설정됨") 과 같이 추가할 수 있습니다.

그러면 화면에 보이지 않는 스크린 리더 라이브 영역을 생성하고 스크린 리더가 해당 영역을 인식했을 시점에 다시 사라지게 함으로써 알림 메시지를 자동으로 읽도록 합니다.

해당 함수는 role dialog, aria-modal true 속성이 있는 영역이 있으면 각 모달 대화상자 내에도 라이브 영역을 생성했다가 삭제합니다.

이는 일반적으로 모달 대화상자의 경우 가려진 부분이 탐색되지 않도록 접근성을 적용하게 되는데 이때 생성된 어나운스를 대응하기 위함입니다.

js 다운로드

[자바스크립트] modal.js 버그 수정 관련

Webacc NV | 2021-11-29 18:46:40

일부 모달 팝업에서 대화상자가 사라졌음에도 이를 감지하지 못하는 버그를 발견하여 이를 수정했습니다.

모달 영역으로 지정된 그 어떤 요소를 클릭했을 때 모달 속성이 디스플레이 난 처리되면 모달 관련 접근성 기능을 없애는 이벤트를 적용했는데 일부 모달에서 모달이 클릭됨을 인지하지 못하는 경우가 있어서 클릭이 아닌 MutationObserver 이벤트를 활용하여 모달의 디스플레이 난 속성을 감지할 수 있도록 했습니다.

아래의 modal.js 팁에 있는 링크를 통해 최신 파일을 내려받을 수 있습니다.

[자바스크립트] aria-expanded.js 버그 수정 관련

Webacc NV | 2021-11-29 10:17:40

얼마전 aria-expanded.js 업데이트 버전을 올려드렸습니다.

몇 가지 버그가 있어 아래에 수정 사항을 정리했습니다.

1. 기존 aria-controls 를 사용하지 않는 aria-expanded 속성에는 영향을 미치지 않도록 수정: aria-controls 속성 없이 aria-expanded 속성 처리를 하는 경우 저희가 업데이트 한 js 를 넣을 경우 에러가 발생했었습니다. 따라서 기존 aria-controls 속성이 없는 aria-expanded 속성에는 영향을 미치지 않도록 수정했습니다.

다음은 기능추가입니다.

1. aria-expanded true 로 변경되어 하위 요소들이 비미달 형태로 확장되었을 때 별도 닫기를 제공하는 경우가 있습니다. 이때 닫기를 눌렀을 때에도 aria-controls 로 지정된 요소의 디스플레이 속성을 캐치하여 none 으로 변경될 경우 aria-expanded 속성이 false 로 변경되도록 업데이트 했습니다.

[android native] 섹션별 제목 제공

Webacc NV | 2021-11-25 15:25:13

Android에서 동일한 형식에 내용만 조금씩 다른 반복된 요소를 만들 때, 가장 많이 사용하는 UI 위젯은 목록 형태의 위젯입니다. 목록 형태의 위젯은 오래전에 쓰이던 ListView와 최근에 많이 쓰이는 RecyclerView가 있습니다. 일반적으로 목록 시작 부분에 HeaderView가 제공되어 있는 경우에는 화면에서 보이는 텍스트를 스크린리더가 제목으로 인식하여 읽어주기 때문에 접근성에 문제가 없습니다.

하지만 상단에 제목이 제공되어 있지 않은 리스트의 경우 스크린 리더가 인식할 수 있는 섹션 제목을 따로 제공해 주어야 합니다. 웹에서는 ul 영역에 aria-label을 이용하여 대체 텍스트를 넣어 주는 것으로 눈에 보이지 않는 목록 타이틀을 제공할 수 있는데 이와 비슷한 방식으로 안드로이드에서는 android:contentDescription를 사용할 수 있습니다. 

contentDescription를 사용하여 리스트뷰 영역에 대체 텍스트 제목을 제공했을 경우. 해당 목록의 첫 항목으로 스크린 리더의 초점이 이동하는 동시에 contentDescription로 제공된 제목을 읽어 주게 됩니다. 이로 인해 스크린 리더 사용자는 여러 섹션으로 나뉘어진 페이지에서라도 탐색을 통해 각 섹션의 제목을 인식할 수 있게 됩니다. 

자바:
RecyclerView myRecyclerView = findViewById(R.id.MyRecyclerView)
myRecyclerView.setContentDescription("널리 포럼")

코틀린 변환:
val myRecyclerView:RecyclerView = findViewById(R.id.MyRecyclerView)
myRecyclerView.contentDescription="널리 포럼"

요소에 속성으로 포함하는 경우
android:contentDescription="널리 포럼"

[자바스크립트] modal.js 업데이트

Webacc NV | 2021-11-23 17:46:22

얼마전 널리 아티클을 통하여 모달 대화상자 접근성 적용 시 쉽게 적용이 가능한 modal.js 를 만들어 공유했습니다.

해당 js 에서 다음 사항을 업데이트 하여 글을 쓰게 되었습니다.

업데이트 내용: 대화상자가 열렸을 때 role="dialog" 속성에 aria-label 또는 aria-labelledby 속성이 없으면 대화상자를 여는 버튼의 텍스트를 aria-label 속성으로 가지고 옵니다.

따라서 대화상자의 접근성 이름을 지정하지 않더라도 대화상자 타이틀을 대화상자를 여는 버튼의 이름으로 읽어주어 '공유하기 대화상자'와 같이 음성 출력하게 됩니다.

다만 조금 더 직관적인 대화상자의 레이블 제공을 위해서는 마크업으로 대화상자의 접근성 이름을 지정해 주는 것이 좋습니다.

modal.js 다운받기

[android native] 자바 및 코틀린 유틸 클래스에 announceToast 메서드 추가.

Webacc NV | 2021-11-23 09:58:48

접근성 관점에서 무언가 일어난 일에 대해 알림을 줄 때는 톡백이 알림을 말하게 하거나 알림과 관련된 상호 작용이 함께 포함되는 경우 초점을 해당 요소로 보내주는 것으로 접근성 대응을 하게 됩니다.

안드로이드에서 Toast, SnackBar 클래스를 사용하여 화면에도 보이는 알림을 제공할 때에는 톡백에서 이를 자동으로 읽어주므로 문제가 되지 않지만 화면상에 보이지 않는 알림을 줄 때는 announceForAccessibility 메서드를 사용합니다.

그런데 announceForAccessibility 메서드에서 알림을 주려면 무언가 화면에 있는 뷰를 참조해야만 합니다.

만약 특정 상황에서 참조할 뷰가 없다면 announceForAccessibility 메서드를 사용할 수 없으며 type_announcement 라는 sendAccessibilityEvent 를 이용해야 하고 이때 여러 줄의 추가 코드가 필요합니다.

따라서 어느 상황에서나 단 한 줄의 코드로 톡백 사용자에게 알림을 제공하는 메서드를 만들어 공유하게 되었습니다.

자바 및 코틀린에서 모두 사용 가능하고 해당 메서드는 announceToast 입니다.

인자 값으로는 context, 알림을 보내고자 하는 스트링 두 개가 들어갑니다.

context 가 필요한 이유는 해당 이벤트는 AccessibilityManager 클래스를 활용하는데 해당 클래스를 사용하려면 context 객체가 필요하기 때문입니다.

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

AccessibilityUtil.announceToast(getApplicationContext(), "This is a test announcement.");

[자바스크립트] aria-expanded.js 업데이트

Webacc NV | 2021-11-22 17:34:28

널리에서 얼마전 WAI-ARIA UI를 조금 더 간편하게 적용할 수 있는 라이브러리를 공개했습니다.

해당 라이브러리 중 aria-expanded.js 부분의 접근성 기능을 조금 업데이트 하여 팁으로 가지고 나오게 되었습니다.

aria-expanded.js 특징은 특정 버튼이나 role="button" 에 true 혹은 false aria-expanded 속성이 있으면 해당 버튼을 누를 때마다 마크업된 속성을 기반으로 true 이면 false 로, false 이면 true 로 변경되는 기능을 합니다.

해당 js 에서 업데이트 된 사항은 다음과 같습니다.

1. 참조하는 aria-controls id 요소가 display none 혹은 block 으로 변경되는지를 확인합니다.

특정 버튼은 눌렀다고 해서 무조건 확장 혹은 축소되지 않고 현 상태가 유지될 수 있습니다.

따라서 실제로는 확장되지 않았는데 aria-expanded 속성이 true 로 변경된다면 오히려 사용자에게 혼란을 주기 때문에 

참조하는 aria-controls 아이디 요소의 display 상태를 체크하도록 했습니다.

따라서 마크업할 때 aria-controls 는 반드시 display block, none 처리되는 요소와 연결해야 합니다.

2. a 버튼을 확장한 상태에서 b 버튼을 다시 확장하면 이전에 확장된 a 버튼이 축소되는 구조들이 많습니다. 

따라서 버튼을 클릭하여 true 상태가 되면 다른 aria-expanded 속성과 aria-controls 와연결된 요소들을 검사하여 display none 속성으로 변경된 요소가 있으면 해당 버튼 속성도 false 로 변경합니다.

업데이트된 aria-expanded.js 다운로드

[android native] 자바 및 코틀린 라이브러리에 setAsIgnoreSelected 메서드 추가

Webacc NV | 2021-11-18 10:22:01

접근성 테스트를 하다보면 선택됨의 상태정보를 가지고 있지 않음에도 선택됨 요소를 함께 읽어주는 경우를 보곤 합니다.

안드로이드에서는 selected true 속성을 통해서 선택됨 상태를 체크하게 되는데 실제 선택됨 상태정보를 가지고 있지 않은 요소에도 개발의 편의성을 위해서 selected 속성을 사용하는 경우가 있습니다.

알림 보기 아이콘이 있을 경우 알림이 있을 때는 selected true 로 설정되어 있는 것이 하나의 예입니다.

톡백에서는 selected true 상태이면 무조건 선택됨 정보를 읽기 때문에 스크린 리더 사용자에게 혼란을 주는 경우가 있습니다.

이를 간편하게 해결하기 위하여 필요시 selected 속성을 톡백에서 읽지 않도록 하는 메서드를 추가하게 되었습니다.

자바 혹은 코틀린 접근성 유틸 라이브러리를 아래 팁을 참고하여 추가한 다음 setAsIgnoreSelected 메서드를 사용하면 됩니다.

인자 값으로는 대상 뷰 객체가 들어갑니다.

예시: AccessibilityUtil.setAsIgnoreSelected(notiView);

[HTML 접근성 리마인드]상태/선택정보, 힌트를 제공할 때 title속성에 의존해서는 안 되는 이유

Webacc NV | 2021-11-16 16:32:57

사용자가 많건 적건, 대한민국에서 사용되는 대표 스크린리더는 총 세가지로, 국산 스크린리더인 엑스비전테크놀로지의 센스리더와 NVAccess의 파이썬 기반 오픈소스 스크린리더 NVDA(Non Visual Desktop Access), Freedom Scientific의 유로 스크린리더 JAWS(Job Access With Speech) for Windows가 있습니다. 그 중 센스리더는 한국인을 위해, 한국 소프트웨어/웹 환경에 맞춰서 제작된 대한민국의 스크린리더로, 뒤에 설명한 외산 스크린리더 두 종류와는 다른 부분 많이 있습니다. 대표적인 사례가 title 속성이라고 할 수 있습니다.

센스리더에서 title은 Tab키로 객체를 직접 탐색하거나, 가상커서(위/아래 화살표키)로 탐색하거나 모두 같은 결과를 보여줍니다.

센스리더는 이 두가지 동작 시에 요소, 유형, 상태, 타이틀, 이 모두를 읽습니다. 장점이라고 할 수도 있으나, 우리나라에서는 Sense Reader를 기준으로 접근성을 테스트하기 때문에 문제시되지 않고, 특정 웹사이트에서는 title로 선택정보나 힌트, 상태정보와 같은 스크린리더 사용자에게 전달되야 하는 요소의 중요 정보를 제공했었습니다.

센스리더의 가상커서로 선택정보가 title로 제공된 링크를 탐색하여 음성 출력 내용을 표시함. 화살표키로 이동해도 선택됨이라는 타이틀을 잘 읽음.

그럼, 다른 스크린리더는 어떨까요? 우리가 쉽게 접할 수 있는 NVDA를 보면, Tab키로 이동 시에는 이 title을 들을 수 있으나, 가상커서(NVDA에서는 브라우즈 모드)로 탐색할 때는 title정보를 읽지 않습니다. 매우 중요한 정보가 있음에도 듣지 못하는 것이지요. 아래와 같이 말이죠.

title로 선택정보를 제공한 링크의 가상커서 탐색을 음성 출력 뷰어로 본 모습, NVDA에서는 title을 읽지 못함

NVDA나 JAWS등 외산 스크린리더에서는 링크에 aria-current라는 속성을 사용하여, 현재 링크, 현재 페이지등을 읽게끔 할 수 있으나, 센스리더는 아직은 이 기능일 지원하지 않는 문제가 있습니다.

그래서 현재로서 두 스크린리더를 만족시킬 수 있는 가장 최선의 방법은 ir기법을 사용하여, 링크 안에 스크린리더로만 들을 수 있는 보 하지만, 이것 또한 만능은 아닙니다. 링크가 아닌 버튼 등을 눌렀을 때, 바로 바로 변경된 텍스트를 알려주지는 않기 때문이지요.  aria-label을 사용하는 것 또한 방법이기는 하나, 부가레이블을 제공하는 것이 아니라, 텍스트를 덮어 씌우는 속성 특성상, 꺼림직한 부분이 있습니다.

다만, 링크 요소만큼은 이 방법이 최선이라고도 할 수도 있습니다. 링크를 누르면 필연적으로 페이지를 다시 불러오게 되고, 사용자는 다시 해당 영역을 탐색해야 하기에 스크린리더가 부가정보를 업데이트하여 읽을 필요성이 없기 때문입니다.

최근에도 아직 많은 사이트에서 title에 선택정보를 제공하고 있음을 보았습니다. 그래서 이번 시간에는 리마인드 차원에서 title 사용에 주의하자는 내용으로 팁을 작성합니다.

아래는 스크린샷에 사용된 마크업입니다.

<!DOCTYPE HTML>
<html lang="ko">

<head>
<title>Document</title>
...
<style>
.mark{
    display:none;
}
.selected .mark{
    display:inline;
    color:red;
}
</style>
</head>

<body>
  <p><a href="#">page1</a></p>
  <p><a href="#" title="선택됨" class="selected"><span class="mark" aria-hidden="true">></span>page2</a></p>
  <p><a href="#">page3</a></p>
</body>

</html>

[iOS native] 컨테이너 단위 접근성 포커스 이동순서 재조정과 vertical scroll bar 이슈

Webacc NV | 2021-11-16 15:01:18

얼마전 중첩된 레이아웃에서의 접근성 순서 재조정에 대해 다룬 적이 있습니다.

한 화면의 레이아웃 접근성 초점이 틀어져서 뷰컨트롤러 자신의 accessibilityElements 배열에 각각의 컨테이너들을 레이아웃 순서에 맞게 배열할 경우 대부분 의도한대로 접근성 포커스 순서가 올바르게 조정됩니다.

그런데 기획에 따라 달라질 수 있겠지만 TableView 내에 수직 스크롤막대가 표시되는 경우 접근성 초점 순서를 재조정했을 때 수직 스크롤막대가 중간에 끼어들어가는 경우에는 한 손가락 쓸기를 해도 다음 요소로 이동이 되지 않는 버그가 있습니다.

예를 들어보겠습니다.

하나의 화면에 3개의 컨테이너가 있고 상단 헤더뷰, 중간 테이블뷰, 하단 푸터뷰가 있다고 가정해 봅시다.

보이스오버 순서가 중간, 상단, 하단으로 이동하는 문제가 있어 accessibilityElements 배열에 헤더, 테이블, 푸터뷰 순서로 초점 순서를 재조정했습니다.

그런데 테이블뷰 끝에 수직 스크롤막대가 있을 경우에는 푸터뷰에 있는 요소쪽으로 수직스크롤막대에서 한 손가락 오른쪽 쓸기를 해도 이동을 하지 않는다는 것입니다.

신기한 것은 반대로 푸터에 있는 첫 번째 요소에서 한 손가락 왼쪽쓸기를 하면 수직스크롤막대로 정상 이동이 됩니다.

접근성 테스트 시 참고하시길 바라며 해당 이슈는 애플에서도 알고 있는 이슈로 빠른 시간 내에 해결되길 바라봅니다.

 

[android native] 자바 및 코틀린 접근성 적용 라이브러리에 setAsEditTextHint 메서드 추가

Webacc NV | 2021-11-15 17:28:38

EditText 내에 화면상으로 보여지는 레이블 메시지를 추가할 수 없을 때 톡백에서만 인식 가능한 레이블 메시지를 쉽게 추가할 수 있도록 메서드를 추가했습니다.

setAsEditTextHint 메서드 안에 대상 뷰와 힌트 메시지 텍스트만 넣어 주면 됩니다.

예시: accessibilityUtil.setAsEditTextHint(editText, "이름");

유틸클래스 자바 다운로드

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

[android native] 자바 및 코틀린 접근성 적용 라이브러리에 드롭다운 요소 유형 추가

Webacc NV | 2021-11-15 11:07:29

안드로이드에서 Spinner 위젯을 사용하면 톡백에서 드롭다운 즉 웹으로 치면 콤보상자라고 읽어주게 됩니다.

그러나 Spinner 위젯을 사용하지 않고 커스텀으로 사용하였을 경우 접근성을 적용하여 이를 드롭다운으로 읽어줄 수 있도록 setAsDropdown 이라는 메서드를 추가하였습니다.

옵션을 변경하는 요소의 경우에는 드롭다운으로 적용할 경우 톡백의 기본 힌트 메시지도 변경하려면 이중탭하세요 로 출력되므로 스크린 리더 사용자에게 조금 더 직관적인 피드백을 줄 수 있습니다.

사용방법은 setAsDropdown 안의 인자 값에 해당 뷰 객체명만 넣어주면 됩니다.

유틸클래스 자바 다운로드

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

[android native] 접근성 적용 라이브러리에 sendToFocusView 메서드 추가(자바 및 코틀린)

Webacc NV | 2021-11-11 10:50:19

접근성 초점을 특정 뷰로 보내야 할 때 안드로이드에서 흔히 sendAccessibilityEvent 메서드를 사용하도록 안내했습니다.

그런데 해당 메서드는 특정 상황에서 작동이 안 됩니다.

1. 해당 이벤트가 적용되어 있으나 스마트폰 볼륨키를 눌러 화면 레이아웃이 변경되었을 때.

2. importantForAccessibility 메서드를 이용하여 모든 뷰들을 접근성에서 숨겼다가 다시 나타나게 했을 때.

따라서 이런 모든 상황에 영향을 받지 않게 하려면 performAccessibilityAction 메서드를 초점을 보내고자 하는 뷰에 적용해야 합니다.

따라서 이를 좀 더 간단하게 적용할 수 있는 메서드를 유틸 클래스 및 객체에 추가하게 되었습니다.

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

특정 뷰로 초점을 보내고자 하는 시점에 sendToFocusView 메서드를 적용하며 인자 값에는 대상 뷰 객체명만 넣어주면 됩니다.

예시: 

AccessibilityUtil.sendFocusThisView(filterButton);

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

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

[android native] 확장 축소 라디오버튼 적용 유틸 추가

Webacc NV | 2021-11-10 12:52:15

현재 업데이트 하고 있는 안드로이드 접근성 적용 유틸 자바 및 코틀린 라이브러리에 라디오버튼 확장축소 메서드가 추가되었습니다.

라디오버튼이 체크되는 동시에 하위에 또 다른 요소들이 생겨나거나 해제되면서 축소되는 기능을 가진 요소에 사용이 가능합니다.

메서드는 expandCollapseRadioButton 이며 인자값에는 뷰 객체와 isChecked 조건 변수가 들어갑니다.

라디오 버튼이 체크되면서 확장될 때 대상 뷰의 selected 속성이 트루로 설정되고 축소될 때 flase 로 설정된다면 한번 적용하는 것만으로 추가 접근성 적용은 필요치 않습니다.

그러나 그렇지 않다면 체크되면서 확장되거나 체크 해제되면서 축소될 때 isCheck 변수를 true false 로 변경해 주어야 합니다.

이로써 지금까지 사용 가능한 메서드로는 버튼, 라디오버튼, 탭, 체크박스, 토글버튼, 확장축소버튼, 확장축소라디오버튼, 톡백 실행여부 체크, 실행하려면 이중탭하세요 힌트 없애기 입니다.

유틸 클래스 자바 다운로드

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

[android native] 버튼 확장 축소 자바 및 코틀린 util 클래스 추가

Webacc NV | 2021-11-09 10:36:40

현재 자바 및 코틀린 접근성 적용 라이브러리를 꾸준히 업데이트 하고 있습니다.

오늘은 어제에 이어 확장 및 축소 버튼을 적용하였습니다.

특정 요소를 이중탭했을 때 하위 항목이 확장되거나 축소되는 버튼에서 적용이 가능합니다.

적용 방법은 다음과 같습니다.

1. 다음 예시와 같이 확장 혹은 축소가 적용되는 메서드를 해당 뷰에 추가합니다.

AccessibilityUtil.expandCollapseButton(fruitButton, false);

확장되었을 때 대상 뷰에 selected 속성이 추가된다면 별도의 작업이 필요 없습니다.

그러나 해당 속성이 없다면 다음 예시와 같이 true false 속성을 상황에 맞게 변경해 주어야 합니다.

AccessibilityUtil.expandCollapseButton(fruitButton, isFruitContainerExpanded);

코틀린 역시 코틀린 형식으로 객체를 만들어 사용할 수 있습니다.

해당 메서드를 사용하게 되면 톡백에서 확장 축소 상태를 읽어주며 확장 축소에 대한 커스텀 액션이 자동 추가됩니다.

유틸 클래스 자바 다운로드

코틀린 라이브러리 다운로드

[iOS native] frequentlyUpdated accessibilityTraits 접근성 구현 시 참고사항

Webacc NV | 2021-11-08 16:16:36

특정 요소에 보이스오버 포커스가 머무르고 있는 동안 퍼센트가 변경되거나 정보가 수시로 업데이트 되어 중간에 보이스오버 사용자가 이를 바로바로 피드백 받을 수 있도록 하기 위해 사용하는 속성이 바로 frequentlyUpdated accessibilityTraits 입니다.

그런데 해당 속성을 적용하더라도 텍스트가 변경될 때마다 바로바로 음성 피드백을 주는 용도로는 사용할 수 없습니다.

이는 해당 속성은 텍스트 정보가 업데이트 될 때마다 읽어주는 것이 아니라 몇 초 간격으로 중간중간에 상황 피드백을 하는 용도이기 때문입니다.

따라서 만약 보이스오버 초점이 머무르고 있는 동안 특정 텍스트가 변하는 것을 모두 읽어주도록 구현해야 한다면 UIAccessibilityPostNotification .announcement 이벤트를 사용해야 합니다. 

[android native] 접근성 util 클래스에 isTalkBackOn, removeClickHintMsg 메서드 추가

Webacc NV | 2021-11-08 13:09:01

위에서 소개한 안드로이드 접근성 적용 클래스 라이브러리에 톡백 감지 및 활성화 하려면 이중탭하세요 라는 클릭 힌트 메시지 없애는 메서드를 추가하였습니다.

톡백 탐지는 바로 위 톡백 탐지 링크를 참고하셔서 메서드를 활용하시면 됩니다.

removeClickHintMsg(View view): 클릭 리스너 자체를 없애는 것이 가장 좋지만 그렇지 못할 경우 인자 값에 없애고자 하는 뷰를 넣어 주면 활성화 하려면 이중탭하세요 라는 힌트 메시지가 제거됩니다. 

그러나 가능하다면 클릭 속성 자체를 제거하시는 것이 좋습니다.

유틸 클래스 자바

유틸 클래스 코틀린

[android native] AccessibilityUtil 자바 클래스 소개

Webacc NV | 2021-11-06 13:31:10

이틀전 공개해 드린 AccessibilityUtil 코틀린 객체에 이어서 오늘은 자바 플랫폼에서 사용할 수 있는 유틸 클래스를 소개하려고 합니다.

코틀린과 마찬가지로 현재는 버튼, 라디오버튼, 체크박스, 토글버튼, 탭 요소에 적용 가능하고 여러 다른 요소들을 업데이트 할 예정입니다.

다음과 같이 적용을 합니다.

1. 접근성 적용 자바 파일을 다운받아 프로젝트에 적용합니다.

자바 파일의 경우 브라우저에서 링크를 누르면 파일이 다운받아지지 않고 자바 파일을 텍스트 형태로 보여주게 되므로 팝업키나 오른쪽 마우스를 눌러 다른 이름 저장을 눌러 다운로드 합니다.

프로젝트에 포함을 시킬 때는 가장 상단에 있는 패키지 네임은 실제 프로젝트 패키지 네임으로 변경해 주어야 합니다.

2. 이미지뷰, 텍스트뷰, 레이아웃과 같은 객체에 다음과 같은 접근성 적용이 가능합니다.

setAsRadioButton, setAsButton setAsTab, setAsToggleButton, setAsCheckbox.

3. 괄호 안 인자 값으로는 대상 뷰 객체가 기본으로 들어가며 버튼을 제외한 모든 요소에는 true 혹은 false 속성이 함께 들어갑니다. 이는 해당

요소들은 선택됨 혹은 선택안됨 정보를 가지고 있기 때문입니다.

예시: AccessibilityUtil.setAsTab(meat, false);

참고: 대상 뷰는 반드시 텍스트 혹은 대체 텍스트 정보가 들어가 있는 뷰여야 합니다.

4. 선택됨, 선택안됨 상태는 대상 뷰의 isSelected true/false 값으로 상태가 변경된다면 별도의 작업이 필요 없습니다.
그러나 대상 뷰에 isSelected 속성이 없다면 속성을 추가하거나 상태정보가 변경되는 시점에 앞에서 가지고 온 객체의 true/false 값 변경 상태를 추가해 주어야 합니다.

[Android native] 접근성 적용을 통해 스크린 리더 사용자에게 알림 제공하기

Webacc NV | 2021-11-05 12:10:51

스크린 리더를 통해 주기적으로 업데이트가 되는 영역의 정보를 탐색하여 가져와야 하는 경우, 해당 방식에 대한 접근성이 없어 번거로운 경험을 하게 되는 일이 많습니다. 사용자가 일일이 재탐색하지 않고 영역이 업데이트될 때마다 스크린 리더가 그 내용을 읽어 주는 기능을 접근성에서 넣어 줄 수 있습니다. android에서는 accessibilityLiveRegion을 이용할 수 있습니다. 웹에서 흔히 알고 있는 aria-live의 특성과 비슷합니다. 한 영역을 라이브 영역으로 지정해 놓고, 그 영역에서 동적으로 변경되는 내용이 있을 때마다 스크린 리더가 해당 내용을 읽어 줍니다. 즉 accessibilityLiveRegion의 경우, 시각적으로 변경된 콘텐츠, 텍스트의 상태를 실시간으로 읽어 주는 역할을 합니다. 

또 다른 형태의 알림 제공 방식으로 announceForAccessibility가 있습니다. accessibilityLiveRegion와는 다르게 시각적으로 보이는 콘텐츠를 그대로 읽어 주는 게 아닌 레이아웃 변경 상태를 스크린 리더 사용자에게 직접적인 메시지를 통해 알려 줍니다. 저울에서, 중량을 표시해 주는 숫자 텍스트 영역을 라이브 영역으로 지정해 두었다고 합시다. accessibilityLiveRegion는 해당 저울에 올라간 물건이 바뀔 때마다 변하는 중량을 읽어 줍니다.  announceForAccessibility의 경우 저울에 무언가 올라가 있다, 혹은 내려가 있다, 구체적으로는 어떤 물건이 어떻게 올라가 있는지까지 등 변화하는 환경의 사실을 알려 주는 용도로 쓰이게 됩니다. 두 메서드를 적절히 이용한다면 접근성에 있어 좋은 해결 방식이 될 수 있습니다. 

[android native] 코틀린 접근성 적용을 위한 util 클래스, AccessibilityKotlin.kt 소개

Webacc NV | 2021-11-04 17:33:51

안드로이드 접근성 적용 시 커스텀 컨트롤에 대한 요소 유형 및 상태정보를 조금 더 쉽게 제공할 수 있도록 메서드만 넣어 주면 관련 속성을 자동으로 만들어주는 코틀린 유틸 객체를 만들어 공유하게 되었습니다.

현재는 버튼, 라디오버튼, 체크박스, 토글버튼, 탭 요소에 적용 가능하고 여러 다른 요소들을 업데이트 할 예정입니다.

다음과 같이 적용합니다.

1. 접근성 적용 코틀린 파일을 다운받아 프로젝트에 포함시킵니다. 

가장 상단에 있는 패키지 네임은 실제 프로젝트 패키지 네임으로 변경해 주어야 합니다.

2. 실제 변경해야 하는 요소가 있는 코틀린 파일에서 다음 예시와 같이 AccessibilityKotlin 객체를 만듭니다.

val radio1 = AccessibilityKotlin

3. 2번에서 만든 객체를 통해서 텍스트뷰, 이미지뷰, 레이아웃뷰와 같은 요소들에 다음과 같이 접근성 적용이 가능합니다.

setAsRadioButton, setAsButton setAsTab, setAsToggleButton, setAsCheckbox.

4. 괄호 안 인자 값으로는 대상 뷰 객체가 기본으로 들어가며 버튼을 제외한 모든 요소에는 true 혹은 false 속성이 함께 들어갑니다. 이는 해당 요소들은 선택됨 혹은 선택안됨 정보를 가지고 있기 때문입니다.

예시: radio1.setAsRadioButton(button1, false)

참고: 대상 뷰는 반드시 텍스트 혹은 대체 텍스트 정보가 들어가 있는 뷰여야 합니다.

5. 선택됨, 선택안됨 상태는 대상 뷰의 isSelected true/false 값으로 상태가 변경된다면 별도의 작업이 필요 없습니다.

그러나 대상 뷰에 isSelected 속성이 없다면 속성을 추가하거나 상태정보가 변경되는 시점에 앞에서 가지고 온 객체의 상태를 변경해 주어야 합니다.

[리마인드] 접근성 관점에서 iOS의 뷰컨트롤러, 안드로이드에서의 액티비티

Webacc NV | 2021-11-03 12:57:55

아래에 다루는 내용은 예전에 팁을 통해 이미 다룬 적이 있으나 접근성에서 중요한 비중을 차지할 뿐만 아니라 리마인드 차원에서 다시 한번 정리할 필요성이 있다 판단되어 해당 글을 게시하게 되었습니다.

액티비티, 뷰컨트롤러는 스크린 리더 사용자에게 화면 전환에 대한 정보를 주는 역할을 합니다.

뷰컨트롤러가 변경되면 보이스오버에서 자체적으로 다른 화면으로 전환되었다는 사운드를 재생시킵니다.

액티비티가 변경되면 톡백에서 각 액티비티 타이틀을 읽는 동시에 사운드를 출력함으로써 다른 화면으로 전환되었음을 알립니다.

위의 전제를 가지고 생각했을 때 접근성 관점에서 다음과 같이 정리를 할 수 있겠습니다.

1. 화면이 전환되는 경우에는 다른 뷰컨트롤러, 액티비티 사용을 권장한다.

2. 안드로이드에서 액티비티 타이틀은 화면의 표시 유무와 관계 없이 각 화면의 제목을 적절하게 삽입한다. 액티비티 레이블은 매니페스트의 activity)label 혹은 액티비티 클래스 내에 setTitle 메서드를 사용하여 삽입할 수 있다.

[iOS native] 테이블뷰 하나의 셀 내의 여러 텍스트를 각각의 초점으로 제공해 주어야 할 때

Webacc NV | 2021-11-01 14:25:31

하나의 테이블뷰 셀에 여러 콜렉션뷰들이 있고 각 콜렉션뷰 안에는 UILabel 텍스트가 있다고 가정해 봅시다.

지난 팁에서 공유한 바와 같이 한 셀에 여러 텍스트가 있으면 하나의 초점으로 제공될 것입니다.

이는 콜렉션뷰 자체는 보이스오버의 접근성 초점과는 의미가 없기 때문입니다.

그런데 각 텍스트가 버튼이 아니라 별개의 텍스트임에도 별도로 접근성 초점을 제공해야 하는 경우 어떻게 해야 할까요?

방법은 간단합니다.

해당 셀의 accessibilityElements 를 셀이 아닌 콜렉션뷰로 변경해 주면 됩니다.

예: self.accessibilityElements = [self.collectionView]

이렇게 되면 보이스오버의 접근성 초점은 셀이라는 것을 거치지 않고 바로 콜렉션뷰로 가게 되므로 각 텍스트의 초점이 별도 분리됩니다.

[android native] 맥락상 분리되어야 하는 TextView 초점이 하나로 합쳐져 있을 때 점검사항

Webacc NV | 2021-11-01 09:32:58

접근성 진단을 하다보면 분명히 텍스트뷰는 두 개인데 초점이 하나로 합쳐져서 맥락상 맞지 않는 경우를 발견하는 때가 있습니다.

예를 들어보겠습니다.

가는날, 11월 1일, 오는 날, 11월 2일 이라는 요소들이 있고 쉼표를 기준으로 뷰들이 나누어져 있다고 한다면 총 뷰들은 4개 입니다.

각 날짜는 이중탭하여 날짜 변경이 가능한 뷰로 되어 있다고 가정하겠습니다.

그런데 톡백으로 탐색해 보면 가는날 오는날, 11월 1일, 11월 2일과 같이 두 개의 텍스트뷰가 하나이 초점으로 탐색되는 경우가 있습니다.

이때 우리는 해당 오류 내용을 분석하기 위해 디버깅을 하게 되는데 두 가지 경우를 유심히 볼 필요가 있습니다.

1. 4개의 뷰를 감싸고 있는 상위 뷰에 focusable 속성이 true 로 되어 있거나 클릭 리스너가 포함되어 있지 않은지: 상위의 focusable 속성이 true 이거나 클릭 리스너가 있으면 하위에서 별도 포커스 및 클릭 속성이 없는 요소들은 하나로 통합됩니다. 만약 1번의 경우라면 이 문제를 해결하기 위해 상위 뷰의 클릭, focusable 속성을 제거해야 합니다.

2. onCreateViewHolder 메서드를 사용하여 해당 영역을 뷰그룹으로 묶은 경우: 흔히 목록뷰와 같은 RecyclerView, ListView 등에 많이 사용되는 방법으로 이런 경우에도 클릭, focusable 속성이 별도로 없는 텍스트뷰는 초점이 하나로 합쳐지게 됩니다. 만약 이 상황에서 초점을 분리시키려면 각 텍스트뷰에 focusable true 속성을 주어 마치 다른 포커스를 가진 뷰처럼 만들어 주어야 합니다. 다만 이렇게 하면 하드웨어 키보드로 탐색 시 탭키를 눌렀을 때 각 텍스트뷰가 포커스 되는 문제는 있겠습니다.

[android native] 접근성 포커스 캐치하여 SeekBar 볼륨키로 조절 가능하게 하기

Webacc NV | 2021-10-30 18:01:43

2020년, 오류 유형별 모바일 접근성 해결방안을 통하여 안드로이드 뮤직 플레이어에서의 SeekBar 접근성 개선 방안에 대해 설명한 적이 있습니다.

슬라이더를 구현할 때 onStopTrackingTouch 이벤트가 감지되어야 슬라이더가 조절되도록 개발되는 경우가 대부분이므로 이에 대한 접근성을 해결하기 위해 접근성 서비스가 감지되었을 때에는 해당 메서드 없이도 슬라이더를 조절할 수 있게끔 하는 방법이었습니다.

그런데 접근성 서비스가 켜져 있는지를 감지하는 것보다 조금 더 효율적인 방법이 있는데 바로 접근성 초점을 캐치하는 것입니다.

접근성 서비스 실행 여부를 캐치하는 것도 좋은 방법이지만 실행 여부만 캐치할 경우 액티비티가 실행된 이후에 접근성 서비스가 켜지는 경우에는 해당 메서드 반영이 되지 않습니다.

예를 들어 저시력 사용자가 뮤직 플레이어 화면에 들어와서 톡백을 실행하는 경우에는 접근성 서비스 실행 시 구현된 이벤트가 동작하지 않는다는 의미입니다.

물론 접근성 서비스 실행 여부와 더불어 실시간으로 접근성 서비스가 켜지거나 꺼지는 것을 감지하는 accessibilityStateChangeListener 이벤트를 함께 사용할 수도 있지만 이렇게 되면 그만큼 코드가 길어지게 됩니다.

따라서 톡백의 초점이 해당 슬라이더에 있을 때에는 onStopTrackingTouch 이벤트를 적용하지 않도록 하는 방법이 훨씬 코드 양도 줄이고 톡백을 언제 실행하더라도 바로바로 적용되므로 효율적입니다.

            public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
                isFromUser = b;
                value1 = i;
                value2 = b;
                if (mSeekBar.isAccessibilityFocused()) {
                    if(mPlayer!=null && value2){
                        mPlayer.seekTo(value1*1000);
                    }
                }
            }

 

[HTML5 접근성 기본 리마인더] 시멘틱 영역 유형(랜드마크)와 레이아웃

Webacc NV | 2021-10-28 09:13:32

오래전에 WAI-ARIA 아티클에서 랜드마크에 대해 다룬 적이 있습니다. 하지만, 호랑이 담배 피던 시절처럼 너무 오래된 것 같습니다.

이번 팁은 랜드마크에 대해 다시 짧게 알아보는 시간을 가지도록 하겠습니다.

웹에도 랜드마크가 있어요? 랜드마크가 뭐예요?

랜드마크, 상징적인 건물이나 조형물, 환경, 기타 등등, 지역이나 공간을 대표하거나, 정체성 중 하나인 요소를 말합니다.

어떤 것들을 랜드마크라고 부르시는지는 대부분 아시겠지만, 미국에 가면, 자유의 여신상, 더 좁게는 뉴욕에 가면, 멘헤튼의 엠파이어 스테이트 빌딩이 떠오릅니다. 

서울하면, 경북궁같은 조선시대 궁궐, 현대에 와서는 63빌딩이나 롯데타워 같은 높아서 유명한 건물들도 랜드마크죠. 경주하면 불국사의 역사적인 여러 명소와 건축물이 떠오릅니다.
이렇게, 특정 지역 하면 수학공식처럼 떠오르는 상징적인 것을 랜드마크라고 합니다.

HTML5 기반의 웹에도 이런 상징적인 역할을 부여하는 랜드마크 요소가 있습니다.

웹 랜드마크, 어떤 것이 있나요?

우리가 웹에서 가장 먼저 보게 되는 것부터 얘기하도록 하지요.

header 태그는 일반적으로 웹페이지의 대 제목(h1) 로고가 들어갑니다. Windows나 Mac의 창 위에 제목 막대가 있는 것처럼 말이죠.
그리고, 간편하게 사용자가 자주 사용하는 기능, 링크 등을 노출해 놓기도 합니다.

WAI-ARIA 명세에서는 role="banner"을 사용하여 header 태그를 대신하는 div를 만들 수 있습니다. 우스갯소리지만, role 명칭이 banner로 돼 있어서, 간혹 국내에서 캐러셀과 혼동하기도 합니다.

다음은 nav태그입니다. 높은 확률로 header 태그 다음으로 보게되는 영역입니다. navigation의 앞 세자로 줄여서 만든 태그로, 사이트를 탐색하는 링크들을 모아놓는 역할을 합니다.

GNB, LNB라는 약자를 보신 적 있거나, 사용하시고 계실 것입니다. 이는 Global Navigation Bar와 Local Navigation Bar의 약자로, div가 아니라 nav태그를 사용하면 조금 더 영역에 의미를 부여할 수 있고, 
스크린 리더 사용자는 랜드마크로 활용할 수 있게 됩니다.

그 다음 주로 나오는 것은 main태그이며, 본문 영역을 나타냅니다. role 또한 동일한 role="main"값을 사용하며, 맨 마지막으로 페이지의 맨 아래에 배치되는 회사 정보 영역은 footer 태그를 사용하며 role으로는 role="contentinfo"로 푸터 영역임을 나타낼 수 있습니다.

이외에도 독립적인 각각의 글에 사용되는 article 태그(role="article")가 있으며 대표적으로 페이스북의 각 포스팅이 이 article 영역으로 이루어져 있습니다. 그리고 연속되고 연관된 내용을 영역으로 나누는 section(role="region") 태그가 있습니다(예: 각 책의 장, 절 등. 또는 카테고리 레이아웃 ).

section 태그 혹은 role="region" 랜드마크는 반드시 aria-label 혹은 aria-labelledby 속성으로 영역 이름에 대한 레이블을 지정해 주어야합니다.

이 외에도 보조 영역을 표시할 때 사용하는 role="complementary", 검색 영역임을 알려줄 때 사용하는 role="search" 랜드마크가 있습니다.

이렇게, 기존에 div의 클래스만으로 구분해 놓았던 것들을 시멘틱 태그로 교체하거나, 시맨틱 태그를 대체하는 role만을 제공하더라도 스크린리더 사용자에게는 페이지를 이해하는 데에 많은 도움이 됩니다. 레이아웃 구성 시 시멘틱 태그를 잊지말고, 사용하는 것을 실천합시다.

[iOS native] 보이스오버 초점이 테이블뷰 내부에 있을 때의 데이터 재구성 관련

Webacc NV | 2021-10-27 09:40:10

테이블뷰 내부의 요소중 하나를 탭하여 실행하면 테이블뷰 전체의 데이터를 재구성(reload)하는 경우가 있습니다. 

이 때 보이스오버 초점이 이중탭한 요소에 머무르지 못하고 다른 요소로 초점이 튀어버리는 문제가 발생합니다. 

따라서 이런 경우 아래 코드 예시와 같이 보이스오버 초점이 실행 가능한 요소에 있을 때는 reloadData 메서드 대신 reloadItems 메서드를 사용하여 데이터가 변경되는 요소만 재구성하는 것이 필요합니다.

해당 이슈에 대한 자세한 설명은 11월에 발행될 널리 아티클을 참고하시기 바랍니다.

여기서는 관련된 코드 예시를 첨부합니다.

// 보이스오버 초점을 가지기 시작했을 때와 잃었을 때의 값을 가지고 오기 위한 변수를 만듭니다.
    var isVoiceOverRunning: Bool = false
    
    override func accessibilityElementDidBecomeFocused() {
// 보이스오버 초점을 가질 때의 값을 가지고 오기 위한 메서드입니다.
        self.isVoiceOverRunning = true
    }
    
    override func accessibilityElementDidLoseFocus() {
// 보이스오버가 초점을 잃었을 때의 값을 가지고 오기 위한 메서드입니다.
        self.isVoiceOverRunning = false
    }
// 보이스오버 초점을 가지고 있으면 테이블뷰의 전체 데이터를 재구성하지 않고 변경되는 영역만 찾아 재구성시킵니다.
        if cell.isVoiceOverRunning {
            let beforeIndexPath = IndexPath(row: beforeFilter?.rawValue ?? 0, section: 0)
            self.collectionView.reloadItems(at: [beforeIndexPath,indexPath])
        } else {
                        self.collectionView.reloadData()
            self.collectionView.layoutIfNeeded()
            }

 

[WEB] 링크와 버튼을 구분하여 사용하기

Webacc NV | 2021-10-22 09:23:33

부제: 커스텀 버튼을 만드는 것은 시간 낭비이며 삽질에 가깝다

링크는 현재 웹사이트와 다른 웹사이트를 이어주거나, 웹사이트 내의 다른 영역을 이어주는 역할, 비유하자면 교통수단 역할을 합니다. 그러나, 링크의 수난은 여전히 계속되고 있습니다. 홍길동도 아닌 것이 링크이되 링크로 동작하지 못하는 링크가 많은 웹사이트에 있습니다.

앞에서 언급했듯, 링크는 페이지와 페이지를 연결하는 웹사이트의 교통수단?같은 존재입니다. 목적지를 관리하는 누군가가 문만 열어놓았다면, 누구나 어디서든 출입할 수 있게끔 해주는 어느 애니메이션의 “어디로든 문” 같은 존재이지요.

그런데, 많은 웹사이트에서 이러한 링크를 버튼 기능으로 사용하고 있습니다. 오래전부터 링크와 버튼에 대해서 접근성 이슈를 얘기해 왔으나, 아직도 말이지요.

링크와 버튼은 “눌러서 실행한다” 라는 작동 방법에 있어서 공통분모가 있습니다.

그래서 일까요? 사람들은 링크의 기본 기능을 무시하고, 버튼으로 사용합니다. Button이라는 요소가 있음에도 말이지요.

과거보다는 당연히 여러 Front End 개발자가 스크린 리더에 대해 알고 있습니다. 그러나 여전히 많은 개발자는 용도만 같다면 상관 없기에 링크를 버튼으로 둔갑시키고 있습니다.

그래서? 그게 나쁜 겁니까?

개발자 입장에서는 나쁜 의도는 하나도 들어가 있지 않습니다. 당연한 애기이겠지요. 그러나, 잘못된 사용으로 인해 고통받는 서비스 이용자나, 그것을 고쳐야 하는 개발자를 포함한 서비스 제공자에게 있어서, 감히 나쁘다고 말할 수 있습니다. 서로가 편리하지 않을 뿐더러, 그렇다고 어느 한 쪽이 편리하지 않습니다. 서로 지고 있는 것이지요.

그러면 도대체 왜 나쁜 겁니까?

스크린 리더를 조금이라도 아는 분도 있을 것이며, 모르는 분도 있을 것입니다. a 태그는 a 태그일 뿐, button이 아닙니다. 스크린 리더는 그 점을 여과없이 보여줍니다. a 태그라면 ‘링크’, button이라면 버튼이라고 읽어주지요.

스크린 리더 사용자는 개발/기획 주체와 서비스 제공자의 생각과 의도를 알 수 없습니다

먼저 사용자 측면부터 얘기해 보겠습니다. 당연히 스크린 리더 사용자는 컴퓨터를 배울 때, “링크”는 어떤 요소인지 교육을 받습니다. 링크는 누르면 다른 페이지로 이동하는 요소라고 말이지요. 높은 확률로 링크 요소에는 별도의 상태정보를 주지 않은 사이트가 많습니다. 링크에는 이렇다고 할만한 여러 상태 정보를 줄 속성이 없기 때문입니다. 스크린리더 사용자는 당연히 링크 요소이니까, 누르면 막연하게 어느 페이지로 나를 인도할 것이라고 생각하고, 페이지가 불러와질 때 까지 기다릴 것입니다.

누르면 즉각적으로 스크린 리더를 반응하게 할만한 상태 정보나 초점의 변화와 같은 이벤트가 없기 때문에 스크린리더 사용자는 개발자가 링크를 어떤 요소처럼 사용했는지 이해할 수 없습니다. 반대로, 버튼을 링크의 목적으로 사용하는 것은 허용되나, 링크는 버튼 요소와 철저히 구분되어야 합니다. 링크는 오로지 연결을 위해 만들어진 요소이기 때문입니다.

개발자와 서비스를 제공하는 기업에게는 어떤 이득이 있습니까?

자, 이제 사용자 측면만 이야기했으니 웹 문서를 작성하는 사람 입장에서의 단점도 이야기 해 보겠습니다. 링크를 버튼처럼 사용하기 위해서, 가장 많이 사용하는 방법은 두 가지가 있습니다. 링크의 href 값에 “javascript:void(0);”을 넣고, 클릭 이벤트를 넣는 방법과 href=”#”을 넣고 클릭 이벤트를 넣는 방법, 이 두가지가 가장 원시적인 방법이고, 많은 웹사이트에서 사용하는 방법일 겁니다. 이 두 가지는 차라리 나은 방법이라고 할 수 있을지도 모릅니다.  href값을 아예 안 줘서 초점 조차 안 가는 경우도 있으니까요.

href=”#”을 사용하는 방법은 빈 해시 링크를 생성하는 것이기 때문에 누를 때마다 주소 입력상자 끝에 #이 붙게 됩니다. 보통 이것을 원치 않기 때문에 href=”#”을 쓰는 링크의 클릭 이벤트에는 event.preventDefault()라는 이벤트 객체의 기본동작을 방지하는 메서드를 함께 불러옵니다. Button 태그였다면 필요하지 않았을 작업인데 말이지요. 뭐 그깟 코드 한 줄 추가되는 것 갖고 그러냐 싶겠지만, 비효율적인 것은 비효율적이라고 말할 수 밖에 없지요.

그리고 앞서, 스크린 리더 사용자는 링크로 받아들여 페이지 로딩을 기다린다는 문제점 때문에 버튼처럼 인식하게 하기 위해 WAI-ARIA의 role 속성을 통해 스크린 리더에게 해당 링크를 버튼으로 읽게끔 합니다. 버튼 태그는 눈으로 보기에 단순해 보이지만, 커스텀 기능으로 구현할 때, disabled와 같은 속성을 일일이 구현해 줘야 하며, Space 키로 활성화하는 등 하지 않아도 될 불필요한 일을 너무 많이 하게 됩니다. 간혹, 앵커 태그가 CSS 스타일링이 쉬워서 앵커 태그를 쓴다는 얘기를 듣기도 하지만, 작은 프로젝트이건, 큰 프로젝트이건, 버튼 태그도 스타일링이 그리 어렵지 않습니다.

많은 서비스에서 현실적으로 당장의 수정이 어렵기 때문에 a 태그를 유지하되 role=”button”을 구현합니다. 하지만, 이는 정말 지저분하고 비효율적이며, 얘기치 않은 버그를 만들기도 합니다.

직접 구현하는 경우도 있지만, 일일이 구현하는 것은 시간 낭비이기 때문에 우리는 빠른 개발을 위해서 다른 개발자가 만든 라이브러리를 가져다 씁니다. 하지만, 잘못 사용된 링크 요소 부분을 button 태그로 바꿀 수 있다면 그 라이브러리는 필요치 않게 됩니다. 그리고, 서버에 별도의 추가 스크립트 파일을 두거나, CDN 요청을 덜 해도 됩니다.

사용자를 위해 button태그를 쓴다고 생각하지 말고, 서비스를 제공하는 기업, 사용자, 모두가 편하기 위한 것임을 생각하고, 링크가 아닌 button이 필요한 순간에는 button 태그를 더 많이 사용했으면 합니다.

[iOS native] 스크린 리더용 영역 정보 제공하기 2탄

Webacc NV | 2021-10-13 18:17:22

예전에 TableView 에 accessibilityLabel 을 주면 테이블뷰에 접근했을 때 어떤 영역인지 읽어준다는 것을 공유한 적이 있습니다.

이처럼 화면에 보여지는 제목을 줄 수 없을 때 영역 정보를 주는 것은 레이아웃을 이해하도록 도울 수 있습니다.

그런데 TableView 가 아닌 UIView, UIScrollView 와 같은 영역에 어떤 영역인지에 대한 정보를 주어야 하는 경우가 있습니다. 

해당 컨테이너뷰들은 테이블뷰와 달리 accessibilityLabel 만 준다고 해서 영역 정보를 읽어주지 못합니다.

accessibilityLabel 과 동시에 accessibilityContainerType 을 semanticGroup 으로 재정의해 주어야 합니다.

다음 예시를 참고합니다.

        scrollView.accessibilityLabel = "banner section"
        scrollView.accessibilityContainerType = .semanticGroup

 

[iOS native] 커스텀 액션 네임이 특정 요소에서 추가되거나 삭제될 때

Webacc NV | 2021-10-12 12:56:34

커스텀 액션을 적용할 때 액션 네임이 항상 고정되어 있는 경우는 별 문제가 없겠지만 특정 액션 네임이 

상황에 따라 삭제되거나 추가되는 경우 커스텀 액션 적용 시 주의가 필요합니다.

예를 들어서 공유하기 버튼을 커스텀 액션으로 추가했다고 생각해 봅시다.

그러면 각 요소마다 보이스오버로 접근 시 공유하기 커스텀 액션이 표시될 것입니다.

하지만 특정 요소에는 공유하기뿐만 아니라 댓글달기 기능도 있다고 생각해 봅시다.

그러면 댓글 달기는 모든 요소에 있는 것이 아니기 때문에 커스텀 액션 적용을 할 때 주의가 필요하다는 의미입니다.

이때는 어떻게 하면 될까요? 

사실 해결 방법은 너무나 간단합니다.

댓글달기가 화면에 표시되는 경우에는 accessibilityCustomActions 안에 댓글까지 포함한 배열 형태의 액션을 추가하도록 하면 됩니다.

즉 댓글이 포함된 커스텀 액션 메서드와 댓글이 미포함된 커스텀 액션 메서드 두 개를 만들어 두고 조건문을 활용하여 댓글이 화면에 표시되는 경우와 그렇지 않은 경우를 구분하여 이미 지정한 메서드를 추가하면 됩니다.

[iOS native] TableViewCell 내에 여러 UILabel 이 포함되는 경우

Webacc NV | 2021-10-08 18:50:52

테이블뷰를 사용하여 화면을 구성하는 경우 각각의 TableViewCell 내에 텍스트, 버튼 등의 여러 객체들이 들어가게 됩니다. 

그런데 이때 접근성 관점에서 유념해야 하는 것이 하나 있는데 바로 한 셀에 여러 UILabel 과 같은 텍스트뷰들이 있으면 보이스오버는 해당 요소들이 하나의 초점으로 잡힌다는 것입니다.

따라서 만약 한 셀에 4개의 이미지 버튼과 4개의 텍스트를 가진 UILabel 을 배치했다면 보이스오버 초점은 4개의 UILabel 텍스트가 단 하나의 초점으로 제공되며 각 버튼이 별도로 포커스 됩니다.

그러나 UILabel 의 accessibilityTraits 를 버튼으로 주게 되면 초점은 분리됩니다.

따라서 하나의 셀에 각 UILabel이 고유의 클릭 이벤트를 가지고 있다면 accessibilityTraits를 .button으로 적용하여 초점을 분리시켜 주어야 각 레이블을 실행할 수 있겠습니다.

[Android native] 톡백에서 활성화 하려면 이중탭하세요 힌트 아예 없애기

Webacc NV | 2021-10-08 18:15:50

접근성 테스트를 하다보면 이중탭을 해도 아무런 동작이 없으나 톡백에서는 활성화 하려면 이중탭하라는 메시지가 출력되는 경우가 있습니다.

이는 클릭을 했을 때의 동작을 정의하지 않더라도 클릭 혹은 롱클릭 리스너가 시스템 자체에서든 수동으로든 추가되는 순간 AccessibilityAction 내에 클릭 액션이 추가되기 때문입니다.

이런 경우 클릭에 대한 접근성 힌트를 없애려면 클릭 리스너 자체를 삭제하는 것이 가장 깔끔한 방법입니다.

그러나 여러 상황 상 그렇게 할 수 없는 경우에는 AccessibilityAction 에 추가되어 있는 클릭 액션 자체를 삭제하는 방법으로도 이를 해결할 수 있습니다.

AccessibilityAction 내의 클릭 액션을 삭제하는 방식으로 접근성이 적용된 대표적인 예는 구글에서 제공하는 MaterialDesignTabLayout 입니다.

최신 탭레이아웃 라이브러리를 적용하여 탭을 개발하게 되면 선택된 탭의 경우 활성화 하려면 이중탭하라는 힌트 메시지를 발화 하지 않는 것을 확인할 수 있습니다.

아래는 AccessibilityAction 내의 클릭 액션 자체를 제거하는 코드 예시입니다.

핵심은 AccessibilityNodeInfo 내에서 clickable false, removeAction 메서드를 사용하면 됩니다.

        ViewCompat.setAccessibilityDelegate(view, new AccessibilityDelegateCompat() {
            @Override
            public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
                super.onInitializeAccessibilityNodeInfo(host, info);
                info.setClickable(false);
                info.removeAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK);
            }
        });

 

[Android native] 중첩된 레이아웃으로 인해 접근성 초점 순서가 틀어질때

Webacc NV | 2021-10-07 13:10:23

iOS 와 마찬가지로 안드로이드에서도 레이아웃이 중첩되면 접근성 초점 순서가 틀어질 수 있습니다.

WebView, RecyclerView 등을 화면 전체로 하고 해당 레이아웃 위 또는 아래에 다른 도구 버튼들을 두는 경우가 대표적입니다.

이런 경우에도 accessibilityTraversal before, after 속성을 사용하여 접근성 초점을 재조정할 수 있습니다.

예를 들어 backRView, frontRView  두 개의 RecyclerView 가 있고 화면 상으로 배치된 레이아웃 순서와 달리 접근성 초점은 back RecyclerView 가 먼저 접근된다고 합시다.

이 때 다음과 같이 backRView 에 대한 접근성 순서를 변경할 수 있습니다.

android:accessibilityTraversalAfter="@id/frontRView"

다만 ViewPager, RecyclerView 와 같이 시맨틱한 레이아웃이 아닌 LinearLayout, RelativeLayout 등은 레이아웃 자체의 초점 순서를 변경할 수 없으며 하위의 뷰들을 참조해야 합니다.

[Android native] EditText 내부에 화면에 보여지는 힌트를 제공할 수 없을 때 해결 방법 코드 예시

Webacc NV | 2021-10-07 11:00:39

지난번 EditText 내의 레이블을 제공하기 위해서 contentDescription 사용을 해서는 안 된다는 게시글을 작성한 적이 있습니다.

당시 해당 게시글을 통해 AccessibilityNodeInfo 객체에서 hintText 를 추가함으로써 문제를 해결할 수 있다고 공유를 하였습니다.

오늘은 해당 게시글에 대한 코드 예시를 공유하고자 합니다.

코드 예시에서는 편집창에 금액 입력이라는 화면에 보이지 않는 접근성 레이블을 넣어 보도록 하겠습니다.

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

        ViewCompat.setAccessibilityDelegate(editText, object : AccessibilityDelegateCompat() {
            override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfoCompat?) {
                super.onInitializeAccessibilityNodeInfo(host, info)
                info?.hintText = "금액 입력"
            }
        })

 

[HTML] 톡백과 크롬에서의 <input type="number" 밸류 값 읽지 못하던 이슈 관련

Webacc NV | 2021-10-06 10:50:05

톡백 최신 버전과 크롬 정식의 최신 웹뷰를 사용하더라도 아래와 같은 마크업 구조에서 톡백이 밸류 값을 읽지 못하는 문제가 있습니다.

<input type="number" value="3" aria-label="수량>

해당 문제는 2021년 10월 6일 현재 크롬 카나리 최신 버전에서 해결이 되었습니다.

따라서 약 2개월 정도 있으면 정식 버전 크롬에도 반영될 것이라 생각합니다.

관련하여 참고 하시기 바랍니다.

[iOS native] 두 손가락 문지르기 제스처로 뒤로 이동하도록 구현하는 코드 예제

Webacc NV | 2021-09-24 12:08:15

지난 번 iOS 뒤로가기 제스처 접근성에 대한 아티클을 작성한 적이 있습니다. 

본 팁에서는 accessibilityPerformEscape 적용 예제를 공유하려고 합니다.

내비게이션 컨트롤러의 네이티브 뒤로가기 버튼을 사용하지 않을 때 아래 예시를 참고하여 뒤로가기 버튼의 접근성을 적용할 수 있겠습니다.

    override func accessibilityPerformEscape() -> Bool {
        self.navigationController?.popViewController(animated: true)
        return true
    }

 

[Android 짧은 팁] 동적으로 생성한 요소의 접근성 초점 순서를 변경해야 할 때

Webacc NV | 2021-09-24 11:12:36

들어가기 앞서, 본 팁은 코틀린(Kotlin)으로 작성되었습니다.

Android 앱을 만들다 보면 동적으로 생성되는 View의 순서를 바꾸고 싶을 때도 분명 있을 겁니다.

그럴 때는 View.generateViewId() 메소드와 단순한 연산만으로 유니크한 id 정수를 부여할 수 있습니다.

물론 for문으로 View를 생성하는 상황은 많지 않지만, for문으로 예제를 하나 만들어보도록 하겠습니다.

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)
  var buttonIdBase = View.generateViewId()
  val buttonList = arrayListOf<Button>()
  val myButtonGrid:GridLayout = findViewById(R.id.MyGridLayout);
  for (i:Int in 1..12) {
    val btn:Button = Button(this)
    btn.id = ButtonIdBase
    btn.contentDescription = i
    buttonList.add(Btn)
    buttonIdBase += 1
    myButtonGrid.addView(btn)
  }
  // 만약, 필요에 의해 10번째 버튼 다음에 12번째 버튼에 접근성 초점이 가게 하려면
  ButtonList[9].accessibilityTraversalAfter = ButtonList[11].id
}

generateViewId는 유니크한 int값을 하나 만듭니다. 이 상태에서 for문을 통해 숫자를 하나씩 더해가며 유니크한 연속된 아이디 값을 만들어 각 버튼에 부여하고, 버튼을 배열에 담았습니다.

accessibilityTraversalAfter나 accessibilityTraversalBefore는 반드시 id를 참조해야 하는데, 반복으로 생성된 요소에는 일반적으로 id가 없기 때문에 접근성 초점 순서를 조절할 때 곤란할 수 있습니다. 방법이야 달라지겠지만 동적으로 생성되는 요소에도 이렇게 id를 부여하여, 접근성 초점의 순서를 변경시킬 수 있습니다.

[iOS native] 중첩된 레이아웃 구조로 인해 접근성 포커스 순서가 틀어질 때

Webacc NV | 2021-09-24 10:33:17

예전에 iOS 접근성 초점 순서 재조정하기에 대해 다루었습니다. 

이번에는 해당 초점 재조정에 대한 응용 편입니다.

한 컨테이너 안에서의 특정 뷰들 사이에서 접근성 순서를 재조정하는 것이 아닌 레이아웃 자체가 틀어졌을 때의 순서 재조정 방법입니다.

예를 들어 TableView 가 전체 화면을 차지하고 있고 TableView 위 아래로 레이아웃이 중첩되어 다른 버튼들이 표시되는 경우에는 중첩된 레이아웃으로 인해 초점 순서가 틀어지는 경우가 있습니다.

이런 경우에도 accessibilityElements 변수를 활용해서 관련 문제를 수정할 수 있습니다.

다만 위에서 든 예시와 같은 경우는 레이아웃 자체의 초점이 틀어진 것이므로 해당 컨테이너가 들어 있는 상위 즉 view 의 accessibilityElements 를 재조정하면 됩니다.

예를 들어 뷰컨트롤러 내에 headerView, tableView, footerView 가 있다면 다음과 같이 수정할 수 이겠습니다.

self.view.accessibilityElements = [headerView, tableView, footerView]

[인터넷] 센스리더 7.7 업데이트에 따른 일부 확장축소, 선택됨 및 팝업 읽어주기 기능 추가 관련

Webacc NV | 2021-09-18 10:23:36

접근성을 적용한 마크업 시 확장/축소, 선택됨, 팝업 있음 같은 정보를 스크린 리더 호환성을 고려하여 제공하기 위해 고민을 하신 분들이 많으리라 생각합니다.

이번 센스리더 7.7 업데이트를 통해 WAI-ARIA 속성 중 aria-expanded, aria-haspopup, aria-selected true 속성을 지원하게 되었습니다.

따라서

1. 링크나 버튼에 aria-expanded true false 적용 시 확장됨, 축소됨 정보를 읽어주게 됩니다.

2. 버튼에 aria-haspopup 적용 시 풀다운 버튼 메뉴 라고 읽어줍니다.

3. 탭컨트롤에 aria-selected true 적용 시 가상커서를 켠 상태에서는 선택된 탭에는 선택 이라는 정보를 함께 읽습니다. 

접근성 구현 및 테스트 시에 참고하시기 바랍니다.

 

[iOS native] 커스텀 탭막대 접근성 코드 적용 예제

Webacc NV | 2021-09-16 18:45:49

얼마전 널리 아티클을 통해 커스텀 탭막대에 대한 접근성 적용하기에 대해 다루었습니다.

본 팁에서는 커스텀 탭막대 접근성 적용 시 참고하실 수 있도록 간단한 코드 예제를 공유하려고 합니다.

	// 커스텀 탭 요소들이 들어 있는 상위 superView 에 tabBar trait 적용
        footerView.accessibilityTraits = .tabBar
        // 기본으로 선택된 탭에 selected trait 추가
        self.footerButton.accessibilityTraits.insert(.selected)
    // 다른 탭 실행 시 선택됨 상태 변경 및 화면 변경 이벤트 적용
    @IBAction func onPlusModeButtonClicked(_ sender: Any) {
            self.footerPlusButton.accessibilityTraits.insert(.selected)
            self.footerButton.accessibilityTraits.remove(.selected)
            UIAccessibility.post(notification: .screenChanged, argument: self.footerPlusButton)
        self.isPlusMode = true
        self.tableView.reloadData()
    }
    

 

[iOS native] 이중탭했을 때의 동작 재정의하기

Webacc NV | 2021-08-17 21:30:34

가끔 접근성을 테스트 하다보면 분명히 탭을 해서 실행 가능한 요소임에도 불구하고 보이스오버를 켠 상태에서 이중탭을 하면 실행이 안 되는 경우들을 보곤 합니다.

여러 가지 이유가 있겠지만 대표적인 원인으로는 접근성 초점을 가지는 요소의 하위 뷰 들 중 실행 가능한 요소가 있을 경우 보이스오버가 해당 뷰에 정확하게 초점을 맞추지 못해 발생하는 경우 입니다.

예를 들어 보겠습니다.

테이블뷰셀 내에 두 개의 UILabel, 한 개의 UISwitch 가 있습니다.

그러면 보이스오버에서는 총 2번의 초점이 제공될 것입니다.

이는 테이블뷰셀의 경우 한 셀을 기본적으로 하나로 처리하려는 경향이 있기 때문에 두 개의 UILabel은 하나의 초점으로 제공합니다.

그런데 두 개의 초점을 만약 하나의 초점으로 제공해 주는 것이 맥락상 자연스럽다면 테이블뷰셀에 접근성 초점을 주고 accessibilityLabel, accessibilityTraits, accessibilityValue 를 각 레이블 및 스위치에서 정보를 오버라이드 할 것입니다.

그런데 이렇게 할 경우 정작 이중탭을 하면 스위치가 온오프 되어야 하지만 동작이 안 될 수 있습니다.

이때 사용할 수 있는 메서드는 accessibilityActivate 입니다.

해당 메서드는 보이스오버를 실행한 상태에서 이중탭을 했을 때 실행할 동작을 재정의할때 사용합니다.

안드로이드와 비교하자면 replaceAccessibilityAction 메서드 내의 ACTION)CLICK 의 동작을 재정의하는 것과 비슷하다고 할 수 있겠습니다.

따라서 다음 코드 예시와 같이 사용할 수 있겠습니다.

class SwitchTableViewCell: UITableViewCell {

    @IBOutlet weak var settingTitleLabel: UILabel!

    @IBOutlet weak var settingSubtitleLabel: UILabel!

    @IBOutlet weak var settingSwitch: UISwitch!

    override func awakeFromNib() {

        super.awakeFromNib()

        isAccessibilityElement = true

    }

    override var accessibilityLabel: String? {

        get { "\(settingTitleLabel.accessibilityLabel ?? ""). \(settingSubtitleLabel.accessibilityLabel ?? "")" }

        set {}

    }

    override var accessibilityTraits: UIAccessibilityTraits {

        get { settingSwitch.accessibilityTraits }

        set {}

    }

    override var accessibilityValue: String? {

        get { settingSwitch.accessibilityValue }

        set {}

    }

    override func accessibilityActivate() -> Bool {

        settingSwitch.isOn = !settingSwitch.isOn

        return true

    }

}

 

[Android native] onInitializeAccessibilityEvent 메서드 이해하기

Webacc NV | 2021-07-24 18:36:20

우리가 널리 아티클 및 포럼에서 여러 번 다루었던 onInitializeAccessibilityNodeInfo 는 톡백에서 읽어주는 요소 유형이나 상태정보 혹은 접근성 액션과 같은 노드 정보를 수정할 때 사용하는 메서드 입니다. 

오늘 함께 살펴보고자 하는 것은 onInitializeAccessibilityEvent 입니다.

해당 메서드는 메서드 이름만 봐도 알 수 있듯이 접근성 이벤트와 관련된 메서드로 특정 접근성 이벤트가 발생했을 때를 캐치하여 무언가의 작업을 할 때 사용합니다.

예를 들어 우리가 클릭 리스너가 있는 뷰를 이중탭했다고 생각해 봅시다.

그러면 톡백에서 두둑 하는 사운드가 발생합니다. 이것은 접근성 이벤트 중 클릭 이벤트가 발생했다는 것입니다.

또는 한 손가락 쓸기를 통해서 특정 요소에 포커스 하였고 톡백에서 포커스 한 내용을 읽었다고 하십시다.

이때 접근성 이벤트 중 AccessibilityFocused 이벤트가 발생합니다.

이와 같은 접근성 이벤트가 발생했을 때 이를 조건문으로 활용하여 원하는 기능을 구현할 수 있으며 이때 해당 메서드를 사용합니다.

적용방법은 간단합니다.

onInitializeAccessibilityEvent 메서드 및 super 메서드를 오버라이드 한 다음 if 문을 사용하여 접근성 이벤트 타입 중 클릭, 접근성 포커스됨, 뷰에 포커스됨 등의 이벤트가 있으면 무언가를 해줘 라고 구현하면 됩니다. 다음 예시를 참고합니다.

        ViewCompat.setAccessibilityDelegate(nextTrack, new AccessibilityDelegateCompat() {
            @Override
            public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
                super.onInitializeAccessibilityEvent(host, event);
                if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED) {
                    Handler handler = new Handler(Looper.getMainLooper());
                    handler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            timePosition.announceForAccessibility(timePosition.getText());
                        }
                    }, 500);

 

[iOS native] 액션 핸들러를 이용하여 커스텀 액션 적용하기

Webacc NV | 2021-07-08 17:30:04

지난번에 iOS 커스텀 액션 구현 방법에 대해 공유한 적이 있습니다.

해당 예제에서는 선택자, 즉 이미 구현된 함수 메서드를 가져와서 accessibilityCustomActions 변수 안에 배열 형태로 삽입함으로써 사용자가 커스텀 액션을 이중탭하였을 때 실행하고자 하는 기능을 삽입하는 방식이었습니다.

그런데 이 방법 외에도 iOS 에서는 액션 핸들러를 이용하여 커스텀 액션 내에서 실행될 것들을 직접 구현할 수도 있습니다. 

따라서 오늘은 해당 액션 핸들러를 사용하는 예제를 공유하려고 합니다.

액션 핸들러를 사용하더라도 accessibilityCustomActions 변수 안에 배열 형태로 액션을 넣는 것은 같습니다.

그런데 액션 핸들러를 사용할 때는 선택자가 아닌 액션 네임과 각 액션 핸들러가 들어가게 됩니다.

액션 네임은 보이스오버에서 읽어줄 커스텀 액션 네임이며 핸들러는 기능 실행에 대한 것입니다.

다음과 같이 코드를 구현할 수 있습니다.

 func makeAccessibilityCustomActions() -> [UIAccessibilityCustomAction] {
 let action = UIAccessibilityCustomAction(name: actionName) { (action) -> Bool in
            UIAccessibility.post(notification: .announcement, argument: "\(actionName)됨")
            self.setFavorite()
            self.accessibilityCustomActions = self.makeAccessibilityCustomActions()
            return true
        }
        return [action]
    }

현재는 하나의 액션만 넣었지만 let action2, let action3 과 같이 여러 액션을 만들어서 리턴해 주면 해당 개수만큼 커스텀 액션이 늘어나게 됩니다.

단 액션 네임이 관심 선택, 관심 해제와 같이 상황에 따라 변경되어야 하는 경우에는 스트링 변수를 사용하여 네임을 만든 다음 액션 네임이 변경되어야 할 시점에 스트링 네임을 변경하고 그것을 다시 accessibilityCustomActions 배열에 넣어 주어야 합니다.

[Android native] 커스텀 뷰에 onTouchListener 구현 시에는 performClick 적용 필요

Webacc NV | 2021-06-19 16:30:52

동영상 플레이어에서 화면 터치를 통해 숨겨진 화면 컨트롤을 다시 보이게 하거나 숨기는 작업 등을 구현할 때 onTouchListener 를 사용하게 됩니다. 그런데 이러한 onTouchListener 사용시에는 톡백 사용자는 이중탭을 해도 기능 실행이 전혀 안 됩니다. 따라서 다음 코드 예시와 같이 onTouchListener 사용 시에는 손가락 떼기 이벤트 발생 시 performClick 메서드를 함께 적용해 주어야 합니다.

자세한 설명은 7월달에 공개될 동영상 플레이어 접근성 적용하기 아티클을 참고해 주시기 바랍니다.

  public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        isTouching = true;
        return true;
      case MotionEvent.ACTION_UP:
        if (isTouching) {
          isTouching = false;
          performClick();
          return true;
        }
        return false;
      default:
        return false;
    }
  }
  // performClick 메서드를 오버라이드 할 때 실행될 동작을 아래에서 정의합니다.
  @Override
  public boolean performClick() {
    super.performClick();
    return toggleControllerVisibility();
  }

 

[iOS native] 버튼과 텍스트가 분리된 접근성 개선하기 두 번째

Webacc NV | 2021-06-11 17:08:22

작년에 버튼 이미지와 텍스트 레이블 초점이 분리된 경우의 접근성 개선 방안에 대해 다룬 적이 있었습니다.

해당 방법대로 접근성 개선을 할 경우 보이스오버 사용자가 버튼과 텍스트 레이블을 하나의 포커스로 읽을 수 있습니다.

그러나 위와 같은 방법 적용시 다음과 같은 문제가 있습니다.

저시력 사용자가 보이스오버를 켠 상태에서 화면에 있는 이미지 혹은 텍스트 레이블 터치 시 내용을 읽어주지 않음.

이는 두 요소 중 하나의 요소를 접근성 초점에서 완전히 제거했기 때문에 화면에는 두 영역이 있으나 보이스오버에서는 한 영역만 초점이 제공되기 때문입니다.

따라서 오늘은 다른 방법을 추가적으로 공유를 하려고 합니다.

1. 버튼과 버튼에 해당하는 텍스트가 있는 요소를 별도의 컨테이너 안으로 옮깁니다.

즉 예를 들어 하나의 컨테이너에 6개의 객체들이 있는데 그 중 두 객체가 버튼과 이미지라고 합시다.

그렇다면 기존 컨테이너 안에 또 다른 buttonAccessibilityContainer 와 같은 UIView 컨테이너를 만들고 버튼 이미지와 레이블 텍스트를 해당 컨테이너 안으로 옮기는 것입니다.

2. 버튼과 텍스트만 들어 있는 컨테이너의 isAccessibilityElement true 로 설정합니다.

그러면 보이스오버는 하위 두 요소에는 초점을 제공하지 않습니다.

3. 해당 컨테이너에서 accessibilityLabel, accessibilityTraits 를 설정합니다.

이렇게 하면 보이스오버에서도 하나의 초점으로 버튼 이미지와 레이블을 읽을 수 있고 접근성 프레임 영역도 두영역이 다 하나로 잡히게 됩니다.

스토리보드에서 컨테이너를 추가로 만들어 초점을 하나로 합치고자 하는 객체를 넣은 다음 다음과 같은 함수를 만들어 사용할 수도 있겠습니다.

    private func setContainerViewAccessibility(containerView: UIView, traitsView: UIView, label: String) {
        
        containerView.isAccessibilityElement = true
        containerView.accessibilityLabel = label
        containerView.accessibilityTraits = traitsView.accessibilityTraits
    }

이러한 함수를 만들었다면 다음과 같이 적용이 가능합니다.

self.setContainerViewAccessibility(containerView: self.confirmView, traitsView: self.confirmButton, label: self.confirmLabel.text ?? "확인")

 

[iOS native] UIAccessibilityNotification .announcement 코드 예제

Webacc NV | 2021-06-09 16:57:48

접근성을 개선할 때 참고하실 수 있도록 여러 상황에 대한 코드 예제를 팁 형태로 올려드리고 있습니다.

보이스오버에서 특정 상황에서 알림 메시지를 자동으로 보이스오버가 말하도록 해야 할 때 사용하는 메서드에 대해 소개는 하였으나 정작 이에 대한 코드 예제가 없어 이를 공유하게 되었습니다.

이전 배너, 다음 배너 버튼을 누를 때마다 배너가 넘어가면서 배너의 내용을 자동으로 읽어주도록 하는 것을 예로 들어 예제 코드를 공유합니다.

알림 메시지에 대한 접근성 적용시 참고하시기 바랍니다.

    @IBAction func next(_ sender: UIButton) {
        self.count = self.count + 1
        if self.count > self.banners.count - 1  {
            self.count = 0
        }
  
        let x = UIScreen.main.bounds.width * CGFloat(self.count)
        self.scrollView.contentOffset = CGPoint(x: x, y: 0)
        sleep(1) // 알림 메시지를 주기 전 약간의 딜레이 주기
        UIAccessibility.post(notification: .announcement, argument: self.banners[self.count])
    }

 

[Android native] 편집창, 체크박스와 레이블이 분리된 경우 labelFor 속성 활용하기

Webacc NV | 2021-06-03 18:05:15

편집창이나 체크박스를 구현할 때 레이블을 해당 뷰 안에 넣는 경우도 있지만 텍스트뷰와 분리하는 경우도 많습니다.

이런 경우 톡백에서 체크박스나 편집창에 초점을 주면 어떤 체크박스나 편집창인지를 읽지 못합니다.

물론 체크박스나 편집창에서 한 손가락 오른쪽 혹은 왼쪽 쓸기를 했을 때 이에 해당하는 텍스트뷰를 읽어주기는 하지만 여러개의 편집창이 있거나 혹은 컨트롤 단위로 이동했을 때 바로 해당 레이블을 듣지 못하게 되어 접근성 수정이 필요합니다.

방법은 아주 간단합니다.

1. TextView 에 labelFor 속성을 추가합니다. labelFor 에는 텍스트뷰와 연결되는 편집창 혹은 체크박스의 id 를 연결합니다.

2. 해당 체크박스에는 1번에서 labelFor 속성에 명시한 id 를 추가합니다.

[iOS native] 탭하여 실행 가능한 이미지뷰에 반드시 대체 텍스트를 제공해야 하는 이유

Webacc NV | 2021-06-01 18:18:55

아이폰, 안드로이드 모두 이미지뷰에 대체 텍스트를 삽입하지 않으면 보이스오버, 톡백에서 해당 이미지뷰는 장식용으로 간주하고 초점 자체가 가지 않습니다.

그런데 안드로이드의 경우에는 해당 이미지뷰에 클릭 리스너 이벤트가 추가되는 순간, 즉 탭할 수 있는 동작이 들어가는 순간 대체 텍스트가 없다 하더라도 이미지뷰에 초점을 받을 수 있게 됩니다.

물론 톡백에서 이미지뷰에 포커스를 하면 라벨 지정되지 않음 버튼, 혹은 id 값이 있다면 btnCancel 버튼과 같이 읽어줄 것입니다.

하지만 보이스오버의 경우에는 이미지뷰에 UITapGestureRecognizer 이벤트가 추가되어 이중탭하여 무언가를 실행할 수 있게 되었다 하더라도 안드로이드와 달리 대체 텍스트가 없으면 접근성 초점이 제공되지 않습니다.

따라서 실행 가능한 이미지뷰에 대체 텍스트가 없으면 보이스오버 사용자는 해당 객체를 실행조차 못하는 접근성 문제가 발생하게 됩니다.

따라서 대체 텍스트 삽입의 중요성에 대해 우리는 더욱 생각하며 개발을 해야 하겠습니다.

[Android native] 커스텀 라디오버튼 접근성 적용하기

Webacc NV | 2021-05-31 16:55:01

여러 팁을 통해 커스텀 스위치, 커스텀 체크박스, 커스텀 탭 등의 접근성 적용 예제에 대해 다룬 적이 있습니다.

사실 커스텀 라디오 버튼 역시 구현 방법은 비슷하지만 샘플 코드가 있으면 개발 시에 참조하기 편리하므로 아래에 커스텀 라디오 버튼에 대해서도 샘플 코드를 공유하려고 합니다.

참고: 본 예제의 라디오버튼은 이미지뷰로 적용하였으며 단품, 세트 중에서 선택하는 커스텀 라디오 버튼이라고 가정하겠습니다.

접근성 구현 시 참고가 되었으면 좋겠습니다.

        // 이미지뷰의 접근성 정보 수정을 위해 AccessibilityDelegate 객체 만들기
        button1.accessibilityDelegate = object : View.AccessibilityDelegate() {
            override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfo?) {
                super.onInitializeAccessibilityNodeInfo(host, info)
                info?.className = RadioButton::class.java.name
                info?.isCheckable = true // 체크, 체크 해제에 대한 음성 안내
                info?.isChecked = button1.isSelected // 조건문에 따라 checked true false 적용
                info?.isSelected = false // 선택된 요소에 selected 적용한 경우 체크됨과 중복되므로 해제

            }
        }

 

[Android native] 탭레이아웃 라이브러리에 적용된 탭 요소 접근성 적용과 커스텀 탭에 대한 접근성 대응

Webacc NV | 2021-05-31 15:13:57

안드로이드 탭 레이아웃 접근성 기능 업데이트에 대해 말씀드린 적이 있었습니다.

지난번에도 언급한 바와 같이 탭 요소를 탭이라고 읽어주도록 업데이트가 되었는데 원칙적으로는 안드로이드 접근성 API 에는 탭이라는 접근성 역할이 없습니다.

그럼 라이브러리에서 톡백이 어떻게 탭이라고 읽도록 구현했을까요?

그에 대한 정답은 바로 roleDescription 입니다.

구글에서도 톡백의 각 탭을 탭으로 읽도록 하기 위해서 AccessibilityNodeInfoCompat roleDescription 메서드를 사용한 것입니다.

따라서 탭을 구현할 때 탭레이아웃을 사용하면 접근성 구현이 별도 필요하지 않습니다.

하지만 기본 탭레이아웃을 사용하지 않고 텍스트뷰 등으 탭으로 사용한 경우에는 다음 예시와 같이 roleDescription 을 사용하여 탭 요소를 탭으로 읽도록 구현할 수 있습니다.

    ViewCompat.setAccessibilityDelegate(customTab, new AccessibilityDelegateCompat() {
            @Override
            public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
                super.onInitializeAccessibilityNodeInfo(host, info);
                info.setRoleDescription("탭");
            }
        });

 

[Android native] 확장/축소 접근성 적용 예제

Webacc NV | 2021-05-31 12:20:52

작년 8월 즈음에 안드로이드에서의 확장/축소 접근성 적용에 대해 한번 다룬 적이 있었습니다.

이번 시간에는 확장 축소가 적용된 샘플 코드를 공유해 보려고 합니다.

코드 적용에 대한 자세한 설명은 8월에 발행될 접근성 블로그 아티클을 참고해 주시기 바랍니다.

참고: 이번에 다룰 코드 예시에서는 과일 버튼이 있다고 가정하고 과일 버튼은 확장 축소가 가능한 버튼이며 기본적으로는 축소되어 있습니다.

        // 접근성 정보 수정 
        fruitButton.setAccessibilityDelegate(new View.AccessibilityDelegate() {
            // 확장/축소 정보 삽입
            @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
                super.onInitializeAccessibilityNodeInfo(host, info);
                info.setClassName(Button.class.getName());
                
                if (isFruitContainerExpanded) {
                    // 확장된 상태이므로 축소 액션 삽입
                    info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE);
                } else {
                    info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
                }
            }

// 톡백에서 작업 메뉴를 열어 확장 축소 적용 가능하도록 구현
            @Override
            public boolean performAccessibilityAction(View host, int action, Bundle args) {
                if (super.performAccessibilityAction(host, action, args)) {
                    return true;
                }
                if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) {
                    // 톡백에서 축소하는 액션을 작업 메뉴에서 실행 시
                    isFruitContainerExpanded = false;
                    collapseFruitContainer();
                } else if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
                    isFruitContainerExpanded = true;
                    expandFruitContainer();
                    return true;
                }
                return false;
            }

 

[iOS native] 버튼, 레이블과 같은 요소의 상위 컨테이너에 접근성 초점을 주는 의미

Webacc NV | 2021-05-28 13:27:36

iOS 보이스오버는 기본적으로 UIView, UICollectionView 와 같은 컨테이너에는 접근성 초점이 제공되지 않습니다. 

그 이유는 해당 컨테이너들은 접근성에서는 의미를 가진 요소가 아니며 버튼, 레이블, 텍스트와 같은 요소들을 품고 있는 요소들이기 때문입니다.

그런데 만약 이러한 컨테이너에 접근성 초점을 주면 어떻게 될까요?

containerUiView.isAccessibilityElement = true

이렇게 하면 해당 컨테이너 안에 포함된 그 어떤 요소에도 접근성 초점이 제공되지 않습니다.

따라서 접근성 초점을 줄 때는 상당히 주의가 필요합니다.

하지만 상황에 따라서는 이러한 상위 컨테이너 뷰에 접근성 초점을 주어야 하는 경우도 있습니다.

대표적인 예가 하위 컨테이너 뷰들을 접근성 초점에서 재구성해야 할 때입니다.

하위 뷰들의 초점 순서가 논리적이지 못하거나 하위 뷰들의 헤더와 데이터가 별도의 뷰로 구현되어 있거나 분리되어 있어야 할 뷰들이 하나로 합쳐져 있는 것 등 이미 구현된 뷰를 접근성에서 재구성해야 할 때 사용을 합니다.

즉 가상의 접근성 요소들을 만드느 것입니다.

앞으로 몇 차례에 걸쳐 해당 주제에 대해 다루어 보도록 하겠습니다.

[iOS native] accessibilityTrait 변경 시 유의사항

Webacc NV | 2021-05-26 17:27:57

커스텀 컨트롤에서 각 요소의 유형이나 상태를 접근성 서비스에서 적절하게 읽어줄 수 있도록 하기 위해 우리는 accessibilityTraits 속성을 많이 변경하곤 합니다. 

그런데 안드로이드나 웹과는 달리 iOS accessibilityTraits 속성은 요소 유형, 상태정보 속성을 함께 포함하고 있습니다.

따라서 웹이나 안드로이드는 특정 요소에 대한 상태정보를 변경할 때 역할(role) 속성이 이미 정의되어 있다면 상태 정보만 상황에 따라 변경해 주면 됩니다.

그러나 iOS에서는 위에서 설명한 것처럼 accessibilityTraits 를 재정의하는 순간 기존의 모든 역할, 상태정보 등은 초기화 되므로 이에 대한 주의가 필요합니다.

예를 들어 UIButton 으로 되어 있는 요소에 선택됨 정보를 주어야 한다고 생각해 봅시다.

선택됨 정보는 accessibilityTraits = .selected 를 통해서 줄 수 있습니다.

그러나 바로 위의 코드와 같이 적용하면 기존 버튼 속성은 사라져버립니다.

우리는 버튼과 선택됨을 함께 읽도록 해야 하므로 accessibilityTraits = ].button, .selected] 와 같이 속성을 주어야 하는 것입니다.

혹은 accessibilityTraits 추가시에는 insert, 삭제 시에는 remove 를 사용하는 것도 방법이 될 수 있습니다.

직군별 체크리스트 문의드립니다.

연연 | 2021-05-21 17:59:16

안녕하세요. 널리의 접근성 자료와 아티클들 잘 보고 있습니다.

 

널리에서 공유하고 있는 웹 접근성 교육의 '직군별 체크리스트'를 업무 시에 유용하게 참고하고 있습니다.

감사합니다.

 

직군별 체크리스트에서 직군별로 유의해야 할 접근성 지침 항목이 뚜렷하게 구분되진 않을 것이라 생각합니다.

어떤 기준으로 구분하였는지 여쭤보고 싶네요.

기준을 참고하여 모바일 접근성 지침에도 적용해 보고 싶습니다.

감사합니다. 

[Android native] 커스텀 토글버튼 접근성 구현

Webacc NV | 2021-05-07 19:06:02

즐겨찾기 켜짐/꺼짐, 알람 켜짐/꺼짐과 같은 토글 버튼을 구현할 때 스위치나 토글버튼 클래스를 사용하지 않고 커스텀뷰로 구현하는 경우에는 톡백을 사용하는 스크린 리더 사용자가 현재 상태가 꺼짐 상태인지 켜짐 상태인지 상태정보를 알 수 없습니다.

이때 상태정보를 대체 텍스트로 제공하기보다는 AccessibilityNodeInfo 객체를 수정하여 상태정보를 읽을 수 있도록 하는 것이 사용성 측면에서 훨씬 도움이 됩니다. 

AccessibilityNodeInfo 객체의 활용에 대해서는 여러 번 언급하였지만 예제 코드를 공유하는 차원에서 아래에 다시 변경하는 방법을 정리합니다.

// AccessibilityDelegate 객체 만들기
checkBox.setAccessibilityDelegate(checkBoxAccessibilityDelegate);

    final View.AccessibilityDelegate checkBoxAccessibilityDelegate = new View.AccessibilityDelegate() {
// onInitializeAccessibilityNodeInfo 메서드 오버라이드 하기
        @Override
        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
            super.onInitializeAccessibilityNodeInfo(host, info);
// 토글 버튼으로 클래스 네임 설정
            info.setClassName("android.widget.ToggleButton");
            //setCheckable을 적용해야 음성 안내
            info.setCheckable(true);
// 켜짐, 꺼짐 변수에 setChecked 대입
            info.setChecked(isChecked);
        }
    };

 

[HTML] 본문 바로가기 구현 시 tabindex="-1"을 함께 적용하는 경우

Webacc NV | 2021-05-07 11:56:04

일반적으로 웹페이지 가장 처음에는 본문 바로가기 링크를 구현하며 구현하는 방법은 두 가지 정도로 분류할 수 있겠습니다.

1. 해시: <a href="#content">본문 바로가기</a> 와 같은 형식으로 마크업을 하는 방식입니다.

2. 스크립트 방식: <a href="#content" onclick="$('content').tabIndex=-1;$('content').focus();return false;">본문으로 바로가기</a>

2번처럼 구현할 경우 위의 예시에서처럼 tabindex="-1" 속성을 함께 주어야 하는데 해당 속성을 메인 콘텐츠 전체를 감싸고 있는 div에 주게 되면 스크린 리더 사용자 입장에서는 화살표 키와 탭키를 함께 사용하여 내용 탐색 시에 문제가 있습니다.

예시: 본문 콘텐츠 내에 a, b, c 3개의 링크가 있고 b 링크 다음에 여러 줄의 텍스트가 있다고 가정해 봅시다.

화살표키를 이용해서 b 링크 다음에 있는 텍스트를 탐색하다가 탭키를 누르면 당연히 c 링크로 이동을 해야 할 것입니다.

그러나 초점은 a 링크로 이동합니다.

왜일까요?

tabindex="-1" 속성이 본문을 감싸는 콘텐츠에 들어가 있기 때문에 b 링크 다음에 있는 텍스트를 화살표를 이용하여 접근하는 순간 디브 자체에 초점을 받는 것이나 마찬가지가 됩니다.

따라서 b 링크 아래의 텍스트 콘텐츠를 읽고 있더라도 초점은 본문 전체에 받고 있으므로 탭키를 누르면 첫 번째 링크인 a로 이동하는 것입니다.

이러한 문제를 해결하려면 본문 바로가기는 href="#content" 와 같이 제공하거나 반드시 스크립트를 사용하려면 본문을 구성하는 첫 요소에 tabindex="-1" 속성을 적용해야 하겠습니다.

[iOS native] TableView 내에서 다른 화면으로 이동하는 버튼 구현 시 유의사항

Webacc NV | 2021-05-06 18:06:36

A 화면에서 B 화면으로 전환되는 요소들을 테이블뷰로 구현하는 경우가 있습니다.

이때 상황에 따라 여러 가지 경우의 수가 있겠지만 각 셀에 레이블을 주는 경우가 대부분일 것입니다.

그런데 보이스오버에서 B 화면으로 갔다가 다시 뒤로 돌아오면 이전에 눌렀던 요소를 선택됨이라고 읽는 경우를 가끔 발견하곤 합니다.

그것은 테이블뷰 tableView:didSelectRowAtIndexPath 메서드 구현 시 deSelectRow 메서드를 함께 포함시켜 주지 않았기 때문입니다.

이렇게 되면 선택됨 정보가 여전히 남아 있어서 보이스오버에서 이전 화면으로 돌아왔을 때 조금 전 눌렀던 버튼을 선택됨 이라고 읽게 되므로 잘못된 정보를 주게 됩니다.

따라서 테이블 뷰 구현 시 선택됨 상태를 가지는 요소가 아니라면 tableView:didSelectRowAtIndexPath 메서드와 함께 반드시 
tableView.deselectRow(at: indexPath, animated: true) // Deselects the row after tapping 메서드를 함께 포함시켜 주어야 합니다.

아래에 이와 관련된 예제를 포함하였습니다.

테이블 뷰 내에 과일, 채소가 있습니다.

각 화면에 들어갔다가 돌아오면 선택됨을 읽지 않을 것입니다.

스토리보드가 없는 뷰 컨트롤러를 만들고 테스트해 볼 수 있습니다.

import UIKit

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    let items = ["Fruit", "Vegetable"]
    var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
        self.title = "Prevent Selected Example"

        tableView = UITableView(frame: self.view.frame, style: .plain)
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.dataSource = self
        tableView.delegate = self
        tableView.tableFooterView = UIView() // Removes empty cells
        self.view.addSubview(tableView)
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = items[indexPath.row]
        cell.accessibilityTraits = .button // Setting accessibility traits to button
        return cell
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if indexPath.row == 0 {
            let fruitVC = FruitViewController()
            self.navigationController?.pushViewController(fruitVC, animated: true)
        } else {
            let vegetableVC = VegetableViewController()
            self.navigationController?.pushViewController(vegetableVC, animated: true)
        }
        tableView.deselectRow(at: indexPath, animated: true) // Deselects the row after tapping
    }
}

class FruitViewController: UIViewController {

    let fruits = ["Apple", "Banana", "Orange", "Grapes", "Pineapple"]

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .white
        self.title = "Fruits"

        for (index, fruit) in fruits.enumerated() {
            let label = UILabel(frame: CGRect(x: 20, y: 100 + (index * 40), width: 200, height: 30))
            label.text = fruit
            self.view.addSubview(label)
        }

        let backButton = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(handleBack))
        self.navigationItem.leftBarButtonItem = backButton
    }

    @objc func handleBack() {
        self.navigationController?.popViewController(animated: true)
    }
}

class VegetableViewController: UIViewController {

    let vegetables = ["Carrot", "Broccoli", "Potato", "Lettuce", "Tomato"]

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .white
        self.title = "Vegetables"

        for (index, vegetable) in vegetables.enumerated() {
            let label = UILabel(frame: CGRect(x: 20, y: 100 + (index * 40), width: 200, height: 30))
            label.text = vegetable
            self.view.addSubview(label)
        }

        let backButton = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(handleBack))
        self.navigationItem.leftBarButtonItem = backButton
    }

    @objc func handleBack() {
        self.navigationController?.popViewController(animated: true)
    }
}


 

[Android chrome] aria-current 정식 지원 관련

Webacc NV | 2021-04-27 10:54:35

지난 번에 aria-current 속성이 크롬 브라우저 카나리아 버전에서 지원이 시작되었다는 것을 공유한 적이 있습니다.

이제는 해당 속성이 크롬 정식 버전에서도 지원됩니다.

따라서 톡백 최신 버전과 크롬 브라우저 최신 버전을 사용하면 aria-current="page, true" 등의 속성이 추가된 요소의 선택 정보를 음성으로 들을 수 있습니다.

다만 크롬 정식 버전을 사용하지 않는 커스텀 웹뷰에서는 해당 속성이 지원되지 않습니다.

이는 aria-current 속성은 톡백에서 지원을 못한 것이 아니라 브라우저 API에서 지원하지 못한 이슈이기 때문입니다.

[모바일 웹] 안드로이드 모바일웹, 크롬 인스펙터에서 요소 빠르게 찾기(스크린리더 사용자를 위한 팁)

Webacc NV | 2021-04-20 12:33:46

이번 팁은 스크린 리더 사용자를 ㅜ이한 팁입니다.

스크린 리더 사용자가 접근성 테스트 업무를 하면서 이슈 리포팅을 할 때 모바일 웹을 윈도 크롬으로 요소 검사를 하기가 쉽지 않습니다.

모바일과 윈도 크롬을 연결하고 인스펙터를 실행하면 모바일 화면이 컴퓨터 화면에 표시되고 'Select an element in the page to inspect it'을 체크하면 요소 검사를 하고 싶은 텍스트를 마우스로 찍으면 해당 요소로 빠르게 이동이 가능하지만 스크린 리더에서는 해당 화면 자체의 접근이 불가능하기 때문입니다.

게다가 문자열 찾기를 실행해도 그 문자에 해당하는 요소로 자동으로 초점이 이동되지 않습니다.

하지만 NVDA 스크린 리더에서 제공하는 OCR 기능을 이용하면 어느정도 쉽게 해당 기능을 수행할 수 있습니다. 

1. 모바일과 PC를 연결하고 크롬 주소창에 chrome:inspect 입력을 합니다.

2. 여러 버튼 중 디바이스를 누른 다음 가상커서를 켠 상태에서 화살표키를 아래로 내리다보면 나의 디바이스 이름이 나옵니다. 나오지 않는다면 USB 디버깅 이슈를 확인하시기 바랍니다.

3. 디바이스에서 엔터를 누릅니다.

4. 화살표를 내리다보면 현재 내가 모바일에서 접속한 페이지가 있고 그 아래에 inspect 가 있습니다. inspect 에 커서를 맞추고 엔터를 누릅니다. 그러면 크롬 요소 검사 창이 표시됩니다.

5. 문서 처음으로 이동한 다음 버튼 단위로 이동하여 Select an element in the page to inspect it 을 체크합니다.

6. 가상커서를 켠 상태에서 g 를 누르면 Screencast view of debug target 그래픽이라고 읽어주는 곳이 있습니다.

6. NVDA 키와 r을 누르면 해당 이미지 문자 인식을 합니다.

7. 문자 인식이 완료되면 화면을 살펴본 다음 요소 검사를 하고자 하는 문자에 맞추고 마우스 포인터를 해당 글자에 보낸 다음 왼쪽 마우스 클릭 단축키를 눌러주면 해당 요소로 이동하게 됩니다.

[iOS native] 접근성 테스트 시에 세 손가락 탭을 통해 대략적인 컨테이너 파악해보기

Webacc NV | 2021-04-14 15:04:05

안드로이드 앱 접근성 진단시에는 현재 톡백이 읽어주는 요소에 대해 대략적으로 접근성 디버깅을 할 수 있으므로 특정 오류를 파악하기 수월하지만 iOS 의 경우에는 소스코드가 없는 이상 디버깅 자체가 불가능합니다.

그러나 완벽하지는 않지만 현재 보이스오버가 읽어주는 요소가 어떤 컨테이너에 속해 있는지 대략적으로 알 수 있는 방법이 있습니다.

바로 세 손가락 한 번 탭을 이용하는 것입니다.

세 손가락을 한 번 탭 하면 현재 요소가 위치한 곳이 화면 어디쯤인지, 그리고 이미지인 경우 이미지 설명 등을 제공하게 되는데 그것과 별개로 TableView, ScrollView 컨테이너에 속해 있는 경우에는 이에 대한 정보도 알려줍니다. 

스크롤뷰인 경우: x페이지 중 y페이지.

TableView 인 경우: 총 x행부터 y행까지.

NavigationBar, UIView 와 같은 일반 컨테이너에 속해 있을 경우에는 해당 정보 자체를 알려주지 않습니다. 

따라서 특정 요소에서 오류가 발생할 경우 대략적으로 그 오류가 속한 컨테이너를 알면 오류 파악이 더 용이할 수 있겠습니다.

[Android native] 볼륨 버튼으로 특정 이벤트 구현 시 SeekBar 로 읽어주지 않게 하는 예제

Webacc NV | 2021-04-12 16:45:01

지난 시간에 이어서 오늘은 볼륨 버튼으로 촬영, 항목 순서 이동 등의 이벤트를 구현할 때 톡백에서 해당 요소를 SeekBar 로 읽어주지 않도록 하는 예제를 공유하려고 합니다.

ViewCompat.setAccessibilityDelegate(view, new AccessibilityDelegateCompat() {
//roleDescription 을 사용하려면 ViewCompat.setAccessibilityDelegate 클래스를 사용해야 합니다.
            @Override
            public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
                super.onInitializeAccessibilityNodeInfo(host, info);
                info.setClassName(SeekBar.class.getName());
                info.setRoleDescription("button");
                info.setTooltipText("볼륨키로 실행 가능");
            }
});

 

[Android native] 볼륨 버튼으로 특정 동작 실행 이벤트 구현 시 참고사항

Webacc NV | 2021-04-09 18:26:20

지난번 안드로이드 톡백에서 볼륨 버튼으로 이전, 다음 배너 넘기기를 하는 방법에 대해 공유한 적이 있습니다.

해당 방법은 배너뿐만 아니라 볼륨 버튼으로 특정 요소가 실행되도록 해야 하는 경우에 편리하게 사용이 가능합니다.

예: 위/아래로 이동, 카메라 앱에서의 촬영 버튼 등.

그런데 이 방법을 사용하였을 때 겪게 되는 한 가지 고민이 있습니다.

바로 요소 유형 문제인데요.

요소 유형을 무조건 SeekBar 즉 슬라이드 라고 읽는다는 것입니다.

예를 들어 촬영 버튼을 볼륨키로도 실행할 수 있도록 한다고 했을 때 톡백에서는 촬영 슬라이더 라고 읽어주므로 정확한 역할 정보를 주지 못합니다.

이를 해결하기 위해서는 AccessibilityNodeInfoCompat 을 사용하고 setClassName 을 SeekBar 로 주는 것과 더불어 roleDescription 을 함께 주면 됩니다.

roleDescription 은 톡백에서 읽는 역할 자체를 커스텀으로 주는 것으로 기존에 있던 요소 유형을 완전히 무시하게 됩니다.

다음 팁에서는 해당 메소드를 적용한 예제를 공유하도록 하겠습니다.

[Flutter] appBar title에 관하여

Webacc NV | 2021-04-02 19:02:24

앞으로 틈나는대로 플러터의 접근성에 대한 팁을 공유할까 합니다.

플러터는 구글에서 개발한 플랫폼으로 하나의 코드만으로도 iOS, Android 두 플랫폼 모두에서 개발이 가능한 장점이 있습니다.

플러터 역시 여러 접근성 API를 제공하므로 이에 대한 정보를 틈틈이 다루어보고자 하는 것입니다.

오늘 처음으로 다루어보고자 하는 것은 상단의 화면 제목으로 사용되는 Scaffold.appBar > title 속성입니다.

해당 속성은 화면 상단의 제목을 표시하는 용도로 사용하며 안드로이드로 치면 activity 내의 setTitle이 추가된 화면과 같고 iOS로 치면 NavigationBar 영역에서의 화면 제목을 표시하는 것과 같다고 보시면 됩니다.

iOS에서는 NavigationBar 타이틀을 사용하면 보이스오버가 해당 화면 제목을 자동으로 머리말이라고 읽어주지만 안드로이드에서는 setTitle 사용해도 톡백에서 제목 유형으로 읽어주지 않는 특징이 있습니다.

그런데 플러터에서는 화면 제목을 앞에서 설명한 바와 같이 appBar title 속성으로 제공하면 안드로이드 톡백에서도 해당 요소를 제목으로 읽어줍니다. 

이것을 플러터식 접근성 용어로 설명하면 Semantics 내의 header 속성이 true로 설정됩니다.

이 부분에 대해서는 앞으로 조금씩 설명드리겠습니다.

따라서 플러터로 앱 개발 시에 화면 제목을 appBar title 속성으로 사용하면 별다른 접근성 구현 없이도 안드로이드에서도 제목으로 읽어준다는 장점이 있겠습니다.

[Android native] 음성검색 접근성 구현 시 setTitle 메소드 사용과 관련된 변경 사항

Webacc NV | 2021-03-24 09:38:32

지난 번 안드로이드 앱 음성검색 화면 접근성 구현 예제에 대해 설명을 한 적이 있습니다.

음성 검색 실행 시 액티비티 화면이 변경되는 경우에는 setTitle 값을 "" 으로 비워 두라고 말씀을 드렸습니다.

그런데 안드로이드 11에서는 그렇게 비울 경우 앱 이름을 읽는 문제가 있습니다.

따라서 setTitle 안에 " " 형태로 스페이스를 하나 비워 두어야 아무런 화면 제목을 읽지 않으며 스크린 리더 사용자도 음성검색을 제대로 수행할 수 있습니다.

해당 이슈에 대해 참고하시기 바랍니다.

[Android native] onRequestSendAccessibilityEvent 적용 예제

Webacc NV | 2021-03-19 11:49:29

며칠전에 onRequestSendAccessibilityEvent 메소드에 대해 살펴보았습니다. 

오늘은 구체적으로 해당 이벤트를 적용하여 롤링이 되고 있는 이미지뷰에 포커스 했을 때 롤링을 멈추는 방법에 대해 공유하려고 합니다.

현재 이미지 3개가 롤링되고 있는 상태이며 해당 이미지 3개는 flipper 라는 부모 뷰 안에 들어가 있다고 가정하겠습니다.

        flipper.setAccessibilityDelegate(new View.AccessibilityDelegate() {

            @Override

            public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, AccessibilityEvent event) {
                if (event.getEventType() == TYPE_VIEW_ACCESSIBILITY_FOCUSED) { //포커스가 접근할때 불리는 이벤트입니다.
                    flag = false;
                } else if (event.getEventType() == TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED) {
                    flag = true;
                }
                return super.onRequestSendAccessibilityEvent(host, child, event);
            }
        });

 

 

[Android native] 접근성 검사기 업데이트 소식(여러 화면 자동 캡처 후 접근성 결과 확인)

Webacc NV | 2021-03-16 10:05:50

접근성 검사기는 접근성에 대해 잘 모르는 사용자도 손쉽게 특정 앱의 접근성 이슈들을 자동으로 진단할 수 있도록 만든 애플리케이션입니다.

앱 설치 후 접근성 설정에서 서비스를 활성화 하면 접근성 검사 버튼이 항상 따라다니게 되므로 접근성 검사를 하고 싶은 화면에서 언제든지 검사를 수행할 수 있습니다.

물론 정교한 테스트는 접근성 테스터의 손을 거쳐야 하지만 대체 텍스트 유무, 명도대비, 터치 영역과 같은 부분들은 충분히 활용이 가능합니다.

그런데 최근 업데이트 된 접근성 검사기에서는 여러 화면을 연속으로 촬영하여 접근성 결과를 한꺼번에 출력하는 기능이 추가되었습니다.

기록을 누른 다음 다른 화면으로 이동하기 위해 탭을 하면 자동으로 화면 변화를 인지해서 촬영을 해 주는 기능입니다.

촬영을 다 마친 후에는 알리 패널에서 기록 중지를 눌러 촬영된 결과들ㅇㄹ 볼 수 있고 메일 등으로 공유도 가능합니다.

모바일 앱에서는 아직 대체 텍스트가 없는 앱들이 상당히 많습니다.

따라서 이 기능을 충분히 활용하여 대체 텍스트에 대한 누락은 미연에 방지할 수 있겠습니다.

[PC Chrome] 정식 버전에서의 aria-current 속성 정식 지원에 관한 소식

Webacc NV | 2021-03-15 16:32:59

몇 달전에 aria-current 지원에 관련하여 공유한 적이 있습니다.

PC 크롬에서 페이지 내에서 버튼이나 링크를 눌렀을 때 페이지가 새로고침 되지 않고 aria-current 속성만 업데이트 되는 경우에는 변경된 aria-current 이벤트를 제대로 캐치하지 못한다는 것이었습니다. 

이는 크롬에서 IA2_EVENT_OBJECT_ATTRIBUTE_CHANGED 이벤트를 발생시켜 주지 않아 일어나는 문제였는데 크롬에서 이를 개선해 주었으며 정식 버전에도 반영되었습니다.

따라서 89.0.4389.82 버전 이상을 사용하신다면 적어도 NVDA 에서는 aria-current 속성이 완벽히 지원되는 것을 확인할 수 있습니다.

[Android native] onRequestSendAccessibilityEvent 메소드에 관하여

Webacc NV | 2021-03-11 16:53:21

어제 아티클을 통해서 몇 차례에 걸쳐 스크롤 이벤트가 없는, 이미지가 변경되는 형식의 롤링 배너 접근성 적용에 대해 다룬다고 말씀을 드렸습니다.

우리의 목표는 접근성 포커스가 배너 이미지에 머무르면 다른 이미지로 변경되지 않도록 하는 것입니다.

그런데 이렇게 하려면 한 가지 고민이 필요합니다.

배너 이미지가 여러 개인데 그 중 하나에 포커스를 하고 있는 것을 어떻게 캐치를 하는가 입니다.

이때 사용할 수 있는 메소드가 onRequestSendAccessibilityEvent 입니다.

이 메소드는 접근성 이벤트를 캐치할 때 사용하는데 child views 즉 자손 뷰들 중에 뭔가 이벤트가 일어나는 것을 캐치할 때 사용을 합니다.

그러니까 예를 들어 이미지가 10개가 있고 그 이미지가 몇 초 간격으로 변경된다고 생각해 봅시다.

우리는 변경되는 이미지에 대한 것을 캐치해서 접근성 이벤트를 조작해야 합니다.

그러기 위해서 이미지를 품고 있는 부모 뷰에 onRequestSendAccessibilityEvent 메소드를 사용할 수 있다는 것입니다.

이렇게 하면 자손 뷰에서 뭔가 이벤트가 발생할 때 그것을 캐치하고 조작하는 접근성 구현이 가능해집니다.

다음 팁에서는 구체적인 예시를 살펴보겠습니다.

[android native] 스크롤 이벤트가 없는 롤링 배너의 접근성 개선에 대한 아이디어

Webacc NV | 2021-03-10 12:04:18

널리 아티클을 통해, 그리고 팁을 통해 몇 차례 다룬 적이 있지만 모바일에서 롤링배너가 삽입되는 경우 접근성과 관련해서 여러 가지 고민을 하게 됩니다.

그 고민 중 하나는 접근성을 위해서는 롤링되고 있는 배너를 정지할 수 있는 버튼을 두어야 하지만 모바일이라는 특성상 화면이 좁기 때문에 해당 버튼을 배치하기가 어렵다는 것입니다.

그래서 네이티브 앱에서는 스크린 리더가 켜져 있으면 배너가 정지된 상태로 제공되는 방법이 있어 이에 대해 다루었습니다.

하지만 단순히 배너를 정지 상태로 제공하는 것 말고도 한 가지 더 방법이 있습니다.

바로 접근성 포커스가 배너에 머무르고 있는 동안에는 배너가 롤링되지 않도록 하는 것입니다.

단 해당 방법은 스크롤 이벤트가 제공되지 않는 배너에서만 적용해야 합니다.

스크롤 이벤트가 적용되는 배너에서는 배너가 롤링될 때마다 톡백에서 스크롤 사운드를 발생시켜 화면 탐색에 상당한 방해를 주기 때문입니다.

그럼 다음 시간부터 몇 차례에 걸쳐 해당 방법에 대해 공유하도록 하겠습니다.

[iOS native] 커스텀 액션 적용 예제

Webacc NV | 2021-03-09 10:35:47

지난 iOS 네이티브 앱에서의 커스텀 액션 작동 방법에 이어서 오늘은 커스텀 액션 적용 방법에 대해 다루어 보려고 합니다.

사용자가 초점을 보낼 수 있는 특정 요소에 커스텀 액션을 적용하려면 UIAccessibilityCustomAction 클래스를 사용합니다.

커스텀 액션 안에는 네임, 실행 대상, 실행할 메소드 세 가지의 값이 들어갑니다.

네임은 말 그대로 보이스오버 드에서 출력될 커스텀 액션의 이름으로 삭제, 위로 이동 등이 예라 할 수 있습니다.

타겟은 해당 액션을 이중탭하여 실행했을 때 어떤 요소에 동작을 적용할 것인지를 정해주는 것으로 일반적으로는 자기 자신, 즉 self 를 사용합니다.

실행할 메소드는 사용자가 커스텀 액션에 맞추고 이중탭을 실행했을 때 어떤 동작을 할 것인지를 알려주는 것입니다.

위의 설명을 바탕으로 아래에 커스텀 액션 코드 예시를 넣어 보았습니다.

item.accessibilityCustomActions = [
                UIAccessibilityCustomAction(
                        name: "삭제",
                        target: self,
                        selector: #selector(deleteItem)
                    ),
                    UIAccessibilityCustomAction(
                        name: "위로 이동",
                        target: self,
                        selector: #selector(moveUpItem)
                    ),
                UIAccessibilityCustomAction(
                    name: "아래로 이동",
                    target: self,
                    selector: #selector(moveDownItem)
                )
            ]
        }

 

[Android native] isAccessibilityFocused() 메소드 활용하기

Webacc NV | 2021-03-05 16:51:40

안드로이드 앱 접근성을 구현하다보면 현재 접근성 서비스가 포커스 하고 있는 요소를 가지고 와야 하는 경우가 있을 수 있습니다.

예를 들면 현재 콘텐츠를 다운받는 화면에 있다고 가정해 봅시다.

다운로드 진행률이 ProgressBar 클래스로 구현되어 화면에 표시되고 있다면 안드로이드 11 톡백에서는 사용자가 특별한 접근성 구현 없이도 초점을 이동하여 퍼센트 확인이 가능할 것이고 10 이하버전에서는 변경되는 퍼센트 값에 대한 대체 텍스트를 해당 클래스에 넣어 줌으로써 접근성 이슈는 해결됩니다.

그런데 다운로드 진행률을 ProgressBar 클래스를 사용하지 않고 아이콘으로 표시되도록 구현했다면 해당 아이콘 뷰에 대체 텍스트 형태로 진행률을 표시해 주어야 합니다.

문제는 대체 텍스트 형태로 구현을 하면 사용자가 해당 아이콘 뷰에 포커스 했을 때 현재 퍼센트는 알 수 있지만 변경되는 퍼센트를 바로바로 확인할 수 없습니다.

announceForAccessibility 메소드를 사용하는 것이 해결책이 될 수 있지만 그럴 경우 사용자가 해당 아이콘에 포커스 하고 있지 않을 때에도 계속 변경되는 퍼센트를 읽어 이 또한 정신 없게 만드는 원인이 됩니다.

따라서 좀더 사용성을 고려했을 때 초점이 퍼센트 아이콘에 가 있을 때에만 업데이트 되는 퍼센트 정보를 읽게 하는 것이 접근성을 높이는 방법 중 하나입니다.

이때 사용할 수 있는 조건문이 isAccessibilityFocused() 입니다.

즉 해당 아이콘 뷰가 접근성 초점을 받고 있다면 무엇인가를 하라는 명령어 입니다.

따라서 우리는 해당 조건문 안에서만 announceForAccessibility 메소드를 통해 업데이트 되는 퍼센트 정보를 읽게 할 수 있으며 사용자가 초점을 다른 곳으로 이동하면 해당 정보는 읽지 않게 됩니다..

[iOS native] 커스텀 액션 이용하기

Webacc NV | 2021-03-03 18:18:01

지난번에는 몇 차례에 걸쳐 안드로이드에서의 접근성 커스텀 액션에 대해 살폈습니다.

이번에는 iOS 에서의 커스텀 액션 사용방법과 구현 방법에 대해 함께 살펴보고자 합니다.

우선 커스텀 액션에 대한 개념은 안드로이드와 같으므로 여기서는 설명하지 않습니다.

오늘 함께 살펴보고자 하는 것은 iOS에서는 커스텀 액션을 어떤 식으로 접근할 수 있는가 입니다.

개발자가 커스텀 액션을 구현한 요소 혹은 시스템 자체적으로 커스텀 액션을 가진 요소에 포커스를 맞추면 보이스오버의 로터가 동작 로터로 자동 변경됩니다.

로터란 보이스오버를 컨트롤할 수 있는 여러 옵션으로 한 글자씩 읽기, 단어 단위로 읽기, 음성 속도 변경하기 등이 포함됩니다.

로터는 두 손가락 시계 혹은 시계 반대방향 돌리기를 통해서 다른 로터로 변경하고 한 손가락 위 또는 아래 쓸기를 통해 옵션을 변경할 수 있는데 예를 들어 사용자가 음성 속도 로터로 변경했다 하더라도 커스텀 액션을 가진 콘텐츠에 위치하면 한 손가락 위 또는 아래 쓸기 옵션이 커스텀 액션으로 변경됩니다.

커스텀 액션이 있는 요소에 포커스 하면 상세정도에서 동작 피드백 설정을 어떻게 했느냐에 따라 음성으로 동작이 있음을 알려주거나 사운드로 알려주거나 아무런 피드백도 하지 않습니다.

한 손가락 위 또는 아래 쓸기로 실행할 액션을 선택하고 이중탭하면 그 요소가 자체적으로 가지고 있는 클릭 이벤트가 아닌, 커스텀 액션으로 구현한 이벤트가 실행됩니다.

다음 팁에서는 커스텀 액션 구현 방법에 대해 설명하겠습니다.

[Android Web] TalkBack에서의 aria-current 속성 지원 소식 관련

Webacc NV | 2021-02-25 09:48:36

웹에서 aria-current는 탭 컨트롤을 제외한 내비게이션 메뉴, 회원가입과 같은 곳에서의 현재 단계를 표시할 때 사용하는 속성으로 여러 요소 중 어떤 요소가 선택된 요소인지를 알릴 때 사용합니다.

그런데 모바일 웹에서 톡백으로는 해당 속성이 지원되지 않아 aria-current 속성을 넣어 준다 하더라도 안드로이드 스크린 리더 사용자는 어떤 요소가 선택된 요소인지 알 수 없었습니다.

그런데 이번에 구글에서 이 문제를 수정해 주었습니다.

해당 이슈는 톡백 문제라기보다는 크롬 웹뷰에서 aria-current 속성을 톡백에게 제대로 전달하지 못해 발생한 문제로 현재는 크롬 카나리에서 수정된 버전을 체험해 볼 수 있습니다.

따라서 약 두어 달 정도 지나면 정식 버전에도 적용되지 않을까 예상해 봅니다.

[Android native] 안드로이드 11에서의 RatingBar 접근성 지원 업데이트

Webacc NV | 2021-02-23 09:28:14

예전에 이곳 팁을 통하여 RatingBar 클래스의 접근성 구현에 대해 다룬 적이 있습니다.

RatingBar 는 평점 데이터를 받을 때 사용하는 클래스로 일반적으로 리뷰 작성 화면에서 어느 정도의 만족도를 줄 것인지를 물어볼 때 흔히 사용합니다.

그런데 안드로이드 10 버전까지는 접근성 구현을 하지 않으면 평점을 올리거나 내리는 것을 드래그 형식으로 제스처를 해야 했기 때문에 스크린 리더 사용자가 조작하기가 어려워 이를 대응할 수 있는 방법을 설명했었습니다.

그러나 안드로이드 11에서는 RatingBar 구현 시 시스템 자체적으로 볼륨키를 이용하여 평점을 올리거나 내릴 수 있도록 수정되었습니다.

따라서 AccessibilityNodeInfo 클래스 변경은 안드로이드 10 이하에서만 적용하면 됩니다.

다만 각 퍼센트별 1점, 별 한 개 등의 점수는 contentDescription 을 통하여 마크업해 주는 것이 좋습니다.

[iOS native] frequentlyUpdated trait 특징

Webacc NV | 2021-02-22 10:13:11

각 플랫폼마다 특정 영역의 업데이트 되는 텍스트를 스크린 리더가 자동으로 읽어주도록 하는 속성들을 제공하고 있습니다.

웹은 우리가 너무나 잘 알고 있는 aria-live 이며 안드로이드 역시 accessibilityLiveRegion 입니다. 

iOS에서는 frequentlyUpdated accessibilityTrait 속성이 이와 비슷하다고 할 수 있습니다.

그러나 이 속성은 웹, 안드로이드에서 말하는 라이브리전과는 차이가 있습니다.

웹, 안드로이드의 라이브리전은 초점과 상관 없이 개발자가 지정해 놓은 영역의 콘텐츠가 업데이트 되면 자동으로 읽어주는 반면 iOS의 frequentlyUpdated 는 지정된 요소에 초점을 유지하고 있을 때만 업데이트 되는 콘텐츠를 읽어주도록 합니다.

따라서 초점 받은 객체와 상관 없이 웹이나 안드로이드처럼 업데이트 되는 콘텐츠를 읽도록 하려면 AccessibilityAnnouncement 를 사용해야 합니다.

[iOS native] adjustable trait 사용 시에는 업데이트 값을 accessibilityValue로 마크업

Webacc NV | 2021-02-19 12:45:45

iOS 에는 안드로이드와 달리 대체 텍스트에 해당하는 accessibilityLabel 외에 accessibilityValue가 있습니다.

말 그대로 레이블에 대한 값이 있을 경우에는 이를 대체 텍스트 안에 넣지 않고 밸류 안에 넣도록 권고하고 있는 것입니다.

특히 커스텀으로 슬라이더를 만들어서 접근성 구현 시에는 변경되는 값은 반드시 accessibilityValue 값으로 넣어 주어야 합니다.

그렇게 하지 않고 대체 텍스트 안에 다 넣으면 사용자가 슬라이더를 업데이트 할 때 업데이트 되는 정보를 읽어주지 못합니다.

예를 들어보겠습니다.

뮤직 플레이어에서 재생 구간 조절 슬라이더를 UISlider를 사용하지 않고 커스텀 클래스로 슬라이더를 구현했다고 가정해 봅시다.

그러면 이 슬라이더의 접근성 구현을 위해 대략적으로 다음과 같은 작업들이 필요할 것입니다.

1. 보이스오버 사용자가 한 손가락 위 또는 아래 쓸기를 통해 재생 구간 이동을 할 수 있도록 하기 위하여 해당 요소의 accessibilityTrait 를 .adjustable로 설정.

2. 한 손가락 위로 쓸기 혹은 아래로 쓸기를 할 때의 이벤트를 구현하기 위해 accessibilityIncrement, accessibilityDecrement 구현.

3. 해당 요소에 재생 구간 이라는 레이블과 현재 시간을 나타내는 밸류 값 적용.

3번에서 재생구간의 값을 나타내는 시간을 대체 텍스트로만 제공하면 보이스오버 사용자가 한 손가락 위 또는 아래 쓸기를 했을 때 변경되는 값을 실시간으로 들을 수 없게 됩니다.

이는 슬라이더에서 값이 변경될 때는 accessibilityValue 정보만 가지고 와서 실시간으로 음성 출력하기 때문입니다.

따라서 값에 해당하는 정보는 accessibilityValue 안에 넣어 주는 것이 필요합니다.

[iOS native] playSound trait 기능에 관하여

Webacc NV | 2021-02-18 11:41:53

오랜만에 iOS 접근성 API 팁을 공유합니다.

보이스오버에서는 사용자가 보이스오버 사운드를 오프로 설정하지 않는 이상 특정 요소를 이중탭하면 보이스오버 클릭 사운드를 출력합니다.

이는 해당 요소가 가지고 있는 이벤트 실행과는 별개로 보이스오버는 해당 요소에 대한 클릭 이벤트를 수행했다는 알림을 제공해 주는 피드백입니다.

그런데 음성 검색 화면에서 음성 듣기를 누를 때 자체 사운드 피드백을 가지고 있다고 가정해 봅시다.

그러면 사용자가 음성 듣기를 이중탭하면 보이스오버 클릭 사운드, 음성 듣기 시작 사운드가 동시에 출력될 것입니다.

위의 예시와 같이 특정 객체를 이중탭할 때 자체 사운드를 가지고 있어서 보이스오버의 클릭 사운드를 필요로 하지 않는 경우에는 .playSound accessibilityTrait 을 사용할 수 있습니다.

자체 사운드를 가지고 있는 요소에 accessibilityTraits = [.button,.playSound] 와 같이 적용하면 그 요소를 이중탭할 때는 보이스오버 클릭 사운드가 출력되지 않게 됩니다.

[Android native] 안드로이드 11에서의 ProgressBar 접근성 지원 업데이트

Webacc NV | 2021-02-17 10:47:22

안드로이드에서 프로그레스바 클래스 사용시 화면에 표시되는 퍼센트에 대한 대체 텍스트를 추가 하지 않으면 톡백에서 퍼센트 정보를 읽지 못한다는 팁을 공유한 적이 있습니다.

그런데 안드로이드 11에서는 ProgressBar 클래스에 대한 접근성 지원이 업데이트 되어서 대체 텍스트가 없어도 퍼센트 요소에 포커스 하면 현재 표시된 퍼센트를 음성으로 알려줄 수 있도록 수정되었습니다.

따라서 ProgressBar 요소에 대한 퍼센트 정보 대체 텍스트는 안드로이드 10 이하 버전에서만 삽입되도록 다음 예시와 같이 구현할 수 있겠습니다.

                if (Build.VERSION.SDK_INT < 30) {
                    pgsBar.setContentDescription("0%");
                }

 

[Android native] 커스텀 액션 외에 톡백에서 자동으로 상황에 맞는 메뉴를 추가하는 경우 리스트 정리

Webacc NV | 2021-02-16 10:10:49

3회에 걸쳐 안드로이드에서의 접근성 커스텀 액션에 대해 살펴보았습니다.

그런데 커스텀 액션을 리서치 하다보면 한 가지 질문이 제기될 수 있습니다.

커스텀 액션 외에 특정 컨트롤에서 톡백의 로컬 컨텍스트 메뉴를 열면(갤럭시 안드로이드 11에서는 음성안내지원메뉴로 통합) 이전 페이지, 다음 페이지, 수정 옵션 등의 상황에 맞는 옵션이 표시되는 경우들이 있는데 이것은 접근성 구현을 별도 한 것인가 입니다.

정담은 그렇지 않습니다.

커스텀 액션은 접근성 구현을 별도로 한 것이지만 다음의 레이아웃 혹은 컨트롤을 사용하면 해당 컨트롤에 대한 상황에 맞는 메뉴를 접근성 서비스에서 지원해 줍니다.

1. ViewPager를 사용하여 여러 페이지 구현: 이전 페이지, 다음 페이지로 가기 메뉴를 표시해 줍니다.

2. URLSpan 혹은 clickableSpan: 해당 속성에는 링크 URL이 들어가게 되는데 웹 브라우저로 연결할 수 있는 링크 열기를 실행해 줍니다.

3. EditText: 복사, 잘라내기, 붙여 넣기와 같은 편집 관련 수정 옵션을 표시해 줍니다. 단 EditText에 contentDescription을 사용하면 해당 메뉴는 표시되지 않습니다.

[Android native] TabLayout 접근성 지원 업데이트 관련

Webacc NV | 2021-02-15 09:26:11

iOS는 TabBar를 통해, 안드로이드는 TabLayout을 통해 탭을 구현하는 경우가 많습니다.

그런데 안드로이드는 네이티브 탭레이아웃을 사용하더라도 톡백이 각 요소를 탭이라고 읽어주지 않아 접근성 구현이 추가적으로 필요했습니다.

한 가지 기쁜 소식은 탭 레이아웃을 추가할 때 최신 안드로이드 라이브러리를 사용하면 접근성 구현을 별도로 하지 않더라도 톡백에서 각 탭의 요소 유형을 탭이라고 읽어주며 현재 포커스 하고 있는 탭이 총 몇 번째 탭 중 몇 번째 탭인지도 함께 알려줍니다.

즉 모듈 라이브러리에 다음과 같이 material 버전이 1.2.1 이상이어야 합니다.

implementation 'com.google.android.material:material:1.2.1'

따라서 접근성 테스트 시에 탭레이아웃을 사용했음에도 탭이라고 읽어주지 않는다면 모듈의 material 버전을 확인하고 업데이트해 보시기 바랍니다.

[Android native] 커스텀 액션 적용 예제

Webacc NV | 2021-02-10 18:11:21

두 번의 팁에 걸쳐서 커스텀 액션의 필요성과 커스텀 액션을 실행하는 방법에 대해서 살펴보았습니다.

오늘은 커스텀 액션을 직접 만들어보는 예제를 공유하려고 합니다.

1. res > value > actions.xml 파일을 만듭니다. 

2. xml 안에는 다음 예시와 같이 각 액션의 네임을 지정해 줍니다. 이것은 AccessibilityNodeInfo 객체 내에 AccessibilityAction 으로 추가될 이름입니다.

   <item name="action_move_up" type="id"/>

 

3. 액션을 만들고자 하는 객체에 다음 예시와 같이 AccessibilityNodeInfo 객체를 만들고 그 객체 안에서 xml에서 만든 액션을 추가합니다.

ViewCompat.setAccessibilityDelegate(contentText, object :AccessibilityDelegateCompat(){
               override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfoCompat?) {
                   super.onInitializeAccessibilityNodeInfo(host, info)
                   info?.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.action_delete, "삭제"))
                   info?.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.action_move_up, "위로 이동"))
}            

 

참고: 액션에 대한 글자가 좋아요, 좋아요 취소와 같이 상황에 따라 변경되는 경우에는 likeAction 과 같이 String 객체를 만들어 상황에 맞게 대입합니다.

4. 마지막으로 만들어 놓은 각 액션을 사용자가 실행하면 무엇이 동작되게 할 것인지를 정의해야 하므로 다음 예시와 같이 performAccessibilityAction 메소드를 사용하여 각 액션에 따른 동작을 정의합니다.

               override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean {
                   when(action){
                       R.id.action_delete -> {
                           itemDeleteListener?.onItemDelete(adapterPosition)
                       }
                       else -> {
                           return super.performAccessibilityAction(host, action, args)
                       }

 

[Android native] 커스텀 액션 실행 방법

Webacc NV | 2021-02-09 09:30:53

커스텀 액션을 적용하는 기술에 대해 알아보기 전에 우선 톡백에서 커스텀 액션을 실행하는 방법부터 먼저 설명하겠습니다.

커스텀 액션은 톡백에서 작업 이라는 용어로 번역되어 있습니다.

따라서 특정 객체에 포커스 했을 때 그 객체가 커스텀 액션을 가지고 있는 경우에는 톡백에서 '작업 사용가능, 위쪽 오른쪽 스와이프 동작으로 보기'라는 힌트 메시지를 출력해 줍니다.

그러면 해당 객체에는 이중탭하여 실행할 수 있는 기본 동작 외에 이전 팁에서 설명한 것과 같이 톡백과 같은 접근성 서비스에서 실행할 수 있는 추가 작업들이 존재한다는 이야기가 됩니다.

따라서 톡백의 안내와 같이 위쪽 오른쪽 연속 쓸기 동작을 하면 로컬 컨텍스트가 열리고 그 안에서 작업을 선택하면 개발 시 적용한 커스텀 액션 목록이 출력되게 됩니다.

참고: 

1. 조만간 널리 아티클을 통해 다루겠지만 안드로이드 11버전이 탑재된 갤럭시에 포함된 톡백에서는 로컬 컨텍스트메뉴와 글로벌 컨텍스트메뉴가 음성안내지원메뉴로 통합되었으므로 작업 역시 해당 메뉴에서 호출할 수 있습니다.

2. 톡백 설정, 즉 음성안내지원설정에서 맞춤동작설정에 들어가면 커스텀 액션을 호출하는 제스처를 별도로 할당할 수도 있습니다.

이렇게 하면 굳이 톡백 메뉴를 거치지 않고 작업 리스트를 바로 호출할 수 있습니다.

[Android native] 커스텀 액션 활용에 관하여

Webacc NV | 2021-02-05 18:39:06

앞으로 몇 차례에 걸쳐서 커스텀 액션에 대해 다루어 보려고 합니다.

커스텀 액션은 접근성 서비스에서만 클릭할 수 있는 액션을 별도로 만드는 것을 말합니다.

커스텀 액션을 만드는 이유는 두 가지가 있습니다.

1. 한 콘텐츠에 대해 반복되는 여러 하위 버튼들이 많을 때: 팟캐스트 리스트가 있다고 생각해 봅시다. 

각 팟캐스트를 누르면 콘텐츠가 재생됩니다. 그런데 각 팟캐스트마다 좋아요, 공유, 댓글 달기, 리스트에서 삭제, 폰으로 다운로드와 같은 객체들이 있다고 생각해 봅시다.

그러면 스크린 리더 사용자가 10개의 팟캐스트를 탐색하려면 한 손가락 옆으로 쓸기를 몇 번 해야 할까요?

2. 드래그 & 드롭과 같이 기존 제스처 사용으로는 수행하기 어려운 객체인 경우: 아이템의 순서를 이동하는 등의 드래그 & 드롭 기능은 스크린 리더 사용자가 수행하기 매우 어렵습니다.

그렇다고 화면 상에 각 요소마다 위로 이동, 아래로 이동이라는 버튼을 두는 것은 모바일이라는 환경을 고려할 때 디자인 배치가 쉽지 않습니다.

다음 팁에서는 커스텀 액션을 사용하기 위한 톡백 환경설정에 대해 다루겠습니다.

[iOS native] accessibilityElementsHidden 메소드 사용에 관하여

Webacc NV | 2021-01-28 10:02:47

예전에 iOS 네이티브에서 accessibilityViewIsModal 에 대해 소개한 적이 있습니다.

딤드 레이어를 구현할 때 해당 속성을 사용하면 선언된 컨테이너와 같은 계층에 있는 다른 컨테이너 내의 요소들은 보이스오버에서 초점을 받지 않도록 할 수 있어 접근성 구현 시 자주 사용됩니다.

그런데 레이어가 하나의 컨테이너만 포함하지 않는 경우에는 이 속성을 사용할 수 없습니다.

레이어가 열렸을 때 레이어 컨테이너와 닫기 컨테이너가 분리되어 있을 경우가 대표적인 예라 하겠습니다.

이 때 사용할 수 있는 속성이 accessibilityElementsHidden 입니다.

이 속성은 웹에서 aria-hidden 속성과 비슷합니다.

즉 해당 속성을 주면 그 속성이 적용된 하위 모든 컨테이너의 초점을 숨깁니다.

따라서 레이어가 여러 컨테이너에 걸쳐 있을 경우에는 accessibilityElementsHidden을 사용하여 레이어가 관련이 없는 모든 컨테이너를 보이스오버 초점에서 제외할 수 있습니다.

[HTML] overflow hidden으로 화면의 콘텐츠를 숨기는 경우

Webacc NV | 2021-01-27 09:36:38

웹페이지에서 상황에 따라 화면에서 콘텐츠를 숨기는 방법은 여러 갖가 있습니다.

visibility hidden, display none, HTML hidden 태그 등을 사용하는 경우에는 별도 접근성 대응을 하지 않아도 숨겨진 콘텐츠는 스크린 리더에서도 읽지 않습니다.

그러나 overflow hidden으로 콘텐츠를 숨기는 경우에는 화면에서는 해당 콘텐츠가 보이지 않지만 스크린 리더에서는 화면에서는 보이지 않는 콘텐츠를 읽을 수 있는 문제가 있습니다.

따라서 overflow hidden을 사용하여 특정 콘텐츠를 보여주거나 숨기는 경우에는 이에 대한 접근성 대응이 필요합니다.

1. 숨겨진 콘텐츠가 키보드 초점을 받지 않는 텍스트 콘텐만 존재하는 것우에는 aria-hidden true 속성을 사용합니다.

2. 숨겨진 콘텐츠가 텍스트가 아닌 링크와 같은 초점을 받는 객체인 경우에는 aria-hidden true 속성과 함께 tabindex -1 속성을 함께 적용합니다.

물론 화면에서 콘텐츠가 다시 보여지는 경우에는 이러한 속성들은 false가 되어야 할 것입니다.

아래는 텍스트 콘텐츠가 overflow hidden 으로 숨겨진 콘텐츠 접근성 적용과 관련된 예시 입니다.

스크린 리더를 실행한 상태에서 테스트 해 보시기 바랍니다.

overflow hidden 접근성 적용 예제

[Android native] isTextEntryKey 적용 예제

Webacc NV | 2021-01-26 09:28:20

지난 시간에 이어서 오늘은 각각의 버튼으로 된 커스텀 키패드가 있다고 가정하고 해당 키패드를 이중탭이 아닌 손가락을 떼면 바로 입력이 되게 하는 예제를 공유하도록 하겠습니다.

키보드는 그리드 레이아웃 매니저를 이용하였고 keyButton 객체로 키보드를 구성했다고 가정합니다.

            keyButton.accessibilityDelegate = object : View.AccessibilityDelegate() {
                override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfo?) {
                    super.onInitializeAccessibilityNodeInfo(host, info)
                    info?.isTextEntryKey = true
                }
            }

 

[Android native] isTextEntryKey 접근성 메소드 소개

Webacc NV | 2021-01-22 15:02:36

안드로이드 플랫폼에서는 기본적으로 톡백에서 키보드를 입력할 때 입력하고자 하는 글자에 포커스 하고 손가락을 떼면 바로 입력이 되는 방식을 취하고 있습니다.

iOS 플랫폼에서는 보이스오버에서 키보드 입력 시 손가락을 떼면 바로 입력이 되게 할 것인지 아니면 일반적인 접근성 제스처와 같이 두 번 탭을 해야 입력이 되게 할 것인지를 사용자가 설정할 수 있습니다.

이처럼 플랫폼별 접근성 지원에서의 특징을 우리는 이해할 필요가 있습니다.

그런데 안드로이드의 경우 송금을 하기 위한 금액 입력 키패드나 인증서 비밀번호 입력을 위한 키패드를 커스텀 키보드로 구현하는 경우가 종종 있습니다.

이러한 커스텀 키보드는 버튼으로 만들어지는 경우도 있고 구현하는 방법은 다양합니다.

문제는 이러한 커스텀 키보드 사용시에는 사용자가 일일이 각각의 키에서 이중탭을 해야 하는 번거로움이 있다는 것입니다.

만약 톡백에게 이것이 키보드야 라고 알려줄 수 있다면 일반 키보드처럼 특정 키에서 손가락을 떼면 바로 입력이 되도록 톡백이 처리를 해줄 것입니다.

이때 사용할 수 있는 메소드가 바로 isTextEntryKey() 입니다.

이 메소드를 적용하면 톡백은 다음과 같이 처리를 해줍니다.

1. 요소 유형을 읽지 않습니다. 

즉 버튼으로 키보드를 만들었다면 각 숫자를 버튼이라고 읽어줄 것입니다. 

그러나 키보드라는 것을 톡백에게 알려주면 버튼이라는 요소 유형 정보를 읽지 않습니다.

2. 활성화 하려면 이중탭하세요 라는 힌트 메시지를 출력하지 않습니다.

3. 키보드에서 손가락을 떼면 바로 입력이 됩니다.

다음 팁에서는 구체적으로 적용하는 예시에 대해 살펴보겠습니다.

[Android native] screenReader focusable 사용 예시와 사용 시 주의사항

Webacc NV | 2021-01-07 10:03:36

지난 팁에서는 screenReaderFocusable 사용의 필요성에 대해 함께 생각해 보았습니다.

오늘은 해당 속성 사용 방법과 유의할 점에 대해 생각해 보겠습니다.

1. screenReaderFocusable true 속성을 사용하면 해당 컨테이너 하위의 모든 view 들이 하나의 초점으로 제공되므로 하위의 모든 view를 하나의 초점으로 합치는 것에 문제가 없을 경우에만 사용해야 합니다.

2. 반드시 하위의 각 view에는 focusable 속성을 false로 줍니다.

다만 해당 컨테이너 레이아웃에 contentDescription, 즉 대체 텍스트를 마크업하는 경우에는 importantForAccessibility YES 속성은 없어도 됩니다. 다만 이렇게 대체 텍스트와 screenReaderFocusable true 속성을 함께 주면 하위의 모든 뷰들을 읽는 것이 아니라 해당 컨테이너의 대체 텍스트만 읽게 됩니다.

3. 별개의 클릭 속성을 수신하는 경우는 초점을 하나로 합칠 수가 없습니다.

[HTML 접근성 기초 & 해외 아티클 번역] 문서와 콘텐츠의 언어 (2/2)

Webacc NV | 2021-01-06 14:04:29

[지난글] [HTML 접근성 기초 & 해외 아티클 번역] 문서와 콘텐츠의 언어 (1/2)

[원문 링크] WebAim: Document and Content Language

지난 글에 이어 "문서와 콘텐츠의 언어" 아티클의 번역 2부를 올립니다.

 

적절한 언어 태그 고르기

국제 규격에 의해 정의된 8000개 이상의 언어 코드는 다양한 언어, 방언, 지역 등을 포함합니다. IANA(인터넷 할당 번호 관리 기관: Internet Assigned Numbers Authority)는 가능한 유효한 값의 정규 레지스트리를 제공합니다. W3C에서는 언어 태그를 고르기 위한 훌륭한 가이드를 제공합니다. 또한, 이 언어 하위 태그 조회 도구를 사용하여 유효한 lang 속성 값에 대해 검색할 수 있습니다. 사용된 언어 값이 접근성을 가장 잘 지원하는지 확인하려면 아래 지침을 반드시 읽으십시오.

 

기본 언어

기본언어는 웹 콘텐츠의 주요 언어입니다. 거의 모든 기본 언어는  두 자리 코드로 사용할 수 있습니다. 예를들자면, lang="en"은 영어, lang="de"는 독일어, lang="zh"는 중국어, 그리고 lang="ar"은 아랍어입니다.

lang 속성 값은 가능한 짧게 유지합니다. 두 자리 기본 언어 코드가 콘텐츠 언어를 식별하기에 충분하다면, 해당 코드를 사용하세요.

 

많은 기본 언어는 다른 보조 언어나 방언을 가지고 있습니다. 영어는 예를들어 영국, 호주 그리고 인도와 같은 변형된 언어를 가지고 있습니다. 중국어는 표준 중국어(Mandarin)과 광동어(Cantonese), 수 없이 많은 다른 언어 또는 방언이 있으며, 그 중 일부는 서로 알아들을 수 없습니다. 그럼에도 불구하고 웹페이지에서 기본 언어를 지정하는 것 만으로도 충분히 이를 지원할 수 있습니다.

 

간단히 말해서, 대부분의 페이지 콘텐츠에 대한 언어는 적절한 자리 기본 언어 코드로 식별될 있습니다. 아주 드믈게 기본 언어 코드에 세 자리 코드를 사용할 수 있지만, 두 자리 코드가 없는 경우에만 사용할 수 있습니다.

 

ISO 표준에서 일부 기본 언어에 대해 세 자리 코드를 정의할 수 있지만(예: 스페인어 "spa") 이러한 코드는 IANA 레지스트리에서 찾을 수 없으며, 스크린 리더에서 이러한 코드에 대한 지원이 매우 미흡합니다.

 

확장 언어

보조 언어나 확장 언어는 아랍어(ar), 중국어(zh), 말레이어(ms), 스와힐리어(sw), 우즈베크어(uz), 콘칸어(konkani), 그리고 수화 언어(sgn) 일곱 가지의 기본 언어에서 사용됩니다. 예를들어 광동어와 중국 표준어는 중국어(zh)의 확장 언어들입니다. 이는 광동어를 "zh-you", 중국 표준어인 lang="zh-cmn" 또는 세 자리 언어 식별코드, "you"와 "cmn"과 같이 같이 명시할 수 있게 합니다.

스크린리더의 확장 언어에 대한 지원은 매우 미흡합니다.  위와 같이 기본 언어만을 사용하거나, 필요하다면 아래 섹션들을 참고하여 적절한 지역 하위 태그를 사용하세요.

 

문자 하위 태그

가끔, 언어의 기본 언어의 문자와 다른 문자를 사용하여 콘텐츠를 표현해야 하는 경우도 있습니다. 예를 들어서 '汉语'는 중국어 간자체로 "중국어"라는 단어입니다. 이것을 라틴어로 표기하면 "Hànyǔ"입니다. 'Hànyǔ'라는 단어는 lang="zh-Latn"을 사용하여 중국어로 식별되게 할 수 있습니다. 문자 하위태그는 항상 기본 언어 코드 뒤에 하이픈과 함께 네 자리 코드로 추가합니다.

 

그런데, 스크린리더의 문자 하위 태그 지원이 미흡합니다. 그리고, 아주 드믈게 필요합니다.  스크린리더가 문자 식별자를 무시하고 기본 언어를 적용하는 경우가 많으며, 일반적으로 식별하는 것에 완전히 실패합니다. 예를들어 영어 페이지에서 <p lang="zh-Latn"> 'Hànyǔ'</p>는 "zh"라는 기본 언어 때문에 중국어로 간주될 수 있지만, 라틴어는 한자가 아니기 때문에 내용을 읽을 수 없을 겁니다(스크린리더는 라틴 알파벳이 아닌 한자를 예상합니다). 이러한 사례에서는 언어 속성의 값을 모두 생략하면 해당 페이지에 정의된 언어인 영어로 라틴 문자를 제대로 읽게 됩니다.

간단히 말해서, 문자 하위태그 사용은 피하십시오.

 

지역 하위 태그

스페인어와 맥시코의 스페인어 차이를 강조하는 페이지와 같이 다양한 방언 또는 하위 언어로 콘텐츠를 구분해야 할 필요가 있거나, 지역적 차이가 뚜렷한 방언과 일치하는 서면 콘텐츠가 있다면 지역 하위 태그를 사용할 수 있습니다. 예를들어, lang="es-ES"는 맥시코에서 읽히는 스페인어를 나타내는 lang="es-MX"와 달리 스페인에서 일반적으로 사용되거나 읽히는 반도 스페인어를 식별합니다.

 

만약 스크린리더가 스페인 반도 및 멕시코 스페인어에 해당하는 음성이 모두 설치되어있는 등, 지역적인 차이를 지원한다면 스크린리더에서 적절한 방언으로 전환하여 읽을 수 있습니다.

 

그런데, 지역 하위 태그는 일반적으로 무시됩니다. 특히 스크린리더의 기본 설정 언어가 기본 언어과 일치하면 그렇습니다.

이는 사용자가 동일한 언어의 다른 방언보다, 기본 방언을 더 선호하고, 더 잘 이해할 것으로 추정되기 때문입니다.

예를 들어 영국의 수많은 스크린 리더 사용자들은 페이지가 lang="en-us"로 지정되어 있더라도 미국 웹 사이트에서 일반적으로 영국 영어 음성을 들을 수 있습니다.

 

지역 하위 태그는 서로 다른 방언으로 내용을 구분해야 경우에만 사용해야 합니다. 중국 표준어와 광동어로 된 페이지(문자는 같지만 서로 이해할 수 없는 경우)는 일반적으로 각각 lang="zh-cn", lang="zh-hk"를 사용하여 콘텐츠를 차별화할 수 있습니다. 지역 하위 태그는 확장 언어 태그보다 훨씬 안정적으로 지원됩니다. 영어의 다양한 방언은 서로 이해할 수 있고, 스크린 리더 사용자가 선호하는 방언을 정의할 수 있기 때문에 영국, 호주, 미국, 또는 다른 영어 버전을 제공하는 사이트에서는 일반적으로 lang="en"을 사용하는 것 만으로도 충분합니다.

 

짧은 요약

허용되는 언어 속성 값에는 다른 변형이 있지만 위의 규칙은 대부분의 웹 페이지 내용에 적용됩니다.

- 언어 식별자를 가능한 짧게 유지합니다. 대부분의 경우 자리 언어 식별자로 충분하고 최적입니다.

- 일반적으로 자리 언어 식별자, 확장 언어 하위 태그 문자 하위 태그는 피해야 합니다.

- 지역 하위 태그는 지역 방언과 기본 언어와  반드시 구별되어야 하는 상황에서 사용될 있습니다.

스크린리더 지원

일반적으로 두 글자 lang 속성값은 스크린리더 지원에 적합합니다. 세자리, 확장, 문자 및 지역 하위 태그에 대한 지원은 사용 중인 브라우저 및 스크린 리더 및 지원 및 설치된 언어 음성에 따라 다릅니다. 지원 여부가 의심스러우면 테스트해야 합니다.

Span 또는 <img> 요소와 같은 인라인 요소에서의 언어변경 지원여부 또한 변수가 많습니다. 가능하면, p, blockquote같은 블록 형태의 요소에 lang 속성을 정의하는 것이 좋습니다.

정의된 언어를 읽으려면 스크린리더가 해당 언어를 지원해야 합니다. 현대의 모든 스크린리더는 여러 언어를 지원합니다. 몇몇 스크린리더에서는 유저가 직접 언어와 언어팩을 설치하거나 설정해야만 합니다.

스크린리더에서 일치하는 언어 음성이 설치되지 않았거나, 지원되지 않는 언어를 발견하면, 일반적으로 콘텐츠의 언어를 명시해 줍니다. 예를 들어, 스페인 어 언어 음성이 비활성 상태이거나 설치되지 않았다면 스크린 리더에서는 “스페인어”라고 음성으로 안내합니다.

 

스크린 리더는 대부분 정의된 언어를 지원하지 않아도 발음이 가능한 콘텐츠 언어라면 읽기를 시도합니다. 폴란드어 컨텐츠를 예를 들자면, 폴란드어는 라틴 계열의 문자로 쓰이므로, 영어 기본 음성으로 설정된 스크린리더가 읽을 수 있습니다(폴란드어를 처음 배우는 초급반처럼 다소 부적절한 발음이나 억양등으로). 반면에 한자는 영어로 바로 발화할 수 없습니다. 그러므로 스크린리더는 중국어를 읽지 않을 것이지만, 사용자에게 중국어 콘텐츠가 있음을 알리기 위해 “중국어”라고 안내될 수 있습니다.

 

[Android native] screenReaderFocusable 속성에 관하여

Webacc NV | 2021-01-05 09:45:18

이름: 홍길동.

나이: 30살.

몸무게: 47kg.

과 같은 화면이 있다고 가정해 봅시다.

화면을 디자인하기에 따라 여러 가지의 상황이 나올 수 있겠으나 각각의 TextView를 분리하여 마크업하는 경우에는 스크린 리더 사용자 입장에서는 해당 콘텐츠를 읽는데 어려움이 있습니다.

왜냐하면 각각의 TextView가 별도 마크업 되어 있으므로 위의 요소를 탐색하려면 한 손가락 오른쪽 쓸기를 6번 해야 하기 때문입니다.

텍스트뷰를 감싸는 컨테이너 레이아웃 안에 해당 정보만 포함된 경우에는 이를 해결하는 방법으로 권장했던 것이 android:focusable true 속성이었습니다.

이것은 웹으로 치면 상위 div 에 tbindex="0"을 주는 것과 같으며 이렇게 구현할 경우 키보드 포커스가 상위 컨테이너에 이동할 수 있기 때문에 하위의 모든 텍스트를 한꺼번에 읽을 수 있어 어느정도 해결방안으로 사용되었습니다.

그러나 android:screenReaderFocusable API가 추가된 후부터는 해당 속성을 사용하는 것을 권장합니다.

그 이유는 사실상 웹과 마찬가지로 안드로이드 역시 하드웨어 키보드로 탐색 시에는 탭키나 화살표키로 이동할 때는 실행 가능한 요소에만 이동이 되어야 하기 때문입니다.

즉 focusable 속성을 사용할 경우 실행 가능하지 않은 요소에도 탭키로 이동 시 초점이 제공되므로 하드웨어 키보드 사용자 입장에서는 자연스러운 내비게이션 방식이 아닙니다.

다음 팁에서는 screenReaderFocusable 사용 예제와 사용 시 주의 사항에 대해 다루겠습니다.

[Android native] 슬라이더로 접근성 노드 변경하여 이전, 다음 배너 넘기기 구현하기 코드 예시

Webacc NV | 2020-12-31 10:09:05

오늘은 이전, 다음 배너를 톡백의 슬라이더로 조작하도록 구현하는 코드 예시를 공유해 보고자 합니다.

우선 배너 재생과 정지 버튼을 톡백이 켜졌을 때만 나타나게 하는 것에 대해서는 이미 다룬 적이 있으므로 생략합니다.

그리고 배너 재생, 정지 버튼을 bannerButton 객체라고 가정하겠습니다.

// bannerButton 을 톡백에서 SeekBar 로 읽도록 하기 위해 AccessibilityNodeInfo className 을 SeekBar 로 변경합니다.
bannerButton.accessibilityDelegate =object : View.AccessibilityDelegate() {
            override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfo?) {
                super.onInitializeAccessibilityNodeInfo(host, info)
                info?.className = SeekBar::class.java.name
// 이제 톡백 사용자에게 해당 요소는 이중탭도 가능하지만 슬라이더 조절도 가능하다는 사실을 알리기 위해 아래 예시와 같이 tooltipText 속성을 추가해 줍니다.
                info?.tooltipText = getString(R.string.bannerRolling)
                                            }
// SeekBar 접근성 개선 팁에서 설명한 바와 같이 SeekBar 클래스네임을 적용하는 순간 한 손가락 위로 쓸기(scrollForward), 아래로(scrollBackward) 이벤트를 사용하여 이에 대한 액션을 정의할 수 있습니다. 
info?.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD) info?.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD            override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean {
                if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) {
// 다음 배너로 전환되는 메소드 구현.
// 다음 배너의 텍스트를 읽어주도록 하기 위해 다음과 같이 announceForAccessibility 적용
                    bannerButton.announceForAccessibility(pagerList[currentPage])
                }
                else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
// 이전 배너로 가는 메소드 구현
// 이전 배너 내용을 톡백 사용자에게 알리기
                    bannerButton.announceForAccessibility(pagerList[currentPage])
                }
                return super.performAccessibilityAction(host, action, args)
            }
        }

 

[HTML 접근성 기초 & 해외 아티클 번역] 문서와 콘텐츠의 언어 (1/2)

Webacc NV | 2020-12-30 11:43:13

[원문 링크]  WebAim: Document and Content Language

언어 식별의 중요성

스크린리더는 콘텐츠 언어가 식별되는 한, 다양한 언어를 말할 수 있습니다. 스크린리더가 정의된 언어를 지원하지 않거나, 발화할 수 없어도 사용자에게 해당 언어를 안내할 수 있습니다.

문서의 언어를 정의하는 것은 Google 번역과 같이 콘텐츠 자동 번역 도구를 사용하는 것 또한 지원합니다.

웹 콘텐츠 접근성 지침(WCAG)의 적합성 수준 A에 따르려면, 문서의 언어는 반드시 프로그래밍 방식으로 정의되어야 합니다. WCAG의 수준 AA을 준수하려면 현재 페이지의 주된 언어와는 다른 언어로 된 부분적인 영역 또한 콘텐츠 언어가 정의되어야 합니다. 이는 가능하다면 스크린리더가 해당 언어로 전환하여 말하게끔 합니다.

페이지의 부분 언어(language of parts)를 명시하는 것은 문서의 기본 언어로는 일반적으로 이해하기 어려운 다른 언어의 내용에만 필요합니다. 예를들어, "Los Angeles"나 "piñata"는 영어 독자들이 이해하는 것에 어려움이 없는 스페인 단어이기 때문에, 영어 웹페이지에서 이 단어들을 스페인어로 식별할 필요가 없습니다.

적절하게 콘텐츠 언어를 정의하는 것은 브라우저에서 "q" 태그를 사용했을 때, 큰 따옴표 부호를 다양한 언어에 맞게 표시하는 것을 허용합니다. 다음의 예는 독일어와 프랑스어로 정의된 것입니다. 브라우저가 언어에 맞게 현지화된 큰 따옴표 기호를 생성했습니다.

 큰 따옴표의 모양이 언어(lang)에 따라 변경된 모습, 위에는 독일어, 아래는 프랑스어

언어가 지정된 경우 브라우저는 다음을 제공할 수 있습니다.

  • 비 라틴계 문자열에 적절한 문자를 제공합니다.
  • 현지화된 날짜 및 시간 입력서식을 제공합니다. (날짜:MM/DD/YYYY와 YYYY/MM/DD, DD/MM/YYYY, 시간: 24시간표기, 오후/오전 표기 등의 표기 형식)
  • 숫자를 표기할 때, 쉼표나 마침표 등 언어에 맞는 세 자릿수 구분자(Thousand Separator)를 제공합니다.
  • 입력 서식에서 언어에 맞는 철자 오류를 확인하여 알려줍니다.

lang 속성

lang 속성은 웹페이지의 언어를 식별하기 위해 하기 위해 사용합니다. 이 속성은 html 태그에 항상 추가해야 합니다. 이 속성에는 웹페이지의 자연어를 식별하는 값을 줍니다. <html lang="en">으로 예를 들자면 이 페이지는 "영문 페이지" 임을 명시하게 될 것입니다.

비슷하게, 다른 HTML의 태그에도 해당 태그에 맞는 자연어를 lang 속성을 추가하여 표기할 수 있습니다. <p lang="ja">를 예로 들자면, 이 문단 태그는 일본어임을 표기하게 될 것입니다.

연결되거나 탐색되는 태그에 언어를 지정하는 데에 "lang" 속성을 사용하지 마십시오. 영어 웹 페이지의 "스페인어로 번역" 링크 텍스트가 "Spanish"로 표시되있다면, 이것은 영어 단어이기 때문에 "lang" 속성을 사용하지 않습니다. 대신, 링크 텍스트가 "Espaøol"이라면 링크에 lang="es"가 정외되어야 합니다.

 

내용이 길어서 1부는 "lang 속성" 섹션까지만을 다룹니다. 2부에서 "적절한 언어 태그 고르기" 섹션부터 다시 찾아 뵙도록 하겠습니다. 감사합니다.

[Android native] 볼륨 키로 이전, 다음 배너 넘기기 구현 방법

Webacc NV | 2020-12-30 09:53:44

어제는 이전, 다음 배너 버튼을 별도로 구현하지 않고 배너 정지, 재생 하나의 버튼으로 배너 넘기기까지 구현할 수 있다는 것에 대해 살펴보았습니다.

오늘은 구현 방법에 대해 다루어 보겠습니다.

1. 볼륨키로 배너 넘기기 작업을 가능하게 하려면 반드시 해당 요소가 톡백에서 SeekBar 요소로 인식이 되어야 합니다.

왜냐하면 SeekBar 로 인식이 될 때에만 볼륨키 조작에 대한 이벤트가 적용되기 대문이빈다.

SeekBar, EditText 요소가 아닌 모든 요소에서의 볼륨키는 톡백이 실행되는 동안에는 항상 스마트폰 자체의 볼륨 조절로만 작동됩니다.

물론 톡백에서 SeekBar 로 인식하도록 하기 위해 실질적으로 SeekBar 클래스를 사용하라는 것은 아니며 AccessibilityNodeInfo 객체 내의 클래스 네임을 SeekBar 로 변경해 주어야 한다는 의미입니다.

2. 배너 재생, 정지 버튼은 버튼 클래스를 사용해서는 안 됩니다. 

버튼 클래스를 사용할 경우 이미 클래스 자체에서 요소 유형을 가지기 때문에 AccessibilityNodeInfo 객체 내에서 클래스 네임을 변경할 수 없습니다.

따라서 TextView 와 같은 뷰로 구현하고 버튼 스타일을 적용해 주어야 합니다.

3. performAccessibilityAction 을 사용하여 접근성 서비스에서 볼륨키를 위 또는 아래로 조작했을 때의 이벤트를 구현하면 됩니다.

다음 시간에는 구체적으로 코드를 통해서 적용 예시를 살펴보겠습니다.

[Android native] 이전, 다음 배너에 대한 접근성 적용에 대한 아이디어

Webacc NV | 2020-12-29 16:45:24

널리 아티클을 통해서 네이티브 앱에 롤링 배너를 표시하는 경우 배너를 정지하거나 이전 혹은 다음 버내로 넘길 수 있는 기능이 없으면 스크린 리더 사용자는 롤링 배너 때문에 어려움을 겪는다는 부분에 대해 말씀드린 적이 있습니다.

그리고 아티클을 통해 제시한 해결 방법은 톡백이 켜졌을 때를 탐지하여 톡백이 켜졌을 때만이라도 배너를 정지한 상태로 제공하고 이전, 다음 버튼을 두는 것에 대해 말씀을 드렸습니다.

그런데 사실상 일반적으로는 스마트폰이라는 공간의 제약 때문에 롤링 배너에 대한 재생/정지, 이전, 다음 버튼을 두지 않는 것이 일반적입니다. 

그런데 스크린 리더 사용자를 위한 대체 수단으로 버튼을 세 개 제공하는 것은 디자인의 고려가 많이 필요할 것입니다.

따라서 세 개의 버튼을 다 제공하는 것이 좋겠지만 버튼을 한 개만 제공하고 해당 버튼으로 배너 일시정지, 배너 재생, 이전 배너, 다음 배너 기능을 다 수행할 수 있도록 한다면 디자인에 대한 고려가 훨씬 줄어들고 사용자의 조작도 간단하여 또 다른 해결 방법이 될 수 있습니다.

대략적인 방법은 다음과 같습니다.

1. 스크린 리더 사용자가 일반적으로 수행하는 이중탭 제스처로는 배너 재생, 정지를 토글합니다.

2. 배너 재생/정지 버튼에 포커스 한 상태로 스마트폰의 볼륨키를 누르면 이전 혹은 다음 배너로 이동하고 해당 배너의 내용을 읽어줍니다.

이에 대한 구현 방법에 대해서는 다음 팁에서 다루겠습니다.

[Android native] AccessibilityNodeInfo setClassName 사용시 참고사항

Webacc NV | 2020-12-28 11:18:30

널리 아티클 및 팁을 통해서 여러 번 말씀드린 바와 같이 컨트롤 요소 유형이 없는 view 를 톡백이 버튼, 라디오버튼과 같은 요소 유형으로 읽도록 하려면 뷰 자체를 다른 클래스로 변경하지 않더라도 setAccessibilityDelegate 객체를 만들어서 AccessibilityNodeInfo setClassName 을 Button.class.getName() 과 같이 구현할 수 있습니다.

그런데 Button, SeekBar 와 같이 컨트롤 유형을 이미 가진 view 는 AccessibilityNodeInfo 의 클래스 네임을 변경하더라도 기존에 가진 속성이 변경되지 않습니다.

즉 ImageView, TextView, LinearLayout 과 같이 뷰 자체에 컨트롤 유형이 들어가 있지 않은 경우에만 변경이 가능합니다.

따라서 기존에 구현된 view 가 컨트롤 유형을 가지고 있으나 스크린 리더에서 읽어주는 요소 유형이 의미에 맞지 않아 다른 요소 유형으로 변경하고자 할 때는 view 자체를 다른 컨트롤로 변경해 주어야 합니다.

[Android native] 접근성 포커스 순서 지정하기 예시

Webacc NV | 2020-12-22 10:00:17

2개의 텍스트뷰와 2개의 버튼뷰가 있다고 가정해 봅시다.

두 텍스트 뷰는 위에 한 줄로 나란히 위치하며 텍스트는 각각 출발역, 도착역입니다.

그 아랫줄에는 두 버튼이 있으며 텍스트는 각각 서울역, 동대구역입니다.

우리는 톡백의 초점 순서를 출발역, 서울, 도착역, 동대구 순으로 재조정할 것입니다.

1. 출발역, 도착역 텍스트뷰에 각각 id를 지정해야 합니다. 

지난 팁에서 설명드렸듯이 포커스 순서를 조정할 때 id 기준으로 조정하기 때문입니다.

여기서는 startStationLabel, destinationStationLabel 로 각각 지정하겠습니다.

2. XML 레이아웃에서 서울, 동대구 두 버튼 뷰에 각각 다음과 같이 포커스 순서를 변경합니다.

        android:accessibilityTraversalAfter="@id/startStationLabel"
android:accessibilityTraversalAfter="@id/destinationStationLabel"

이렇게 하면 우리가 의도한 대로 포커스 순서가 조정됩니다.

 

[Android native] 초점 순서 변경하기

Webacc NV | 2020-12-21 09:54:44

사용자가 한 손가락 오른쪽 쓸기를 통해서 콘텐츠를 탐색하면 기본적으로 왼쪽에서 오른쪽, 위에서 아래 순으로 접근성 초점이 이동됩니다.

그러나 레이아웃 구조에 따라 위와 같은 방식으로 접근성 초점이 이동될 경우 스크린 리더 사용자가 레이아웃 파악이 어려운 경우가 있습니다.

위에는 헤더 제목, 아래는 제목에 해당하는 클릭 요소가 있다고 가정해 봅시다. 

예를 들면 위에는 출발역, 도착역 제목 텍스트가 왼쪽 오른쪽에 각각 표시되고 있으며 출발역, 도착역에 해당하는 요소, 서울, 동대구는 아래에 있습니다.

그렇다면 포커스 순서를 조정해 주지 않으면 출발역, 도착역, 서울, 동대구 순으로 초점이 이동될 것이며 이것은 초점 순서가 논리적이지 않습니다.

이때 사용할 수 있는 것이 accessibilityTraversalAfter 혹은 accessibilityTraversalBefore 입니다.

before 혹은 after 뒤에는 순서를 조정하고자 하는 view의 id를 적어주면 됩니다.

자세한 코드 예시는 다음 팁에서 설명하겠습니다.

[Android native]앱에서 Talkback 사용여부 실시간으로 감지하기

Webacc NV | 2020-12-18 18:58:32

Android에서는 사용자 디바이스가 Talkback 서비스를 사용중인지 확인할 수 있는 API를 제공합니다. Talkback을 감지하는 형태는 두 가지가 있습니다.

1. 특정한 메소드가 실행될 때 서비스 값 검사(예: Activity의 onCreate 메소드나, 각종 사용자 지정 메소드)

AccessibilityManager에는 Talkback 사용 여부를 bool값으로 반환하는 isTouchExplorationEnabled()가 있습니다. 이를 사용하여 스크린리더 사용 여부를 특정 코드 구간에서 수동으로 검사할 수 있습니다. onCreate 메소드의 setContentView 구문 다음에 사용하면 앱을 켰을 때 스크린리더가 켜져있는지 검사할 수 있습니다.

class MainActivity AppCompatActivity{
    private AccessibilityManager A11yManager;

    @Override

    protected void onCreate(savedInstanceState){
      ...
      setContentView(R.layout.Activity_main);
      Context mainContext = getApplicationContext();// 서비스를 받아오기 위해 컨텍스트를 가져옵니다.
      A11yManager = (AccessibilityManager) mainContext.getSystemService(ACCESSIBILITY_SERVICE);//접근성 서비스를 가져옵니다.
      if(A11yManager.isTouchExplorationEnabled()){
        //Talkback이 켜져있으면 실행될 무언가를 작성합니다.
      }
    }
  }
}

2. 앱이 켜져있을 때 Talkback의 활성화 상태를 상시 감지하는 리스너 설치

하지만, 시각장애인 사용자 모두가 Talkback을 계속 킨 상태로 쓰진 않습니다. 저시력 사용자는 Talkback을 자주 끄는 사람이 더러 있기 마련인데요. 만약, 앱을 켰을 때, Talkback이 꺼져있다면, Talkback용 앱 기능이 작동하지 않으니 사용자 입장에서 매우 아쉬울 수 있습니다.

그럴때는 onTouchExplorationStateChangedListener를 사용할 수 있습니다.

class MainActivity AppCompatActivity{
    private AccessibilityManager A11yManager;

    @Override

    protected void onCreate(savedInstanceState){
      ...
      setContentView(R.layout.Activity_main);
      Context mainContext = getApplicationContext();
      A11yManager = (AccessibilityManager) mainContext.getSystemService(ACCESSIBILITY_SERVICE);
      
      //앱이 켜졌을 때도 접근성 기능이 로드되게끔 위에서 설명한 대로 검사합니다.
      loadViewForTalkbackUsers(A11yManager.isTouchExplorationEnabled());
      
      //접근성 기능이 실시간으로 꺼지고 켜짐을 감지하기 위해 리스너를 등록합니다.
      A11yManager.onTouchExplorationStateChangedListener(new AccessibilityManager.onTouchExplorationStateChangedListener(){

         @Override

         public void onTouchExplorationStateChanged(boolean enabled){
            //리스너 콜백입니다. 콜백의 파라미터로 불 변수를 받고, 원하는 함수로 전달하여 기능을 구현하세요.
            LoadViewsForTalkbackUsers(enbned);
         }          
      });
    }
  }
  private loadViewsForTalkbackUsers(boolean condition){
    if(condition){
      // Talkback 켜짐이 감지되면 실행할 명령문을 작성
    }else
      // Talkback 꺼짐이 감지되면 실행할 명령문을 작성
    }
  }
}

다만, 주의할 점은, 리스너는 앱이 실행과 동시에 발생하지 않기 때문에 첫번째 방법과 함께 사용하여야 정상적인 작동이 가능합니다.

만약 첫번째 방법을 같이 사용하지 않는다면, Talkback을 한 번 껐다 켜야 기능이 정상 작동되는 우스꽝스러운 현상이 생길 것입니다.

[Android native] replaceAccessibilityAction 사용하여 활성화 하려면 이중탭하세요 힌트 바꾸기

Webacc NV | 2020-12-18 09:19:59

얼마전에 몇 차례에 걸쳐 replaceAccessibilityAction 메소드의 용도와 사용 방법, 그리고 접근성 포커스가 갔을 때 편집창의 힌트 메시지를 변경하는 예제를 함께 살폈습니다.

이번에는 이중탭할 수 있는 요소마다 톡백이 발화 하는 활성화 하려면 이중탭하세요 라는 힌트 메시지를 다른 것으로 변경하는 예제를 살펴보도록 하겠습니다.

지난 번에 말씀드린 것처럼 replaceAccessibilityAction 안에는 적용하고자 하는 객체 이름과 접근성 액션, 메시지를 포함하는 액션의 경우 메시지, 그리고 액션을 수행했을 때 디폴트와 다르게 동작하도록 해야 할 때의 accessibilityViewCommand 가 들어가게 됩니다.

여기서 우리가 변경할 것은 클릭에 관한 힌트 메시지이며 클릭 액션 자체는 변경하지 않을 것입니다.

그럼 활성화 하려면 이중탭하세요 를 햄버거 주문 작업을 하려면 이중탭하세요 로 다음 예제를 통해 변경해 보도록 하겠습니다.

ViewCompat.replaceAccessibilityAction(hamburgerOrderView, ACTION_CLICK, ("햄버거 주문"), null);

참고: 롱 클릭에 대한 힌트 메시지 변경은 ACTION_CLICK 대신에 ACTION_LONG_CLICK을 사용하면 됩니다.

[Android native] 새로고침 제스처 구현 시에 버튼으로도 기능 실행을 할 수 있도록 고려해야 하는 이유

Webacc NV | 2020-12-14 09:35:14

지난 주에는 콘텐츠 새로고침 구현 시에 스크린 리더 사용자가 이를 인지할 수 있도록 해야 하는 것에 대해 다루었습니다.

오늘은 하드웨어 키보드 사용자 관점에서 조금 더 깊이 생각해 보려고 합니다.

안드로이드는 윈도와 마찬가지로 블루투스 키보드와 같은 하드웨어 키보드를 이용하여 클릭 가능한 요소를 탭키나 화살표키로 이동하고 실행할 수 있습니다.

또한 리스트뷰와 같은 스크롤이 가능한 콘텐츠에서 스크롤도 가능합니다.

그래서 제스처 사용이 불편한 분들 중에는 키보드를 사용하여 앱을 사용하시는 분들도 있습니다.

문제는 안드로이드에서 제공하는 RefreshLayout을 사용하는 경우에는 키보드를 통해서는 콘텐츠 새로고침을 할 수 없다는 것입니다.

이를 해결하기 위해 새로고침이 구현된 화면에서는 새로고침을 키보드로도 수행할 수 있도록 버튼을 두는 것을 권장합니다.

안드로이드 API 설명에 의하면 새로고침은 화면 전체를 새로고치는 기느을 하므로 단순히 새로고침 버튼을 두기보다는 화면 상단 오른쪽에 옵션더보기 메뉴(onCreateOptionsMenu)를 구현하고 그 팝업 메뉴 안에 새로고침 버튼을 두는 것을 권장하고 있습니다.

하드웨어 키보드에서는 옵션 메뉴가 표시된 화면에서는 컨트롤 + ESC 키를 이용하여 해당 메뉴를 실행할 수 있습니다.

[HTML - 해외 아티클 번역] placeholder는 레이블이 아닙니다.

Webacc NV | 2020-12-09 14:29:59

 

[원문 출처] HTMHell - #24 A placeholder is not a label

placeholder에 대한 오해와 잘못된 사용에 대해 다룬 해외 아티클을 번역하여 제공합니다.

아래는 번역된 본문입니다. 다소 많은 의역이 있음을 참고해주시기 바랍니다.

[나쁜 코드 예제]

<input type="text" placeholder="First name">

문제점 그리고 해결방안

  • 모든 입력 서식 요소는 레이블이 필요합니다. 스크린리더 사용자가 서식 영역을 접근할 때, 레이블이 서식 종류와 함깨 안내됩니다(출력 예시: <이름:성, 편집창>). 만약 레이블이 없다면 사용자가 무엇을 입력해야 하는지 모를 수 있습니다. (출력 예시: <편집창>).
  • 스크린리더에 따라서는 placeholder 속성을 레이블처럼 읽지만, 이것에 의존하는 것은 지양해야 합니다.
  • 보통 placeholder 글자는 낮은 명도대비의 밝은 회색으로 표시되기 때문에 저시력자나 강한 햇빛과 같은 환경에 있는 사람이 이 글자를 읽지 못할 수 있습니다.
  • ::placeholder 가상선택자를 사용하면 placeholder의 명도대비를 증가시킬 수 있지만, placeholder의 글자 명도대비가 너무 높으면자동으로 체워진 텍스트 값으로 착각할 수 있습니다.
  • <label> 요소를 사용, 표시하는 것은 입력 서식의 대상 크기를 키워주어 특히 터치로 조작하는 장치를 사용할 때 매우 큰 도움을 줄 수 있습니다.
  • 입력 서식에 오로지 placeholder만으로 레이블 기능을 대체한다면, 유저가 입력 서식에 내용을 입력했을 때 레이블이 사라지며, 특히 복잡하거나 가끔 사용되는 서식 모음에서 사용자로 하여금 단기 기억을 요구합니다.
  • 레이블이 없고, 입력값만 표시되는 입력 서식만이 보이기 떄문에 사용자는 서식을 제출하기 전에 어떤 입력 서식에 무엇을 입력했는지 알 수 없습니다.
  • 브라우저에서 자동으로 값을 체췄다면, 알맞는 편집창에 올바른 값이 들어갔는지 확인하기 위해 내용을 잘라내고 레이블을 확인한 후 다시 붙여넣어야합니다.
  • 만약, placeholder의 텍스트 길이가 입력 서식 영역보다 길다면 잘립니다.
  • 구글 번역과 같은 번역 도구에서 HTML의 속성값은 번역해주지 않을 수 있습니다.
  • 레이블은 입력 서식 위치와 겹치지 않고, 입력 서식 요소 전에 배치될 때 가장 잘 작동합니다.

자세한 내용은 아래의 참고자료 섹션을 참고하시기 바랍니다(영문 페이지).

[좋은 코드 예제]

 <label for="firstname">First name</label>
 <input type="text" id="firstname">

[참고자료]

[Android native] 콘텐츠 새로고침 알림에 관하여

Webacc NV | 2020-12-09 09:21:59

많은 안드로이드 앱에서 화면 새로고침 기능을 지원하고 있으며 대부분 한 손가락으로 화면을 아래로 당기면 해당 기능이 실행됩니다.

톡백 사용자는 해당 기능을 두 손가락 아래로 쓸어내리기 제스처를 통해 수행할 수 있습니다.

문제는 새로고침이 될 때 접근성 구현을 별도로 하지 않으면 스크린 리더 사용자는 콘텐츠가 새로 갱신되었다는 것을 전혀 알 수 없다는 것입니다.

따라서 새로고침 동작이 일어난 경우에는 announceForAccessibility 메소드를 사용하여 이를 스크린 리더 사용자에게 알려 주어야 합니다.

다음 예시를 참고합니다.

 

mSwipeRefreshLayout = findViewById(R.id.swipeRefresh);
        mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                refreshList();
mSwipeRefreshLayout.announceForAccessibility("콘텐츠 새로고침");
            }
        });

[NVDA & WEB] 가상커서(브라우즈모드) 상에서의 엔터 및 스페이스

Webacc NV | 2020-12-08 09:46:27

지난 시간에는 센스리더에서 링크나 버튼과 같은 요소에서의 엔터, 스페이스가 어떤 이벤트로 동작되는지를 함께 살폈습니다.

오늘은 NVDA에서의 동작 이벤트에 대해 생각해보겠습니다.

결론부터 말씀드리면 NVDA는 가상커서(브라우즈모드) 상에서 버튼과 같은 요소에서 엔터나 스페이스를 누르면 모두 다 클릭 이벤트로 작동을 합니다.

따라서 우리는 다음과 같은 이슈를 정리해볼 수 있습니다.

1. 커스텀 버튼을 구현한 경우 스크린리더 가상커서를 활용하는 경우에는 키보드 접근성을 테스트 할 수 없습니다.

즉 키보드 접근성을 테스트 하려면 가상커서나 브라우즈모드를 끄고 동작을 시켜 보아야 합니다.

2. 특정 커스텀 요소가 클릭 이벤트가 아닌 마우스오버와 같은 동작으로 실행되는 경우에는 아무리 키보드 이벤트를 적용했다 하더라도 스크린 리더 사용자는 가상커서를 오프 하지 않는 이상 해당 요소를 실행할 수 없습니다.

접근성 테스트 시에 참고가 되었으면 좋겠습니다.

[센스리더] 인터넷에서의 커스텀 컨트롤 실행 관련

Webacc NV | 2020-11-30 10:06:02

접근성 측면에서 될 수 있으면 항상 네이티브 컨트롤을 사용해야 하지만 어쩔 수 없이 커스텀 컨트롤을 사용하는 경우가 있습니다.

예: <span role="button" tabindex="0">햄버거 선택하기</span>

이러한 커스텀 버튼에 접근성을 적용하기 위해서는 키다운 이벤트를 통해서 키보드 접근성을 보장하는 것 또한 당연한 일입니다.

그런데 센스리더에서 가상커서를 켠 상태로 이러한 커스텀 버튼에서 엔터를 누르면 키보드 이벤트가 실행될까요?

아닙니다.

센스리더에서 가상커서를 켠 상태로 엔터를 누르면 마우스 클릭 동작이 실행됩니다.

따라서 센스리더에서 엔터를 눌러서 커스텀 요소가 실행이 된다고 해서 키보드 접근성을 보장한다 라고 할 수 없으며 정확한 테스트를 위해서는 반드시 CTRL + shift + f12를 눌러 가상커서를 해제한 다음 해당 요소를 테스트 해 보아야 합니다.

다만 스페이스 키는 키보드 동작으로 실행이 됩니다.

따라서 커스텀 버튼을 만든다고 가정할 때 키다운 이벤트를 걸어서 엔터와 스페이스로 실행할 수 있게 해 놓았다면 센스리더에서 가상커서를 켠 상태로 스페이스를 누르면 이 때는 키보드 이벤트가 실행되며 만약 스페이스에 대한 키보드 이벤트가 구현되지 않은 경우에는 실행이 안 되는 것입니다.

다음 팁에서는 센스리더의 클릭 동작과 인터넷 익스플로러에서의 특징에 대해 살피겠습니다.

[Sense Reader and WAI-ARIA] aria-pressed 접근성 지원 관련

Webacc NV | 2020-11-25 18:40:35

센스리더에서도 조금씩 WAI-ARIA 지원 속성이 늘어나고 있으며 최근에 대표적으로 추가된 속성이 role radio, role checkbox, aria-checked 등입니다.

오늘은 aria-pressed 지원에 대해 잠시 살펴 보려고 합니다.

aria-pressed 속성은 토글 버튼을 구현할 때 사용하며 눌려졌을 때와 누르지 않았을 때의 상태를 표시합니다.

최근 센스리더에서는 aria-pressed 속성을 일부는 지원을 하여 aria-pressed true 일 때 선택이라는 상태 정보값을 음성 출력합니다.

예를 들어 버튼 요소의 레이블이 음소거이고 aria-pressed true 이면 선택, 음소거 버튼 이 됩니다.

하지만 false 일 때는 값을 전달하지 못합니다.

센스리더의 WAI-ARIA 지원 범위를 궁금해 하시는 분들이 있어 팁으로 올려봅니다.

[Android native] 음성검색화면 접근성 적용 예제

Webacc NV | 2020-11-23 10:12:56

오늘은 지난 번 음성검색 접근성 구현 관련 설명을 보충하는 시간입니다.

1. 먼저 음성 검색 화면으로 진입 시에 새로운 activity로 전환되는 경우에는 setTitle 값을 빈 값으로 두어 톡백이 액티비티 제목을 읽지 못하게 합니다.

setTitle(" ");

2. 음성검색 화면이 실행될 때 톡백이 그 어누 요소에도 포커스 되지 못하게 하기 위하여 다음 예시와 같이 모든 화면의 요소를 접근성 서비스에서 숨깁니다.

ViewCompat.setImportantForAccessibility(getWindow().getDecorView(), viewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);

3. 약 1초 뒤에 모든 숨긴 포커스는 원래대로 되돌립니다.

new Handler().postDelayed(new Runnable() {
    @Override
    public void run() {
        ViewCompat.setImportantForAccessibility(getWindow().getDecorView(), ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
    }
}, 1000);

4. 음성듣기가 완료되어 다른 activity로 갱신되지 않고 말을 이해하지 못했다는 등의 텍스트가 화면상으로 출력되는 경우에는 다음 예시와 같이 이를 톡백이 바로 읽을 수 있도록 처리합니다.

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                mPlayer.start();
                textView.setText(R.string.voiceNotUnderstand);
                example1.setText(R.string.voiceListen);
                textView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
new Handler().postDelayed(new Runnable() {
    @Override
    public void run() {
        example1.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
    }
}, 1000);
            }
        }, 5000);

위의 예시에서 접근성 이벤트가 두 개 들어간 것을 알 수 있습니다. 

하나는 windowStateChanged 이벤트로 음성 듣기가 끝났을 때 텍스트 내용을 바로 읽을 수 있도록 한 것입니다.

그리고 1초 정도 딜레이를 준 다음 viewFocused 이벤트를 통하여 듣기 버튼으로 초점을 보내주었습니다.

위와 비슷한 방법으로 구현하면 스크린 리더 사용자가 톡백을 실행한 상태에서도 음성 검색을 무난하게 수행할 수 있을 것입니다.

[모바일 스크린리더 & WAI-ARIA] 우리가 표준을 따라야 하는 이유와 WAI-ARIA의 한계

Webacc NV | 2020-11-20 11:44:11

커스텀(Custom)은 주문 제작, 사용자 지정 등의 뜻을 갖고 있습니다. 웹 개발이나 앱 개발에 있어서도 마찬가지로 개발자의 입맛에 맞게 표준에는 정의되지 않은 커스텀 요소를 만들 수 있습니다.

웹에서는 커스텀 요소의 접근성을 지키기 위해 WAI-ARIA 명세를 사용합니다. WAI-ARIA는 보조기술(이하 AT)에서 받아들일 수 있는 요소 정보, 상태 정보, 선택 정보 등, 요소에 필요한 정보를 제공하기 위해 사용합니다.

WAI-ARIA를 사용하지 않고 커스텀 요소를 만들면 생기는 일

만약 WAI-ARIA를 사용하지 않는다면, 스크린리더와 같은 AT에서는 아무리 디자인을 다르게 한들, div나 span으로 만들었다면 아무 요소도 아닌 것으로 인식할 것입니다. AT를 직접적으로 사용하는 스크린리더 사용자에게는 아무런 정보도 알리지 못할 것입니다.

WAI-ARIA는 커스텀 요소를 만드는 만능의 도구가 아니다 (1) : WAI-ARIA는 정보를 제공할 뿐, 기능을 완전히 구현해 주지는 않습니다.

WAI-ARIA를 마법의 지팡이인 듯 사용하지 마십시오. WAI-ARIA는 오로지  사람과 AT 소프트웨어에게 정보를 전달하는 것에 초점을 맞춘 명세입니다. AT 소프트웨어 중 특히 스크린리더에 지대한 영향을 미치므로, 스크린리더를 기준으로 말씀을 드리겠습니다.

스크린리더는 요소의 정보를 전달받으면 가상 커서가 꺼지거나, dialog(대화상자) 역할처럼 현재 영역 외의 모든 영역을 숨겨준다거나 하는 기능을 수행할 수 있습니다. 하지만 요소에는 정보 전달만의 목적을 가진 요소만 있는 것은 아닙니다. OS나 웹에는 라디오 버튼이나, 탭 컨트롤, 메뉴바, 컨텍스트 메뉴, 트리뷰와 같은 요소는 상호작용 가능한 요소(Interactable Element)가 있습니다. WAI-ARIA는 이들에 대한 정보를 제공하나 상호작용할 수 있는 조작방법을 구현해주지는 않습니다.

HTML이나 응용프로그램에서의 커스텀은 하나부터 열까지 전부 스스로 구현해야 한다는 것을 명심해야 합니다.

WAI-ARIA는 커스텀 요소를 만드는 만능의 도구가 아니다(2) : 모바일 기기와의 상호작용을 일반적으로 구현하기 어렵다.

1980년도부터 2000년대 중반까지, 웹이나 응용프로그램을 사용하는 기기는 데스크탑이나 랩탑(노트북)에 한정되어 있었습니다. 하지만, 2007년, 애플이 처음으로 아이폰을 선보여 스마트폰 시대를 예고했으며, 2009년부터 상품화와 대중화가 이루어진 스마트폰이 나오기 시작했습니다.

모바일 환경에서는 모바일 OS에서 제공하는 네이티브 이벤트, 웹에서 제공하는 이벤트가 있습니다. PC 웹 환경에서는 웹에서의 이벤트만 신경쓰면 됬었기에 별 문제가 없었으며, WAI-ARIA를 사용할 때 별 문제가 없습니다.

하지만, 모바일 기기의 AT는 해당 기기에 최적화된 조작법을 포함하는 경우가 많습니다. 예를들어 아이폰에서 슬라이더의 값을 조절할 때에는 위 또는 아래로 한 손가락을 쓸어 값을 조절할 수 있으며, 안드로이드 기기에서는 컨텍스트 메뉴를 열어 슬라이더의 값을 숫자로 직접 입력하여 조절하거나, 슬라이더에 초점을 두고, 음량 조절 버튼으로 슬라이더 값을 조절할 수 있습니다.

하지만 role="slider"로 구현한 슬라이더에서는 이를 일반적으로 구현하여 접근성을 보장할 방법이 없습니다. 이는 모바일 OS에서 지원하는 이벤트이기 때문입니다. 그런데 role을 사용하면, 요소 유형 정보를 AT에 전달하기 때문에, 위 조작방법을 사용할 수 없는 커스텀 요소임에도 불구하고, 볼륨 버튼을 눌러 조절할 수 있다거나, iOS에서는 위 또는 아래로 쓸어서 조절할 수 있다는 안내 힌트가 자동으로 따라 붙게 됩니다.

그러면 어떻게 해야 하는가?

네이티브 앱에는 HTML에 없는 요소가 많습니다. 이러한 요소를 구현할 때에는 당연히 WAI-ARIA를 사용해야 맞는 것입니다. 방법이 이것 밖에 없으니까요. 하지만, 위에서 slider는 HTML5 네이티브 input 태그에서 충분히 구현할 수 있습니다. <input type="range" />는 role="slider"와 동일합니다.

이렇게 대체 가능한 네이티브 HTML 태그가 있다면, 되도록 네이티브 태그를 사용해야 합니다. 네이티브 HTML 태그는 어떠한 OS든 이벤트가 대응되어 있기 때문입니다.

특히 모바일 환경은 대부분 webkit 기반의 safari(iPhone)과 웹킷-크로미움 기반의 웹 브라우저(Android)를 사용합니다.  따라서 PC 브라우저 환경보다 CSS를 사용하여 통합된 디자인을 만드는 것이 보다 수월합니다. 커스텀 요소를 사용하는 이유가 디자인적인 부분이라면 네이티브 태그의 디자인을 수정해 보는 것이 더 바람직해 보입니다.

만약, 어쩔 수 없이 모바일 페이지에서 커스텀 요소를 써야 한다면, AT 사용자를 위해 네이티브 요소를 함꼐 제공하고, 값을 공유하는 것도 생각해볼 만할 가치가 있을 것 같습니다.

[센스리더 & HTML] 가상커서가 자동으로 꺼지는 것 업데이트 관련

Webacc NV | 2020-11-20 08:58:42

스크린 리더는 탭키를 눌러서 라디오버튼이나 탭컨트롤과 같은 특정 요소를 만나면 가상커서를 자동으로 끕니다.

왜냐하면 라디오버튼이나 탭컨트롤 등은 화살표를 눌러서 옵션을 변경하도록 규정하고 있기 때문입니다.

탭컨트롤 등을 구현할 때 키보드 이벤트를 구현해야 하는 이유가 여기에 있습니다.

센스리더에서도 탭키를 눌렀을 때 여러 줄 입력창이나 라디오버튼, 탭컨트롤을 만나면 가상커서를 자동으로 해제합니다.

다만 별도 소리가 없어서 사용자가 그것을 자세히 인지를 하지 못할 뿐입니다.

그런데 7.4 버전부터는 롤 슬라이더, 아리아로 구현한 메뉴 요소를 만나도 가상커서를 자동으로 끕니다.

따라서 슬라이더나 메뉴 내비게이션을 반드시 아리아를 사용하셔야 한다면 키보드 접근성 고려가 필요합니다.

[iOS native] 한 손가락 옆으로 쓸기 탐색으로 특정 영역의 요소들이 탐색되지 않는 경우

Webacc NV | 2020-11-19 14:44:17

아이폰에서 특정 앱이 실행되면 한 화면에 당연한 이야기이지만 여러 요소들이 존재할 것입니다.

그런데 해당 요소들은 화면에는 보이지 않지만 UIView, UITableView, UIScrollView 와 같은 부모 요소들이 존재하게 됩니다.

이것을 컨테이너라고 부릅니다.

따라서 한 화면에는 콘텐츠 유형에 따라 여러 컨테이너들이 존재하게 됩니다.

그런데 문제는 한 손가락 오른쪽 쓸기로 순차 탐색 시에 특정 그룹, 즉 특정 컨테이너는 아예 포커스가 되지 않는 경우가 있다는 것입니다.

따라서 스크린 리더 사용자는 해당 영역이 아예 없는 것처럼 생각할 수 있습니다.

이것의 원인은 무엇일까요?

접근성 초점이 가는 버튼과 같은 view 하위에 또 다른 하위 요소들을 구현했기 때문입니다.

특정 컨테이너 그룹의 접근성 포커스를 수정하거나 그룹의 여러 요소들을 하나의 포커스로 만드는 등의 작업을 하지 않는 이상 컨테이너를 구성하는 테이블, 스크롤뷰 등에는 보이스오버 포커스가 되지 않는 것이 원칙입니다.

즉, 컨테이너 그룹 자체 뷰가 접근성 포커스를 가지게 되면 기본적으로 하위 요소에 포커스가 안 됩니다.

따라서 접근성 초점이 가는 요소 하위에 또 다른 무언가의 콘텐츠들을 두는 것은 보이스오버 입장에서는 컨테이너 부모에 포커스가 되는 상황이 되므로 스크린 리더 사용자에게 큰 문제를 주게 됩니다.

따라서 view를 구성할 때 접근성 초점이 가는 요소 하위에 또 다른 하위 view를 만들지 않도록 주의가 필요합니다.

[Android native] 음성 검색 시에 톡백이 말하지 않게 하기

Webacc NV | 2020-11-18 09:52:19

음성 검색을 누르면 마이크가 켜지면서 톡백이 화면의 내용을을 읽는 것 때문에 스크린 리더 사용자가 어려움을 겪는 경우가 많습니다.

사실 톡백 입장에서는 화면이 변경되면 변경된 내용을 읽는 것이 당연할 것입니다.

그래서 음성 검색 화면에서는 톡백이 아무 말도 못하도록 별도 접근성 구현이 필요합니다.

접근성 구현을 해 주어야 하는 것은 다음과 같습니다.

1. 화면이 전환될 때 액티비티 레이블을 톡백이 읽지 못하게 하기: 이 부분은 지난 팁에서 한번 다룬 적이 있는데 setTitle을 null값 즉 ""로 둡니다.

2. 화면 전환 시 그 어떤 요소에도 포커스 되지 못하게 하기: 톡백은 화면 전환 시에 자동으로 화면의 제목 다음에 있는 첫 요소에 포커스를 위치시킵니다.

화면 제목 이전에 위로 이동과 같은 버튼이 있다면 그 버튼에 포커스가 됩니다.

우리는 이러한 요소에 톡백이 포커스 하지 못하도록, 그래서 아무 말도 하지 않도록 해야 하는 것입니다.

3. 반드시 음성 듣기 시작과 종료에 대한 효과음을 재생해야 합니다.

4. 음성 듣기 종료 시 다른 화면으로 전환되지 않고 현재 화면에 머무르는 경우에는 어떤 메시지가 발생했는지를 자동으로 읽게 해야 하며 듣기 버튼으로 초점을 이동시켜 주어야 합니다.

다음 팁에서는 이를 적용하기 위한 예제 코드를 공유하겠습니다.

[Android native] 콘텐츠 검색 화면 구현 시 검색어 삭제 관련

Webacc NV | 2020-11-13 17:01:11

일반적으로 콘텐츠 검색 화면을 구현하는 경우에는 텍스트 입력창에 글자가 입력되어 있으면 검색어를 삭제하는 버튼을 표시하는 경우가 많습니다.

그리고 텍스트가 삭제되면 해당 버튼은 화면에서 사라지는 형태입니다.

이때도 접근성 구현을 해 주지 않으면 스크린 리더 사용자에게 이슈가 있는데 텍스트 삭제 후에 톡백의 포커스가 상단으로 튀어버리거나 톡백의 초점이 어디에 포커스 되었는지에 대한 음성 피드백을 받을 수 없다는 점입니다.

따라서 이를 해결하기 위해 우리는 사용자가 검색어 삭제 버튼을 누르면 다음 예시와 같이 초점을 검색어 입력 편집창으로 보내주면 됩니다.

public void deleteText(View view) {
    Button clearText = (Button)findViewById(R.id.clearText);
    EditText editText = (EditText)findViewById(R.id.editText);
editText.getText().clear();
editText.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
    }

[iOS native] 포커스 순서 변경 예제

Webacc NV | 2020-11-12 09:56:41

어제는 포커스 순서를 변경하는 방법에 대해 살폈습니다. 

오늘은 관련된 예제 코드를 공유하려고 합니다.

하나의 UIView에 제목 1, 내용 1, 제목 2 내용 2 네 개의 view가 있다고 가정해 봅시다.

그리고 한 손가락 오른쪽 쓸기를 통해 이동할 때 제목 1, 제목 2, 내용 1, 내용 2 순으로 포커스가 된다고 가정해 봅시다.

그러면 우리는 제목 1, 내용1과 같이 포커스 되도록 수정을 해야 할 것입니다.

UIView에 다음과 같이 4개의 view를 포커스 순서대로 AccessibilityElements 배열에 담습니다. UIView 객체가 dataView라는 이름을 가졌다고 가정한다면 다음과 같이 배열 형태로 넣을 수 있습니다.

dataView.accessibilityElements = [header1, detail1, header2, detail2]

 

[iOS native] 포커스 순서 재조정하기

Webacc NV | 2020-11-11 10:12:20

스크린 리더 사용자가 한 손가락 오른쪽 혹은 왼쪽 쓸기를 하면 대부분 화면상의 레이아웃 순서대로 포커스가 이동합니다.

그러나 특정 상황에서는 화면의 포커스 순서가 논리적이지 않아 수정이 필요한 경우가 발생하게 됩니다.

이때는 포커스 문제가 발생하는 컨테이너의 하위 뷰들의 순서를 재정의해줌으로써 해당 문제를 해결할 수 있습니다.

이를 위해서는 컨테이너의 각각의 view들을 accessibilityElements 안에 array 형태로 담습니다.

여기서는 순서가 중요하며 array 안에 담겨 있지 않은 뷰들은 모두 무시됩니다.

다음 팁에서는 구체적인 예시를 공유하도록 하겠습니다.

[Android native] replaceAccessibilityAction을 활용한 톡백 사용자용 힌트 별도 지정

Webacc NV | 2020-11-10 09:48:06

이제 replaceAccessibilityAction의 실제 적용사례를 나눌 차례입니다.

지난 주에는 replaceAccessibilityAction의 문법에 대해 살폈었습니다.

우리는 화면에 보여지는 편집창 레이블을 톡백에서는 다르게 읽어주도록 해볼 것입니다.

먼저 휴대폰 번호를 입력하는 편집창이 있고 실제 레이블은 전화번호, - 제외 라고 가정해 봅시다.

톡백에서는 -를 읽지 않으므로 톡백 사용자를 위해 전화번호, 숫자만 입력 이라고 힌트 메시지를 바꾸어볼 것입니다.

톡백 사용자용 힌트 메시지를 별도 마크업하는 방법이 있으면 좋겠지만 현재는 접근성 API에서 이를 제공하고 있지 않으므로 우리는 접근성 액션 중에서 포커스라는 액션을 활용할 것입니다.

즉 톡백이 전화번호 입력창에 포커스가 가면 즉 접근성 액션 중에서 접근성 포커스가 실행이 되면 이 때는 힌트 메시지를 변경하는 것입니다.

그렇다면 다음과 같이 replaceAccessibilityAction을 활용할 수 있을 것입니다.

ViewCompat.replaceAccessibilityAction(editText, AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_ACCESSIBILITY_FOCUS, "", new AccessibilityViewCommand() {
    @Override
    public boolean perform(@NonNull View view, @Nullable CommandArguments arguments) {
        editText.setHint(R.string.talkBackHint);
        return false;
    }
});

[iOS native] 보이스오버가 자체적으로 만들어내는 대체 텍스트 구분하기

Webacc NV | 2020-11-09 15:58:47

접근성을 테스트 할 때 가장 흔히 접하게 되는 이슈는 대체 텍스트일 것입니다.

이미지에 대한 대체 텍스트가 없다면 스크린 리더 사용자는 해당 요소가 어떤 요소인지조차 알 수 없기 때문입니다.

그런데 최근들어 iOS 보이스오버에서는 이미지 인식 기술을 이용하여 대체 텍스트가 없어도 일부 요소에 자동으로 대체 텍스트를 추가하여 읽어주는 경우가 종종 있습니다.

대표적인 요소들이 뒤로, 닫기, 검색, 홈 등입니다.

그래서 해당 기능을 잘 모를 경우 접근성을 테스트 하는 입장에서는 이러한 버튼들에 대체 텍스트가 들어간 것으로 생각할 수 있으나 보이스오버가 예상하는 정보를 주는 것일 수 있다는 것을 알아 두셔야 합니다.

아래는 뒤로, 닫기, 검색과 같은 버튼들이 실제 대체 텍스트로 추가된 것인지 보이스오버가 이미지를 인식하여 읽어주는 것인지를 구분하는 방법입니다.

1. 보이스오버가 이미지 인식 기술을 통해 유추한 대체 텍스트는 요소 유형 뒤에 읽어줍니다.

예: 버튼, 뒤로.

2. 구분하기 애매하다 판단되는 경우에는 해당 요소에 포커스 하고 세 손가락 한 번 탭을 해봅니다.

세 손가락 한 번 탭은 현재 위치한 포커스가 화면의 어느 부분이 위치해 있는지를 알려주는 기능인데 이미지 인식 기술이 가능한 경우에는 보이스오버가 인식한 이미지에 대한 텍스트를 함께 읽어주고 그렇지 않은 경우에는 '화면 중앙'과 같은 위치 정보만 읽어주게 됩니다.

따라서 위치 정보만 읽어준다면 개발팀에서 대체 텍스트를 추가한 것이라고 볼 수 있겠습니다.

[Android native] replaceAccessibilityAction 사용법

Webacc NV | 2020-11-05 09:53:08

오늘은 어제에 이어서 replaceAccessibilityAction 사용법에 대해 다룰 차례입니다.

replaceAccessibilityAction에는 4가지의 값이 들어갑니다.

1. AccessibilityAction: 어떤 접근성 액션을 대체할 것인지를 정해 주어야 합니다. 

이것은 말 그대로 접근성 액션을 대체하는 것이기 때문에 해당 요소에 클릭이면 클릭, 롱클릭이면 롱클릭 등의 접근성 액션이 이미 들어가 있는 상태여야 하겠습니다.

2. 레이블: 접근성 액션에 따라 톡백에서 디폴트로 가지고 있는 레이블 값을 변경할 때 사용합니다.

예를 들어 클릭 액션에 대한 디폴트 레이블이 활성화 하려면 이중탭하세요 인데 이것을 재생하려면 이중탭하세요 로 변경한다면 레이블을 넣는 란에 "재생" 과 같이 값을 줍니다.

참고로 지난번에 아티클에서도 다룬 적이 있는데 doubletap to 부분은 변경할 수 없습니다.

레이블과 관련이 없는 접근성 액션을 대체하거나 레이블이 아닌 액션만 변경할 경우에는 레이블은 "" 로 넣으면 됩니다.

3. AccessibilityViewCommand: 개발자가 디폴트로 구현한 액션이 아닌 접근성 서비스에서 액션을 따로 지정하는 경우에 사용합니다.

쉽게 예를 들어 개발자가 완료 버튼 클릭 액션을 하면, 즉 톡백을 사용하지 않는 사용자가 탭을 하면 햄버거 주문이 바로 완료된다고 가정합시다.

그렇다면 톡백 사용자도 이중탭을 하면 햄버거 주문이 완료될 것입니다.

그런데 톡백을 실행하고 클릭 액션 즉 이중탭을 하면 햄버거 주문을 완료하겠는지 팝업을 띄우고 싶다고 가정합시다.

이때 AccessibilityViewCommand를 사용할 수 있습니다.

따라서 AccessibilityViewCommand 안에는 사용자가 해당 액션을 했을 때의 구현에 대한 자바 혹은 코틀린 코드를 넣게 됩니다.

만약 AccessibilityViewCommand 는 변경하지 않고 레이블, 즉 힌트만 변경하고 싶다면 역시 AccessibilityViewCommand 는 null로 두면 됩니다.

4. return true 혹은 false: true를 하게 되면 AccessibilityViewCommand 안의 코드를 실행한 다음에 원래 디폴트로 가진 액션을 함께 실행할 때 사용을 합니다.

그러나 false를 하게 되면 AccessibilityViewCommand 안의 코드만 실행하고 종료됩니다.

다음 시간에는 사용 예제를 통해 힌트 메시지를 변경하는 것에 대해 말씀드리겠습니다.

[Android native] replaceAccessibilityAction이란

Webacc NV | 2020-11-04 10:44:37

지난 팁에서 제가 replaceAccessibilityAction에 대해 다룰 것이라고 말씀을 드렸습니다.

안드로이드는 각 요소의 유형에 따라 클릭, 롱클릭, 이전으로 스크롤, 다음으로 스크롤 등과 같은 접근성 액션이 붙게 됩니다.

이러한 액션의 종류는 클릭, 롱클릭 외에도, 이전으로 스크롤, 다음으로 스크롤, 포커스, 포커스 초기화 등 대략 30개가 넘습니다.

접근성 액션은 요소 유형에 따라 시스템에서 자동으로 추가되기도 하지만 상황에 따라 수동으로 액션을 추가하거나 삭제할 수도 있습니다.

그런데 시스템에서 자동으로 추가되어 있는 접근성 액션의 레이블이나 동작을 다른 레이블이나 동작으 변경하고 싶을 때가 있습니다.

예를 들어 클릭을 가지고 생각해봅시다.

만약 버튼 하나를 만들었다면 action_click이라는 접근성 액션이 시스템에서 자동으로 부여됩니다.

action_click 접근성 액션이 삽입되었기 때문에 버튼에 포커스 하면 활성화 하려면 이중탭하세요 라는 힌트가 출력되는 것입니다.

그리고 클릭 액션의 동작 즉 이중탭은 기본적으로 접근성 액션에 대한 수정 작업을 하지 않는 한 시스템 클릭이 동작을 합니다.

여기서 말하는 시스템 클릭이란 개발자가 일반적으로 클릭을 구현한 그 동작이 실행된다는 것입니다.

그런데 클릭의 레이블을 활성화 하려면 이중탭하세요 가 아니라 재생 작업을 하려면 이중탭하세요 등으로 레이블을 변경하고싶거나 톡백의 클릭 액션과 톡백을 끄고 클릭을 했을 때의 액션을 다르게 지정해야 할 때가 있습니다.

이럴 때 사용할 수 있는 것이 바로 replaceAccessibilityAction 입니다.

정리하자면 replaceAccessibilityAction은 현재 포함되어 있고 디폴트로 동작하는 AccessibilityAction을 다르게 수정해야 할 때 사용하는 메소드입니다.

그럼 다음 팁에서는 replaceAccessibilityAction의 코드 사용방법과 편집창에서 어떻게 적용할 수 있는지를 다루도록 하겠습니다.

[Android native] 편집창의 기존 레이블 오버라이드하기

Webacc NV | 2020-11-03 09:22:22

안드로이드에서는 무언가를 입력할 수 있는 EditText 편집 영역을 톡백에서 수정창이라고 읽어주며 해당 수정창에는 android:hint 속성을 통해 무엇을 입력해야 하는지를 표시하게 됩니다.

또한 EditText와 입력 레이블을 표시하는 view가 분리된 경우에는 HTML와 마찬가지로 스크린 리더, 즉 톡백이 편집창에만 포커스 해도 레이블을 읽을 수 있도록 labelFor 속성을 사용합니다.

그런데 편집창에 따라서는 화면에 보여지는 레이블과 톡백이 읽어주는 레이블을 다르게 표시하고 싶을 때가 있을 수 있습니다.

대표적인 예를 두 가지 들어보겠습니다.

1. 전화번호 입력 편집창의 화면에 보여지는 텍스트가 전화번호(-) 제외 라고 표시되고 있을 때: 이때톡백은 -라는 글자를 읽어주지 않습니다.

2. 검색어를 입력하는 편집창의 화면에 보여지는 텍스트가 메뉴검색 인경우: 자동완성을 지원함을 톡백 사용자에게 알리고 싶을 때.

지난 팁에서 EditText에는 contentDescription을 사용하면 안 된다고 말씀을 드렸습니다.

그리고 이미 androd:hint 속성이 제공되고 있을 때는 contentDescription 속성을 넣어도 톡백이 읽어주는 레이블이 contentDescription 레이블로 변경되지 않습니다.

게다가 AccessibilityNodeInfo 객체 내의 setHintText를 변경해도 이미 android:hint 속성이 들어가 있기 때문에 레이블이 변경되지 않습니다.

그렇다면 이 문제를 어떻게 해결해야 할까요?

여러 방법이 있을 수 있겠지만 방법 중 하나가 바로 replaceAccessibilityAction을 사용하는 것입니다. 

다음 팁에서는 replaceAccessibilityAction에 대해서 살펴보고 android:hint 속성 변경을 어떻게적용할 수 있는지 살피겠습니다.

[Android native] 웹뷰의 페이지 로딩 화면을 구현할 경우

Webacc NV | 2020-11-02 19:43:27

안드로이드 웹뷰에 대한 팁은 이번이 마지막이 될 것 같습니다.

지난 안드로이드 웹뷰 팁에서는 페이지 로딩 시에 페이지 제목을 자동으로 읽어주게 함으로써 스크린 리더 사용자가 페이지가 갱신되었음을 알리게 하는 방법에 대해 공유했습니다.

오늘은 페이지 로딩중에 대한 이야기를 해보겠습니다.

화면에 페이지가 로딩중이라는 것을 표시하는 경우에는 스크린 리더 사용자 역시 이를 알 수 있어야 합니다.

일반적으로 페이지 로딩 구현은 ProgressBar를 많이 사용하기 때문에 ProgressBar가 화면에 표시되는 경우 다음 예시와 같이 대체 텍스트 추가 및 ProgressBar에 초점이 자동으로 이동되도록 구현해 주면 됩니다.

이렇게 하면 페이지 로딩 시에 초점이 ProgressBar로 이동되며 로딩이 되고 있는 진행률을 실시간으로 읽어주게 됩니다.

@Override
    public void onProgressChanged(WebView view, int newProgress) {
        progressBar.setProgress(newProgress);
        progressBar.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
        progressBar.setContentDescription(newProgress+"%");
    }
});

[NVDA & WAI-ARIA] aria-current 속성 처리 업데이트 소식

Webacc NV | 2020-10-30 11:35:03

몇주 전에 NVDA에서 크롬 브라우저 사용 시에 DOM 새로고침 없이 스크립트로 aria-current 속성이 변경되는 경우에 이를 NVDA 가상커서가 캐치하지 못한다는 이슈에 대해 공유하였습니다.

그런데 NVDA 개발자버전에서 해당 이슈가 일부 수정되어 aria-current 변경된 값을 사용자가 인지할 수 있게 되었습니다.

NVDA 개발자분의 말에 의하면 aria-current 속성이 변경될 때 크롬 브라우저에서는 IA2_EVENT_TEXT_ATTRIBUTE_CHANGED 이벤트를 발생시킨다고 합니다.

Firefox 브라우저에서는 크롬과는 달리 IA2_EVENT_OBJECT_ATTRIBUTE_CHANGED 이벤트를 발생시킨다고 합니다.

따라서 NVDA에서 크롬 브라우저에서 발생시키는 IA2_EVENT_TEXT_ATTRIBUTE_CHANGED 이벤트를 캐치하도록 수정하여 어느정도 해결이 된 것입니다.

그런데 제가 어느정도라는 말을 사용하는 이유는 aria-current 속성이 변경되었을 때 음성으로 그것을 바로바로 피드백 해 주지 못하기 때문입니다.

다른 곳에 포커스를 했다가 다시 변경된 요소에 포커스를 해야만 변경된 aria-current 속성을 들을 수 있습니다.

이것을 해결하려면 크롬 브라우저 자체에서 aria-current 속성이 변경될 때 firefox 브라우저와 같이 IA2_EVENT_OBJECT_ATTRIBUTE_CHANGED 이벤트를 발생시켜야 할 것으로 보입니다.

그래도 어느정도 변경된 aria-current 속성을 지원하게 된 것은 잘된 일이며 다음 업데이트 되는 NVDA 정식 버전에서 반영되길 기대해 봅니다.

또한 크롬 브라우저에서의 이벤트 속성 변경 역시 기대해 봅니다.

[Android native] 앱뷰에 웹뷰 삽입 시에 페이지 로딩 완료되면 웹페이지 제목 읽어주게 하기

Webacc NV | 2020-10-29 09:18:27

어제는 한 화면에 웹뷰와 앱뷰 요소가 함께 있을 때 앱뷰 요소와 웹뷰 요소를 톡백으로 구분하는 방법에 대해 함께 살폈습니다.

오늘은 앱뷰 안에 웹뷰 구현 시 접근성 적용에 대해 함께 생각해 보려고 합니다.

크롬 브라우저에서는 웹뷰 내에서 특정 링크를 눌러 페이지가 로딩이 되면 로딩 완료시에 스크린 리더 초점이 웹뷰로 이동하는 것을 알 수 있습니다.

이렇게 되면 스크린 리더 사용자는 두 가지를 알 수 있습니다.

1. 페이지 로딩이 완료되었다는 것.

2. 변경된 페이지 타이틀.

하지만 앱뷰 안에 웹뷰를 구현하는 경우에는 기본적으로 페이지가 새로고침이 되어도 스크린 리더에서는 아무런 변화가 없으므로 페이지 로딩이 완료된 것을 알 수 없습니다.

따라서 sendAccessibilityEvent 메소드를 활용하여 페이지 로딩이 완료되었을 때 스크린 리더 포커스를 웹뷰로 보내주는 접근성 구현이 필요하며 예시는 다음과 같습니다.

public void onPageFinished(WebView view, String url) {
new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            browser.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
        }
    }, 1000);
}

여기서 약간의 시간 딜레이를 준 이유는 딜레이를 주지 않으면 어떤 경우에는 웹페이지 쪽으로 초점이 이동되지 모하는 경우가 있기 때문이며 이에 대해서는 각 웹뷰별로 테스트가 필요할 수 있습니다.
}

[Android native] 앱뷰 내에 웹뷰를 포함시키는 경우

Webacc NV | 2020-10-28 09:18:25

콘텐츠에 따라 앱뷰 내에 웹뷰를 삽입하여 화면을 구성하는 경우가 많습니다.

접근성을 테스트 하고 접근성 문제에 대한 해결방안을 제시하려면 현재 탐색하고 있는 요소들이 앱뷰인지 웹뷰인지를 판단하는 것이 중요합니다.

앱뷰냐 웹뷰냐에 따라 해결방안이 전혀 달라질 수 있기 때문입니다.

또한 웹뷰를 구성할 때는 페이지 로딩 시에 페이지 제목을 자동으로 읽어줄 수 있도록 하는 접근성 구현이 필요합니다.

이번에도 두 차례에 걸쳐 웹뷰에 관련된 팁을 공유해 보려고 합니다.

오늘은 톡백으로 접근성 테스트 시에 현재 테스트하고 있는 요소가 웹뷰인지 앱뷰인지 확인하는 방법 몇 가지를 공유하고 다음 팁에서는 웹뷰 구현 시에 페이지 로딩 시 제목을 자동으로 읽어주도록 하는 구현 방법에 대해 다룰 것입니다.

톡백으로 접근성 테스트 시에 현재 탐색하고 있는 요소들이 앱뷰인지 웹뷰인지 확인하는 방법은 다음과 같습니다.

1. 앱뷰에는 제목에 대한 레벨이 없습니다. 따라서 제목 3, 제목 2와 같이 읽어준다면 웹뷰 콘텐츠 입니다.

2. 현재 포커스 하고 있는 객체에서 한 손가락 아래 쓸기를 했을 때 웹뷰와 앱 뷰의 탐색설정 순서가 다릅니다. 앱뷰에서는 문자, 단어,, 줄(줄은 없을 수도 있음), 단락, 제목, 링크, 컨트롤 기본값 순으로 탐색값이 변경됩니다.

웹뷰는 제목, 링크, 컨트롤, 문자, 단어, 줄, 기본값 순으로 탐색 단위가 변경됩니다.

이러한 탐색 순서를 통해서 웹뷰인지 앱뷰인지를 판단한 다음 접근성 테스트를 진행한다면 좀 더 실질적인 해결방안을 도출할 수 있을 것입니다.

[Android native] NestedScrollView 접근성 적용 예시 코드

Webacc NV | 2020-10-27 09:21:42

지난 팁에서는 NestedScrollView 사용 시에 스크롤 동작에 대한 접근성을 구현하지 않으면 스크린 리더 사용자가 한 손가락 쓸기를 이용해서 탐색 시에 화면이 자동으로 스크롤 되지 않는다는 것에 대해 살폈습니다. 

오늘은 이에 대한 접근성을 적용하기 위한 샘플 코드를 공유하려고 합니다.

하나의 화면에 1, 2, 3, 4의 각 텍스트뷰가 있다고 가정해 봅시다.

해당 텍스트뷰는 NestEdScrolView 안에 자식으로 포함되어 있고 현재 화면에는 1과 2만 표시되고 있습니다.

기본적으로는 앞에서 말씀드린 바와 같이 톡백 사용자가 한 손가락 쓸기로 탐색을 하면 마치 화면에는 1과 2만 있는 것처럼 표시되고 더 이상 스크롤이 되지 않습니다.

이것을 우리는 한 손가락 쓸기를 통해 숫자 2에 포커스가 되면 자동으로 스크롤이 되면서 3, 4가 보이게 하고 3에 포커스를 하면 역시 자동으로 스크롤이 되면서 이전 숫자들인 1과 2가 표시되도록 해볼 것입니다.

1. 우선 위로 스크롤과 아래-로 스크롤 두 개의 메소드를 만듭니다.

위로 스크롤 메소드:

private void expandBottomBoard() {
    final NestedScrollView bottomBoard = findViewById(R.id.bottom_board);
    bottomBoard.post(new Runnable() {
        @Override
        public void run() {
            final BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomBoard);
            if (behavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) {
                behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
            }
        }
    });
}아래로 스크롤:
private void collapseBottomBoard() {
    final NestedScrollView bottomBoard = findViewById(R.id.bottom_board);
    bottomBoard.post(new Runnable() {
        @Override
        public void run() {
            final BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomBoard);
            if (behavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
                behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
            }
        }
    });
}

2. 이제 AccessibilityNode가 숫자 2에 포커스를 하면 위로 스크롤을 하게하고 3에 포커스를 하면 아래로 스크롤을 하도록 위의 두 메소드를 AccessibilityNodeInfo에 다음과 같이 적용시킵니다.

TextView text1View = findViewById(R.id.text1);
text1View.setAccessibilityDelegate(accessibilityDelegate1);
TextView text2View = findViewById(R.id.text2);
text2View.setAccessibilityDelegate(accessibilityDelegate2);
final View.AccessibilityDelegate accessibilityDelegate2 = new View.AccessibilityDelegate() {
    @Override
    public boolean performAccessibilityAction(View host, int action, Bundle args) {
        if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) {
            expandBottomBoard();
        }
        if (super.performAccessibilityAction(host, action, args)) {
            return true;
        }
        return false;
    }

};

이렇게 하면 2라는 숫자에 포커스가 되면 자동으로 화면을 스크롤하게 되며 3이라는 숫자에 포커스 했을 때의 방법도 위와 유사합니다.

[Android native] NestedScrollView와 스크롤 접근성 문제

Webacc NV | 2020-10-21 17:19:12

스크롤뷰나 리스트뷰 등을 가진 콘텐츠는 기본적으로 스크롤 동작을 지원합니다. 

톡백에서도 이러한 뷰 안에서는 사용자가 스크롤 제스처를 하지 않아도 자동 스크롤 기능을 오프 하지 않은 이상 한 손가락 쓸기를 통하여 화면 탐색 시에 보여지는 화면 그 이상의 콘텐츠가 존재할 경우 자동으로 스크롤 동작을 수행하게 됩니다.

그런데 NestedScrollView 사용 시에는 하위에 여러 뷰가 존재하고 스크롤이 가능함에도 톡백에서 두 스크롤 제스처를 별도로 하지 않는 이상 화면이 스크롤되지 못합니다.

따라서 스크린 리더 사용자는 NestedScrollView로 구현된 콘텐츠 탐색 시에 스크롤 되는 화면인지를 알기 어렵습니다.

이 문제를 해결하려면 AccessibilityNodeInfo의 AccessibilityAction ACTION_FOCUS 속성을 사용하면 됩니다.

즉 화면 하단의 NestedScrollView 하위 뷰에 포커스 되는 경우 자동으로 스크롤을 확장하도록 하는 것입니다.

다음 팁에서는 이에 대한 샘플 코드를 공유하도록 하겠습니다.

[WAI-ARIA & NVDA] 크롬 브라우저에서의 aria-current 처리 관련

Webacc NV | 2020-10-19 10:07:25

aria-current 속성은 주메뉴 중 어떤 페이지가 선택되어 있는지, 회원가입 단계에서 현재 어느 단계에 있는지, 롤링 배너 영역 중 현재 어느 배너가 롤링되고 있는지 등을 표시할 때 활용됩니다.

aria-current 속성을 지원하는 NVDA와 같은 스크린 리더에서는 해당 속성이 부여된 요소에 포커스 하면 현재 페이지, 현재 단계 등의 정보를 읽어주게 되므로 스크린 리더 사용자가 여러 요소 중 무엇이 현재 선택되어 있는지를 바로 알 수 있게 됩니다.

그런데 현재 NVDA와 크롬 브라우저 사용시 DOM 새로고침 없이 aria-current 속성 변경이 스크립트를 통해 이루어질 경우 NVDA 가상커서, 즉 브라우즈모드가 이를 캐치하지 못하는 이슈가 있습니다.

이 부분에 대해서는 샘플 페이지를 통해 테스트해보실 수 있습니다.

또한 이 부분에 대한 NVDA 버그를 리포트 한 상태이며 NVDA 깃허브 이슈 페이지를 통해 수정을 위한 논의 과정을 확인할 수 있습니다.

[웹접근성] firefox 추가기능 axSHammer 개발과 WAI-ARIA 오남용 관련

Webacc NV | 2020-10-16 12:43:36

웹접근성에서 빠질 수 없는 것이 WAI-ARIA 입니다.

WAI-ARIA를 잘 접목하여 접근성을 개선하면 다양한 상황에서 스크린 리더 피드백을 컨트롤 할 수 있어 스크린 리더 사용자가 웹페이지를 효율적으로 탐색할 수 있도록 도울 수 있습니다.

그러나 WAI-ARIA를 잘못 사용하면 사용하지 않는 것보다 더 사용성을 해치게 됩니다.

특정 페이지를 언급할 수는 없지만 aria-hidden 속성이 잘못 마크업되어 있어 꼭 읽어야 할 콘텐츠를 읽지 못하거나 aria-live를 잘못 사용하여 수시로 광고 콘텐츠를 들어야 하는 등의 경험을 자주 하곤 합니다.

WAI-ARIA를 사용할 때는 최소한으로, 적절한 유형으로 사용해야 하겠습니다.

WAI-ARIA를 잘못 사용하는 것은 비단 우리나라 뿐만은 아닌 것 같습니다.

그래서 James Teh 개발자가 axSHammer라는 firefox 브라우저 추가 기능을 개발하고 있다고 합니다.

해당 추가기능을 사용하면 스크린 리더 사용자가 페이지를 탐색하다가 뭔가 WAI-ARIA를 잘못 사용하여 읽어주지 않거나 과하게 읽어준다고 판단되는 것이 있을 때 aria-live, aria-hidden, role aplication 등의 속성을 disable 시켜 페이지를 탐색할 수 있도록 돕는 것입니다.

그럼 또 다음에 더 좋은 팁으로 찾아뵙겠습니다.

[HTML - WAI-ARIA] aria-hidden 속성의 이해

Webacc NV | 2020-10-14 12:09:30

WAI-ARIA의 속성에는 모든 요소에 적용가능한 aria-hidden이라는 속성이 존재합니다. 이름 그대로 hidden, 숨김처리와 관련된 속성으로, 시각적으로는 보이지만, 스크린리더 상에서 해당 요소를 인식하지 못하도록 접근성 트리에서 숨기는 속성입니다.

aria-hidden을 true로 요소에 적용하면 눈으로는 보고 접근할 수 있지만 마치 css에서 display:none이나 visibility:hidden 속성을 사용해서 숨긴 것처럼 스크린리더 상에서는 전혀 읽을 수 없는 상태가 됩니다.

이번 글에서는 aria-hidden에 대한 오해나 다른 플랫폼에서는 가능하지만 aria-hidden 특성으로 인해 불가능한 기능을 구현하려고 한 사례 중점으로 aria-hidden에 대한 얘기를 Q&A 형식으로 풀어보고자 합니다.

 

Q1. aria-hidden="true" 처리된 div 안에있는 자식 div에 aria-hidden = "false"를 사용했는데 스크린리더로 읽지 못해요. 왜 그런건가요?

모바일, 안드로이드를 기준으로 설명드리자면, Android 접근성 객체에서는 importantForAccessibility의 값을 "no"로 지정하면 특정 자식만을 접근성 요소로 나타나게 하고, 이 외의 모든 것을 숨길 수 있습니다. 그런데, 아직 HTML에서는 이 기능을 손쉽게 구현할 수 없습니다. aria-hidden 특성상 aria-hidden="true"로 지정된 컨테이너의 하위 요소에 아무리 aria-hidden="false"값을 주어도 해당 요소를 표시할 수 없는 것이지요. 이 실수는 국내 웹페이지에서 레이어 팝업이나 대화상자를 커스텀으로 제작했을 때, 가장 많이 발견되는 사례입니다.

따라서 이를 해결하려면 대화상자를 구현할 때에는 주 콘텐츠를 담는 wrapper 영역과 분리된 형제 구조의 div로 dimmed처리를 진행하고, absolute 포지셔닝으로 화면을 덮는 방식으로 시각적 효과를 구현한 다음, dialog가 펼처젔을 때, wrapper에 aria-hidden을 적용해 주는 것이 좋습니다.

 

Q2. 사용자분께서 Tab 키로 저희 웹사이트를 탐색하던 아무런 음성도 출력하지 않을 때가 있다고 합니다. 왜 그럴까요?

aria-hidden은 접근성 트리 상에서만 숨기는 것이지, 키보드 초점을 제거해주지는 않습니다. 따라서, 키보드 초점은 여전히 이동 가능한 상태이므로, Tab 키로 aria-hidden이 있는 요소를 탐색하게 되면 아무것도 안 읽게 되는 것이지요.

 

Q3. 대화상자를 만들 때, 키보드 초점이 바깥 요소로 빠져나가는 것은 막았는데요. 모바일 스크린리더 초점이 대화상자 dimmed 레이어 아래로 빠저나가는 것은 어떻게 막나요?

대화상자(role="dialog")를 사용하면, PC 스크린리더인 NVDA에서는 기본적으로 해당 요소 외의 다른 요소를 가상커서로 탐색할 수 없도록 자동으로 방지해줍니다. 하지만, Talkback은 현재 그렇지 않은 것으로 알고있습니다. 이럴 때 aria-hidden을 사용하면 됩니다.

aria-hidden을 true값으로 설정하게 되면 화면을 쓸어 탐색하는 임의탐색이나, 한 손가락 오른쪽 또는 왼쪽으로 쓸어서 탐색하는 순차탐색 제스처에서 요소를 감지하지 않습니다. 물론, 아무리 모바일 페이지라고 하더래도, aria-hidden만을 적용해서는 안 됩니다. 터치가 불편한 지체장애인 사용자는 모바일도 블루투스 키보드를 통해 사용하는 분들이 많으며, 시각장애인 사용자 또한 마찬가지입니다.

 

Q4. aria-hidden으로 처리된 특정 HTML 속성이나 aria 속성으로 id 참조가 가능한가요? 참조가 불가능하다면 어떤 문제가 생기나요?

id로 참조하는 aria 속성(labelledby, describedby)은 참조가 가능하며, 아무런 문제가 생기지 않습니다. 하지만, table의 caption이나 figure의 figcaption, input이나 select, textarea에 사용되는 label 태그는 aria-hidden으로 숨기게 되면 특정 디바이스에서 해당 텍스트를 인식하지 못해 레이블을 읽지 못하는 문제가 생깁니다. 덧붙여서 aria-labelledby나 aria-describedby 속성은 CSS의 display:none; visibility:hidden; 또는 최신의 랜더링 숨김 처리 방식을 지정하는 content-visibility 속성을 사용하여 숨기더라도 문제없이 참조가 가능합니다.

[HTML5 & 스크린 리더] required 속성 처리에 관하여

Webacc NV | 2020-10-12 12:01:16

회원가입과 같은 폼컨트롤을  사용할 때 스크린 리더 사용자가 각 체크박스, 편집창과 같은 요소가 필수인지 아닌지를 알아야 하는 것은 두말할 나위가 없습니다.

각 요소가 필수인지를 스크린 리더 사용자에게 알리는 방법은 '필수'와 같이 텍스트를 써주거나 aria-required, aria-invalid 속성을 사용하는 것 혹은 HTML5의 required 속성을 사용하는 방법 등이 있겠습니다.

오늘은 HTML5의 required 속성 사용 시 스크린 리더의 읽어주는 것 관련하여 도움이 되실만한 팁을 몇 가지 정리해 보도록 하겠습니다.

1. aria-required의 경우에는 aria-invalid 속성을 함께 사용해야 스크린 리더가 해당 요소가 필수 입력이라는 것과 조건에 맞게 입력하지 않았을 경우 입력 값이 유효하지 않음을 함께 읽어줍니다.

하지만 HTML5의 required 속성을 사용하면 invalid 속성이 함께 포함되어 있습니다.

즉 아이디 편집창이 있고 최소 입력값을 설정하는 minlength 값이 8로 설정되어 있으며 required 속성이 들어가 있으면 스크린 리더는 아이디 편집창이라는 것과 필수 입력이라는 것, 입력값이 유효하지 않다는 것을 함께 읽어주게 됩니다.

다만 minlength 값을 충족하여 8자 이상이 되면 입력 값이 유효하지 않다는 invalid 속성은 살아지고 필수 입력이라는 정보만 남게 됩니다.

2. 모바일 스크린 리더인 보이스오버와 톡백은 HTML5 required 속성을 일부만 지원합니다.

즉 톡백의 경우는 invailid, 보이스오버는 required 속성만 읽어줍니다.

이 부분은 각 스크린 리더에서 빠르게 개선되기를 바라봅니다.

[Android native] 톡백에서 지원하는 요소 이름, 유형, 상태 말하기 순서 설정과 AccessibilityNodeInfo 정보변경 관련

Webacc NV | 2020-10-07 09:46:12

지난 팁에서는 AccessibilityNodeInfo 컨트롤 유형 변경시에 반드시 컨테이너뷰가 아닌, 텍스트 혹은 대체 텍스트를 포함하는 커스텀 컨트롤을 사용한 뷰 자체를 수정해야 한다는 것에 대해 다루었습니다.

이번 팁에서는 왜 그렇게 해야 하는지에 대해 다루어 보려고 합니다.

톡백에서는 요소의 레이블, 유형, 상태정보가 있다고 가정했을 때 그것을 어떤 순서로 읽어줄 것인지를 설정하는 기능을 가지고 있습니다.

만약 사용자가 유형, 이름, 상태 순으로 읽도록 설정하였다면 '체크박스, 햄버거 주문, 선택함'과 같이 읽어주게 됩니다.

따라서 스크린 리더 사용자는 본인의 취향 및 정보 탐색의 성격에 따라 읽기 방식을 변경하여 사용합니다.

그런데 요소 유형을 컨테이너뷰에 제공하게 되면 해당 컨테이너뷰에는 레이블을 포함하지 않기 때문에 사용자가 읽기 순서를 변경하더라도 무조건 요소 유형을 앞에 읽을 수밖에 없으며 사용자의 설정이 제대로 동작하지 않게 됩니다.

즉 컨트롤, 레이블, 상태정보가 하나의 뷰에 포함되어 있어야만 톡백이 사용자의 읽기 순서 설정을 반영할 수 있습니다.

위의 내용을 바탕으로 생각해보면 요소 유형이나 상태정보를 대체 텍스트로 넣지 않아야 하는 이유에 대해서도 우리는 알게 됩니다.

요소 유형이나 상태 정보를 대체 텍스트로 포함하는 경우에도 톡백은 이를 레이블로만 인식하므로 사용자의 읽기 ㅓㄹ정을 반영하지 못합니다.

[Android native] AccessibilityNodeInfo 클래스 사용시 주의해야 할 사항

Webacc NV | 2020-10-06 14:13:06

커스텀 컨트롤을 스크린 리더에서 체크박스, 버튼 등으로 읽도록 하기 위해 AccessibilityNodeInfo setClassName 메소드를 사용해야 할 경우가 종종 있습니다.

그런데 해당 AccessibilityNodeInfo 객체를 만들 때는 반드시 컨테이너 뷰가 아닌, 그 컨트롤을 가지고 있는 뷰 자체, 즉 텍스트 혹은 대체 텍스트가 있는 뷰에 선언을 해 주어야 합니다.

예를 들어보겠습니다.

체크박스를 ImageView를 사용하여 커스텀으로 제공했다고 가정해 봅시다.

그런데 그 이미지뷰 상위에는 <LinearLayout> 컨테이너가 있으며 해당 컨테이너에는 ImageView 하나만 있다고 가정해 봅시다.

<LinearLayout>

 <ImageView>

 android:text="햄버거 먹기"

</>

</LinearLayout>

이때 체크박스로 읽도록 AccessibilityNodeInfo 객체를 선언해 주어야 하는 것은 LinearLayout이 아니라 ImageView입니다.

컨테이너 뷰에 선언을 해도 체크박스라고 읽긴 하지만 사용성에서 약간의 문제가 있습니다. 

이 문제에 대해서는 다음 팁에서 말씀드리겠습니다.

table tr마크업 문의

eirene100999 | 2020-10-05 17:31:09

 table을 작업하다가 문의사항이 있어 올려봅니다

아래 <html 마크업 이미지>에 (빨간라인참고) table 첫번째 tr의 태그에 rowspan값을 tr갯수로 넣고 웹화면을 보았는데 깨지지 않아서요

<html 마크업 이미지>에 있는 마크업처럼 접근성과 사용성방면으로 사용해도 되는지 궁금합니다

(웹화면의 결과는 ie, FF 모두 동일하게 깨지지 않습니다)

 

<html 마크업 이미지>

샘플 html

 

<웹 결과 화면>

샘플 크롬화면

 

 

[iOS native] iOS 14.2에서의 특정 웹뷰 포커스 튀는 버그 해결 관련

Webacc NV | 2020-10-05 15:37:25

얼마전 iOS 14로 업데이트 되면서 특정 웹뷰에서 스크린 리더 사용자가 한 손가락 쓸기로 화면 탐색 시에 보이스오버 포커스가 첫 요소로 튀는 이슈에 대해 공유한 적이 있습니다.

지난 번에도 말씀드린 바와 같이 해당 이슈는 보이스오버에서의 특정 웹뷰 처리 시 버그이며 현재 해당 버그는 14.2 버전에서 해결되었습니다.

물론 14.2 버전은 아직 개발자 버전으로만 배포되어 정식 출시되지는 않았지만 접근성 테스트 시에 특정 웹뷰에서 포커스가 상단으로 튀는 이슈 발생 시 참고 부탁드리며 이후 정식 버전 출시 때 업데이트 하시면 해당 이슈는 없을 것입니다.

[Android native] 커스텀 체크박스 접근성 적용 예제

Webacc NV | 2020-09-28 12:49:33

지난 팁에 이어서 오늘은 안드로이드 커스텀 체크박스를 스크린 리더에서 체크박스 및 체크됨, 체크안 됨을 읽을 수 있도록 하는 예제 코드를 공유하려고 합니다.

먼저 체크박스는 ImageView로 구현하였다고 가정하며 isChecked 라는 boolean 메소드를 통해서 상태가 변경됩니다.

checkBox.setAccessibilityDelegate(checkBoxAccessibilityDelegate);
final View.AccessibilityDelegate checkBoxAccessibilityDelegate = new View.AccessibilityDelegate() {
    @Override
    public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
        super.onInitializeAccessibilityNodeInfo(host, info);
        info.setClassName("android.widget.CheckBox");
        //setCheckable을 적용해야 음성 안내
        info.setCheckable(true);
        info.setChecked(isChecked);
    }
};

[Android native] 커스텀 체크박스에 접근성 적용하기

Webacc NV | 2020-09-25 12:40:52

여러 번 말씀드린 바와 같이 항상 커스텀 컨트롤을 사용해서 체크박스, 버튼, 라디오버튼 등을 구현하는 경우는 접근성 고려에 더 많은 고민이 필요합니다.

그래서 접근성 구현 관점에서 생각해본다면 각 요소의 네이티브 컨트롤을 사용하는 것이 가장 좋은 것임은 두말할 나위가 없습니다.

그 중 하나가 체크박스입니다. 

체크박스 역시 네이티브 CheckBox 클래스를 사용하게 되면 접근성 구현에 대한 별다른 대응을 하지 않아도 됩니다.

체크박스라는 유형, 체크가 가능하는 것, 현재 상태 등을 스크린 리더가 사용자에게 정확하게 알려주기 때문입니다.

그런데 체크박스를 표준 컨트롤을 사용하지 않고 ImageView를 사용하여 구현했다고 가정해 봅시다.

체크가 되었는지 그렇지 않은지는 이미지뷰에서 백그라운드 이미지 파일을 변경해서 표시했다고 가정합시다.

그러면 스크린 리더 사용자는 해당 뷰가 체크박스인지 조차 알 수 없으며 

체크가 되었는지도 알 수 없을 것입니다.

이때 접근성을 구현하기 위해 AccessibilityNodeInfo 속성을 수정할 수 있습니다.

1. 실제로 해당 뷰는 이미지뷰이지만 체크박스라는 것을 알려 주어야 하므로 AccessibilityNodeInfo 객체 안에서 클래스 네임을 체크박스로 변경해야 하고

2. 이미지뷰는 아무런 상태를 가지지 않지만 해당 체크박스는 체크 혹은 체크해제를 할 수 있는 상태정보를 가진다는 것을 알려주기 위해 setCheckable을 true로 설정해야 하며 마지막으로

3. 체크가 되었는지 혹은 해제가 되었는지를 알려주기 위해 setChecked를 조건문에 맞게 변경해 주어야 합니다.

다음 팀에서는 샘플 예시 코드를 공유하겠습니다.

[iOS native] iOS 14 특정 웹뷰 콘텐츠 보이스오버 초점 이슈 관련

Webacc NV | 2020-09-21 16:11:40

얼마전에 iOS 14 정식 버전이 출시되었으며 이번 버전에서도 여러 접근성과 관련된 업데이트가 있었습니다.

그런데 현재 크롬, 사파리 등을 제외한 특정 웹뷰에서 보이스오버 초점이 웹뷰의 첫 요소로 튀어 버리는 이슈가 발생하고 있습니다.

구체적인 증상은 다음과 같습니다.

1. 특정 웹뷰에서 한 손가락 쓸기로 이동 시에 두 번째 링크에 포커스를 하면 초점이 첫 링크로 튀어버리는 이슈가 발생합니다.

2. 한 손가락 오른쪽 혹은 왼쪽 쓸기가 아닌 특정 링크를 임의 터치를 통해 선택한 경우에는 포커스가 튀지 않습니다.

해당 이슈를 애플 접근성팀에 문의한 결과 보이스오버로 특정 웹뷰에 포커스 한 경우 발생되는 버그이며 다음 버전에서 해당 이슈는 수정될 것이라 합니다.

접근성 테스트 하시는 분들은 참고하시면 좋겠습니다.

[Android native] RatingBar class 접근성 적용 예시

Webacc NV | 2020-09-16 18:44:23

지난 글에서 RatingBar 클래스만 사용하게 되면 스크린 리더 사용자가 볼륨키로 점수를 조절할 수 없다는 것에 대해 말씀드렸습니다.

그래서 톡백에서 이 RatingBar를 SeekBar로 인식할 수 있도록 다음 예시와 같이 수정할 수 있습니다.

ratingbar.setAccessibilityDelegate(new View.AccessibilityDelegate() {
    public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
        super.onInitializeAccessibilityNodeInfo(host, info);
        info.setClassName(SeekBar.class.getName());
    }
});

[스크린리더] 가상커서의 이해 - 특정 요소에서 가상커서가 자동으로 꺼지는 이유

Webacc NV | 2020-09-14 15:06:56

학습장애나 시각장애가 있는 개발자분도 이 세상엔 분명 많지만, 대다수는 그렇지 않습니다. 이는 스크린리더를 사용하는 사용자가 적다는 뜻으로도 이해할 수 있습니다. HTML5에서는 시멘틱이 강조되고, UI 형태에 따라 WAI-ARIA의 role을 사용하여 네이티브 앱에 있는 더 많은 유형을 지원할 수 있게 되었습니다.

웹과 앱은 근본적인 차이점이 있습니다. Windows 응용프로그램에서는 가상커서라는 개념을 사용하지 않습니다. 대부분 스크린리더에서 가상커서와 동등한 개념을 가진 기능들은 웹 콘텐츠와 웹엔진을 기반으로 만들어진 소프트웨어(앱)에서만 동작합니다(예외적으로 Microsoft Narrator의 스켄 모드는 Windows 네이티브에서도 동작합니다).

웹기반이 아니라면, 읽기포인터나 객체 탐색와 같은 스크린리더의 다른 탐색 기능을 활용하여 UI 텍스트를 읽고, Tab과 방향키, Space와 Enter만으로 모든 컨트롤을 조작하지요.

하지만 웹에서는 가상커서라는 웹을 탐색하는 별도의 모드와 Windows의 기본 커서가 같이 동작합니다. 그리고 가상커서는 위, 아래, 왼쪽 오른쪽 화살표 키를 텍스트와 요소를 탐색하는 용도로 사용하며, 헤딩(h키) 헤딩 레빌 1부터 6까지(상단 숫자 1키부터 6키) 등, 일반적으로 텍스트를 입력하는 것 외의 아무 기능이 없는 키에 가상커서의 빠른 탐색키를 배치합니다.

role은 단순히 스크린리더에 유형 이름만을 제공하지 않습니다. role은 요소를 읽고 사용자가 조작할 수 있는지(Read & Write), 혹은 읽기만 가능한지(Read Only). 간단하게 말해서 사용자가 상호작용할 수 있는 요소인지를 스크린리더에게 전달합니다. 그리고, 사용자가 조작이 가능하다면 기본 설정상 가상커서를 자동으로 끄게끔 되어있습니다.

가상커서가 동작한다면 글자단위 읽기와 문단 읽기가 화살표 키에 할당되어 있어 라디오버튼을 방향키로 선택할 수 없고, 입력창에서 가상커서가 동작한다면 제목으로 바로가거나, 링크로 바로가는 키가 동작하여 글자를 입력할 수 없기 때문입니다. 

한 문장으로 압축하여 설명드리자면, "이 요소 내에서는 다른 조작방법이 필요합니다"입니다.

Windows에서 제공하는 tab, menu, slider 등 마우스로 클릭하여 선택하거나, 드래그하고, 특정 지점을 눌러 수치를 변경하는 작업 등을 수행하는 요소에는 반드시 키보드 조작법이 있으며, 이러한 키보드 조작이 필요한 요소를 위젯 요소(widget role)라고 합니다.

이러한 위젯 요소는 대부분 키보드 조작이 있으므로 가상커서가 꺼짐을 유의하고, 가상커서가 꺼지는 요소라면 반드시 네이티브와 동일하거나 유사한 조작방법을 꼭 제공해 주세요.

재생시간 조절 슬라이더라고 알려주고, 가상커서도 꺼주는데 오른쪽/왼쪽 방향키로 되감기 기능을 수행할 수 없으면 혼란스러우니까요 :(

 

[JavaScript] 스크린리더 사용자를 위한 재생 슬라이더 시간정보 업데이트에 관하여

Webacc NV | 2020-09-10 13:46:43

웹페이지에서 재생 슬라이더를 만들 때, input[type="range"]과 맥을 같이 하는 role="slider" 역할을 사용하여 커스텀 슬라이더를 만들게 됩니다.

role="slider"에는 총 네가지 보조 속성이 있는데, 실질적으로 스크린리더에 영향을 미치는 속성과 개발자 편의를 위한 속성이 있습니다.

[개발자 편의를 위한 속성]에는 aria-valuemin, aria-valuemax가 있으며, [스크린리더에 영향을 미치는 속성]에는 aria-valuenow와 aria-valuetext가 있습니다.

HTML 5에 추가된 input의 유형중 range에는 max와 min 속성값이 있습니다. 이와 마찬가지로 개발자가 slider를 개발할 때, 최소값 제한과 최댓값 제한을 Number(Element.getAttribute('aria-valuemin')), Number(Element.getAttribute('aria-valuemax')) 이렇게 가져와 이 밑으로 슬라이더의 현재값의 재한을 구현할 때 참조용으로 사용하는 것입니다.

이 중에서 우리가 주목해야할 것은 aria-valuenow와 aria-valuetext입니다.

먼저, aria-valuenow는 input에 value속성을 넣는 것과 같은 역할을 수행합니다. 값으로는 숫자만을 받으며, aria-valuemin과 vlauemax가 설정되있더라도, 강제로 유효하지 않은 값이 들어갈 수 있으므로, 재생 슬라이더를 구현할 때는 반드시 두 속성을 참조하여 인터렉션에 재한을 둬야합니다.

aria-valuetext는 단위 등의 텍스트 보조 정보가 들어와야 할 때 사용하는 속성으로, 재생 슬라이더에서는 다음과 같은 텍스트를 전달하게 됩니다. 이 속성이 없다면, aria-valuenow의 숫자 값을 전달합니다.

const volumeSlider = document.getElementById('slider-vol');
const currentVolume = Number(slider.getAttribute('aria-valuenow'));
volumeSlider.setAttribute('aria-valuetext',`${currentVolume} percent`);

접근성 기능을 제공하고자 이들을 제공한 많은 슬라이더에서 오디오/비디오가 재생하는 동안 이 valuenow / valuetext를 개속 갱신하게끔 해놓은 것을 볼 수 있습니다. 그런데, 그렇게 개속 valuenow / valuetext를 갱신하면 약간의 문제가 있습니다.

aria-valuenow /aria-valuetext는 aria-label처럼 초점을 받은 요소의 해당 속성값이 갱신될 때 마다 스크린리더 사용자에게 정보를 알립니다. 즉, 재생 슬라이더를 조절하려고 초점을 두고 있으면, aria-valuetext가 바뀔 때마다 정신이 없을 정도로 재생시간을 읽어주게 되는 것입니다.

이 aria-valuenow / aria-valuetext는 일반적인 슬라이더처럼 키보드 동작이 발생했을 때(keydown, keyup)나 해당 요소에 초점이 갔을 때(focusin) 상태에서만 갱신이 되어야 합니다.

따라서 위 문제를 해결하려면, 오디오가 업데이트되었을 때에는 valuetext를 갱신하지 않고, 슬라이더에서 값을 조절 좌/우 방향키 또는 Youtube 등에서 제공하는 되감기 키를 눌렀을 때나, 해당 슬라이더를 사용자가 조작하려고 초점을 보냈을 때만 이 값이 바뀌게끔 하면, 이러한 문제를 해결할 수 있으며, 스크린리더 사용자가 편하게 사용할 수 있는 커스텀 슬라이더를 만들 수 있습니다.

[Android native] RatingBar 클래스 적용시 접근성 대응

Webacc NV | 2020-09-10 12:21:01

만족도를 평가하는 별점을 구현할때 일반적으로 RatingBar 클래스를 사용합니다.

그런데 RatingBar 클래스로 구현 시에는 스크린 리더 사용자가 별점을 주기 위해 해당 요소를 길게 이중탭한 상태로 오른쪽 혹은 왼쪽으로 스크롤해서 정확하게 원하는 점수에 맞추어 주어야 합니다.

사실 이중탭한 상태로 왼쪽 오른쪽으로 슬라이드하면서 정확한 값에 맞추는 것이 스크린 리더 사용자에게 쉽지 않기 때문에 이에 대한 접근성 개선이 필요합니다.

개선 방법은 접근성 노드 정보에서 해당 RatingBar 클래스를 SeekBar로 인식하도록 변경해 주면 됩니다.

그렇게 변경하면 실제 클래스는 RatingBar이지만 접근성 노드 정보의 클래스는 SeekBar이기 때문에 볼륨키를 통해서 스크롤이 가능합니다.

이는 SeekBar로 변경하는 순간 SeekBar가 가지고 있는 AccessibilityAction 정보, performAccessibilityAction 이벤트가 부여되기 때문입니다.

코드 예시에 대해서는 다음 팁에서 다루겠습니다.

[WAI-ARIA]aria-label보단 aria-labelledby를 사용해야하는 이유

Webacc NV | 2020-09-08 12:36:07

아이콘 버튼에 스크린리더용 대체텍스트를 제공하는 방법에는 IR기법으로 버튼 내부에 있는 텍스트를 숨기는 방법과 aria-label로 버튼에 대체텍스트를 제공하는 방법이 있습니다.

aria-label은 마치 aria-live를 제공한 것 처럼, 버튼을 눌렀을 때, 레이블 텍스트가 변경되면 이를 알리는 좋은 기능을 수행합니다. 단점도 존재하는데, 바로 웹사이트 번역기능을 사용하는 외국인에게 불친절하다는 점입니다.

aria-label 속성값은 많은 웹사이트 번역기에서 번역하지 못합니다. 일반적인 텍스트 노드만을 번역하는 경우가 많기 때문으로 보입니다.

반면에 aria-labelledby는 이미 입력된 html 요소의 값을 가져오기 때문에 번역이 가능한 대체텍스트를 제공하기 쉽습니다. 웹사이트 번역 API에서 이 텍스트들을 번역할 수 있도록 스크립트로 동적으로 레이블 요소를 생성하지 않고, 정적으로 작성하여 display:none으로 숨겨두는 것이지요.

레이블용 요소들을 display:none으로 숨기더라도, id 레퍼런스에서 가저온 텍스트 값은 유효하기 때문에 시각적으로는 텍스트를 공개하지 않고 편리하게 레이블을 제공할 수 있습니다.

만약, aria-roledescription같이 아이디 레퍼런스로 연결하는 속성이 별도로 없다면 자바스크립트에서 aria-labelledby처럼 스크립트 내부에서 미리 작성된 요소의 아이디를 document.querySelector('#...').innerText나 document.getElementById('id').innerText, JQuery에서 $('#id').text()등으로 불러와서 넣어주면 비슷한 효과를 낼 수 있을 것 같습니다.

[iOS native] accessibilityViewIsModal에 관하여

Webacc NV | 2020-09-07 20:46:01

중첩된 화면에서 가려진 화면에 스크린 리더 초점이 가지 않게 하는 것은 접근성에서 굉장히 중요합니다.

얼마전에 안드로이드 플랫폼에서의 적용 방법에 대해서는 이미 포럼에 공유를 했습니다.

iOS에는 accessibilityViewIsModal 이라는 속성이 있습니다.

이것을 true로 설정하면 같은 계층의 다른 형제들은 스크린 리더에서 포커스가 안 됩니다.

따라서 A, B 컨테이너가 있다고 가정했을 때 현재 화면 상에서 B 컨테이너만 보여지는 경우에는 B 컨테이너에 accessibilityViewIsModal을 true로 설정함으로써 가려진 A 컨테이너에 포커스가 되는 것을 막을 수 있습니다.

모달로 열리는 클래스에 아래 예시와 같이 추가할 수 있겠습니다.

    override var accessibilityViewIsModal: Bool {
        get {
            return true
        }

        set {}

 

[iOS native] ViewController와 화면 변경 알림

Webacc NV | 2020-09-04 12:20:43

보이스오버는 화면이 전환될 때 화면 전환 사운드를 출력함으로써 사용자에게 이를 알립니다.

기본적으로 안드로이드에서는 액티비티가 변경될 때 화면이 변경되었음을 스크린 리더 사용자에게 자동으로 알린다고 말씀드린 적이 있습니다.

그러면 아이폰은 언제 화면이 전환됨을 자동으로 알릴까요?

다른 ViewController로 전환되는 경우에는 특별한 접근성 구현 없이도 보이스오버가 사용자에게 화면이 전환되었음을 알립니다.

그러나 같은 ViewController 안에서 화면이 전환되는 경우에는 screenChangedNotification 메소드를 사용하지 않는 한 보이스오버는 화면이 전환됨을 알리지 않습니다.

따라서 같은 ViewController 안에서 화면이 전환되는 기능을 구현하실 경우에는 반드시 screenChangedNotification 메소드를 적용해 주는 것이 스크린 리더 사용자에게 큰 도움이 됩니다.

            if finished {
                UIAccessibility.post(notification: .screenChanged, argument: nil)
            }

 

개발자가 자주하는 웹 접근성 실수

에어류 | 2020-09-03 13:20:23

안녕하세요! 에어류입니다. 

오늘은 웹 접근성을 적용하는데 있어서 주요 적용직군인 퍼블리셔가 아닌 개발자분들이 자주하는  접근성 실수를 정리해볼까합니다.

우선  아래 그림을 한번 보시겠습니다.

이미지 자체에 자바스크립트를 이용하여 기능을 연결하여 마우스로만 기능실행이 되는 사례(아이디비밀번호 입력창에서 가상키보드 버튼을 자바스크립트로만 구현한 사례와 공인인증서 로그인에서 주민등록번호 입력을 위해 가상키패드 버튼을 자바스크립트로 마우스 이용만을 위해 기능을 제공한 사례)

해당 이미지에서 가상키보드 버튼에 대해 <img src="이미지 경로" alt="가상키보드" onclick="javascript:popup();" /> 

로 제공했다면 키보드가 접근할 수 없는 요소인 이미지 자체에 자바스크립트를 이용하여 기능을 연결하였기 때문에 키보드 사용보장이 되지 않습니다. 

퍼블리셔분들은 쉽게 이해하고 있는 내용이지만 개발자분들이 자주 실수하는 내용 중에 TOP1 입니다. 

 

이를 해결하기 위해서는 <a>, <button> 등의 초점을 받을 수 있는 요소를 사용해주는 것이 가장 좋습니다. 

<a onclick="javascript:popup();"><img src="이미지 경로" alt="가상키보드"></a>

또 가장 많이 실수하는 사례 하나 소개합니다. 

id 값 변경 시 label for 와 함께 변경해달라는 이야기입니다. 

이게 무슨 말이냐고요?

종종 우리는 금융권이나 대형 사이트의 웹 접근성을 적용하는 큰 프로젝트에서 2~3개월에 걸쳐서 퍼블리셔들이 입력도움 검사항목에 따라 입력서식과 레이블을 1:1로 매칭해줍니다. 이를 매칭해주면 입력창이 무엇에 대한 입력창인지 설명이 함께 적용되고, 마우스로 입력창을 찍어 커서를 활성화시킬때에도 레이블 영역까지도 연결이 되어 있어 좁은 입력서식에 커서 활성화가 어려운 운동장애 사용자를 위해서도 적절한 조치가 됩니다. 

그런데 가장 마지막 작업을 하는 개발자분들이 이러한 사실을 잘 이해하지 못하고, id 값 변경에만 신경을 쓴 나머지 label의 값과 매칭 없이 자체적으로 적용해버리는 경우가 있습니다. 이러면 2~3개월간 적용했던 레이블 접근성이 모두 날아가게 되버리죠. ㅠㅠ

입력도움을 위해 label for 값과 input id 값을 다르게 설정한 소스 화면 예시(label for =resident, input id=내챠미_ㅑㅇ1

예시 사례 처럼 레이블의 for 값이 "resident"면 첫번째 input 창의 id 값을 똑같이 "resident"로 해주어야 하는데 "social id_1"로 제공한 사례입니다. 원래 퍼블리셔가 이를 맞춰놓았더라도 개발자가 웹 접근성 지식이 없다면 이런 대참사가 발생할 수 있습니다. 

따라서 퍼블리싱 작업이 끝나면 개발자분에게 상세하게 접근성에 관련되어 실수 가능성이 있는 소스 부분에 대해서는 제대로 커뮤니케이션 하고 진행하시는 것이 중요합니다. 

[HTML & WAI-ARIA] 콤보상자에 aria-activedescendant를 써야하는 이유

Webacc NV | 2020-09-02 12:57:33

국내, 국외 막론하고 현재의 많은 웹페이지에서 자동완성(autocomplete) 기능이 있는 검색 편집창을 사용합니다.
자동완성 편집창이나, 커스컴 콤보 자는 크게, 컨트롤러와 목록 요소 그룹으로 나눌 수 있습니다.
컨트롤러 그룹은 크게 확장 축소를 할 수 있게 돕는 버튼, 키보드 입력이 가능한 경우, 편집창이 있을 수 있으며, 목록 요소 그룹은 말 그대로, 목록 컨테이너와 option 항목이 됩니다.

하지만, 특별한 방법을 사용하지 않는다면 자동완성만을 구현한다고 하여 이 두 요소가 서로 자동으로 연관있는 요소가 되진 않습니다.
WAI-ARIA에는 두 요소 사이의 연관관계를 명시하는 속성이 있습니다. aria-owns, aria-controls, aria-activedescendant 등이 그것이지요.

그중, 콤보상자나, 자동완성 편집창의 컨트롤러에는 aria-activedescendant를 사용합니다. aria-activedescendant는 HTML의 id 레퍼런스를 속성값으로 받으며, 실제 초점과 관계 없이 마치 특정 요소에 초점을 보낸 것과 같은 효과를 줄 때 사용합니다.

웹브라우저의 네이티브 콤보상자를 예로 들어, 실제 초점은 콤보상자에 있지만, 콤보상자를 확장하면, 목록 컨테이너가 표시되며, 목록에는
선택 포커스가 따로 존재합니다.

브라우저에서 Tab을 통한 초점은 하나일 수 밖에 없기 때문에 이런 형태의 UI를 커스텀으로 만들 때,
스크린리더 사용자를 위한 접근성을 지키기 어려운 부분이 있습니다.


이럴 때, 실제 DOM의 초점은 위에서 설명한 컨트롤러에 있으나, 마치 실제 초점이 option 항목에 가 있는 것 처럼 스크린리더 사용자가 콤보상자를 위 또는 아래 화살표키로 탐색했을 때, 읽고있는 항목의 텍스트와 인덱스 정보를 전달해주는 것이 aria-activedescendant입니다.

aria-activedescendant를 꼭 써야하는 이유가 뭔가요?

aria-activedescendant는 단순히 id로 연결된 요소에 대한 정보만을 스크린리더로 전달하지 않습니다.
콤보상자의 경우, 잘 사용하지는 않지만, optgroup이라는 하위 컨테이너가 있습니다. 이는 role="group"과 같은 역할을 합니다. option group을 줄여서 표시한 태그임을 알 수 있지요.

aria-activedescendant는 role="option"의 상위에 있는 부모 요소가 누구인가에 따라, 부모의 정보또한 전달합니다.

<div class="ac-control">
    <input type="text" autocomplete="off" aria-autocomplete="both" aria-controls="select-list" aria- 
    activedescendant="focused-option" role="combobox" aria-label="Search" />
    <button aria-expanded="true">Search</button>
</div>
<ul role="listbox"><!--input에 Br를 입력한 상황을 가정-->
    <div class="optgroup" role="group" aria-label="suggestions">
        <li role="option" data-key="2309">Bravo</li>
        <li role="option" data-key="2310">Braile</li>
        <li role="option" data-key="2311" id="focused-option">Bronze</li>
        <li role="option" data-key="2312">Brother</li>
        <li role="option" data-key="2313">Browser</li>
    </div>
    <div class="optgroup" role="group" aria-label="website-links">
        ...생략...
    </div>
</ul>

위 마크업처럼, option 항목의 부모가 role="group"이라면 마치 optgroup처럼 현재 선택 초점이 간 요소의 그룹또한 읽습니다.

좋은 것은 알겠는데, 그래도 와닿지 않아요. 또 다른 무언가가 있나요?

위처럼 항목에 그룹이 포함되있지 않고, 위/아래 화살표 키를 누르면 바로 자동완성된 텍스트가 편집창에 들어오는 구조라면 스크린리더의 특성상, 편집창에 들어온 글자를 읽습니다. 그래서 안 와닿을 수 있어요.

하지만, 위와 같이 자동완성된 목록이 단순히 글자를 체워넣는 것을 넘어 선택하면 바로 해당 웹페이지로 넘어가는 항목 그룹이 있는 경우, 이에 대한 안내를 지원할 방법이 없으며, 몇 개의 자동완성 제안이 화면에 표시되어 있는지 스크린리더 사용자는 알 수 없습니다.

그리고, 모든 검색 편집창에서 탐색과 동시에 편집창에 텍스트가 체워지는 형태만 있는 것은 아닙니다. 특정 키워드를 입력했을 때, 콤보상자처럼 선택을 진행하는 형태도 있으니까요. 이러한 경우에는 편집창에 아무런 글자도 체워지지 않기 때문에 스크린리더는 "빈줄(blank)"이라는 내용만 사용자에게 전달하게 됩니다. 편집창에 내용이 곧바로 체워지지 않는 형태라면 더욱 더 이를 방지하기 위해 aria-activedescendant가 필요한 것이지요.

 

--참고--

[자동완성편집창 예제] : 자동완성 편집창 예제로 aria-activedescendant가 스크린리더에게 어떤 정보를 전달하는지 체험해보세요. 한글 "올" 자를 편집창에 입력하면 항목이 표시되며, 위/아래 화살표 키로 탐색할 수 있어요.

콤보상자 예제 : 현재 옵션 상자 내 스크롤이 작동되지 않는 버그가 있습니다. HTML의 select 요소를 커스텀으로 구현한 예제입니다.

[Android native] LinearLayout, RelativeLayout 컨테이너에도 필요 시 대체 텍스트 추가 가능

Webacc NV | 2020-09-01 16:47:55

접근성을 고려할 때는 스크린 리더 사용자가 화면을 탐색할 때 현재 사용자가 탐색하는 영역이 어떤 영역인지를 알수 있을까를 고민하는 것이 중요합니다.

ListView, GridView 컨테이너에 대체 텍스트를 삽입해 주면 상황에 따라 편리하다는 팁을 공유한 적이 있는데 읽어주는 방식이 좀 다르긴 하지만 LinearLayout, RelativeLayout 등에도 대체 텍스트를 삽입하여 해당 레이아웃이 어떤 영역임을 알릴 수 있습니다.

다만 ListView, GridView 자체에 대체 텍스트를 삽입한 것과는 읽어주는 방식이 좀 다릅니다.

ListView, GridView는 목록, 테이블로서 시맨틱한 레이아웃으로 인식을 하기 때문에 다른 컨테이너에 있다가 해당 그리드, 리스트 레이아웃에 포커스가 되면 대체 텍스트와 함께 해당 하위 요소를 읽습니다.

예: 사과, 과일 리스트 목록모드 항목 15개.

그러나 LinearLayout, RelativeLayout 등은 시맨틱한 의미를 가지지 않았으므로 대체 텍스트를 삽입하면 메인 콘텐츠에 포커스 되기 전에 초점이 없는 하나의 TextView가 더 포커스 됩니다.

그래서 상황에 따라서는 그 대체 텍스트에 해당 레이아웃의 숨김 제목을 삽입할 수 있으며 android:accessibilityHeading = "true" 속성을 넣음으로써 제목으로 읽어주도록 할 수 있습니다.

쉽게 말해서 HTML에서 숨김 헤딩을 제공했다고 이해하시면 됩니다.

정리하면, 미니 플레이어와 같이 화면에 제목이 없는 다른 레이아웃이 표시되어 숨김 제목을 주는 것이 탐색에 도움이 된다고 판단되는 경우에는 해당 레이아웃 컨테이너 자체에 대체 텍스트를 삽입할 수 있겠습니다.

[HTML & CSS] 레이아웃 테이블에 관하여 with 회원가입 폼

Webacc NV | 2020-08-31 12:11:12

마크업을 할 때, 테이블은 두 가지 종류로 구분하고 있습니다.

테이블이 만들어진 원래 목적인 자료 전달을 주로 하는 데이터 테이블이 있으며, 레이아웃을 잡기위해 사용하는 레이아웃 테이블로 나눕니다.

데이터 테이블은 th(헤더 셀)과 td(데이터 셀)을 모두 사용하며, Office에서 사용하는 표와 같이 자료를 보기쉽게 정리하여 전달하는 본래 표의 용도입니다. 반면, 레이아웃 테이블은 th를 사용하지 않고, td만을 사용하여, 웹페이지나 특정 영역의 레이아웃을 잡는 용도로 사용하는 테이블을 말합니다. 원래 표를 그리는 목적과는 거리가 멀고, 그다지 시멘틱하지 않다고 볼 수 있습니다.

그럼 레이아웃 테이블은 왜 사용했었나요?

HTML5 이전에는 웹에서는 시멘틱이 그리 중요한 위치에 있지 않았습니다. 오로지 개발자 입장에서 구분하기 위해 HTML의 요소 아이디나 클래스로 div나 데이터 셀에 이름을 부여하는 것이 고작이었습니다. 그 당시에 레이아웃을 만드는 방법에는 div와 css 속성인 float을 사용한 방법과 오늘 주로 얘기할 테이블 셀로 만드는 레이아웃이 있었습니다.

레이아웃 테이블은 div에 float 속성을 사용하는 것 보다 비교적 손쉽게 그리드 형태의 레이아웃을 만들 수 있었습니다. 아직도 서비스 기간이 오래된 페이지나 회원가입 폼 등에서 이 레이아웃 테이블을 아직도 많이 볼 수 있어요.

그런데, 일찍이 스크린리더로 웹을 사용해왔던 시각장애 사용자나 글을 집중하여 읽기 힘든 삭습장애를 겪는 사용자들은 시멘틱한 웹이 필요했습니다. 스크린리더가 요소의 이름을 읽기 때문에였어요.

당연히 그 당시에 테이블로 작성된 레이아웃은 스크린리더에서 "표"라고 읽어줬었고, 사용자는 이상하다는 생각을 하면서도, "원래 그런 것인가 보다"하고 넘겼을 겁니다.  하지만 HTML5이 보편화된 지금, 레이아웃을 전부 테이블로 만드는 것은 바람직하지 않은 행동이라고 볼 수 있겠지요.

레이아웃 테이블의 문제점

(1). 스크린리더 특성

NVDA와 같은 해외 스크린 리더에서는 헤더 셀(th)을 사용하지 않을 경우, 레이아웃 테이블로 간주되어 테이블 정보를 읽지 않아 괜찮습니다. 하지만, 국산 스크린리더인 센스리더의 경우, 헤더 셀이 있건 없건간에 테이블 정보를 모두 읽는 문제점이 있으며, 가상커서로 웹페이지의 요소를 하나 씩 탐색할 때, 테이블 정보와 함께 읽게되는 행과 열에 대한 정보도 읽게됩니다.

이것이 무슨 문제이냐 싶겠지만, 스크린리더에 익숙하지 않거나, 필요한 정보만을 듣고싶어하는 사용자 입장에서 행과 열에 대한 정보는 웹을 탐색하는 시간을 늘릴 뿐인 짐덩어리일 것입니다.

(2). 부적절한 레이아웃 테이블 사용

NVDA에서는 th태그가 없는 태그는 위에서 언급하였듯 데이터 테이블로 인식하지 않으며, 테이블 정보를 읽지 않습니다. 하지만, 디자인 목적으로 인해, th 태그를 사용하는 경우도 종종 있습니다. 대표적으로 회원가입 폼에서 th태그의 굵은 글씨체를 활용하는 것을 말씀드릴 수 있겠네요.

이러한 경우, 스크린리더에서는 바로 데이터 테이블로 인식하여, 테이블에 관한 모든 정보를 읽게 됩니다. NVDA의 경우, 빠른 탐색키를 활용하여, 특정 요소에 진입 처음 진입했을 때, 아래와 같이 컨테이너 정보를 모두 읽는 특성을 가지고 있어서, 매우 긴 정보를 한번에 읽게되는 문제가 생기며, 회원가입 폼의 경우, label과 th 정보를 모두 읽기 때문에, 레이블을 두 번 읽는 문제도 생깁니다.

 

음성출력뷰어 화면, 맨 밑줄에 [2행 3열 목록 항목 수 1개 네이버 방문함 링크]라는 출력된 내용이 쓰여있다.

그럼, 이미 테이블 레이아웃으로 만들어진 페이지는 어떻게 하나요? 다시 만들려면 너무나 많은 노력과 시간이 필요해요.

WAI-ARIA의 role 중에는 요소 유형을 없애는 none 값이 있습니다. 이를 통해 스크린리더 사용자가 웹페이지를 탐색할 때 "표"라고 읽는 것을 방지할 수 있으며, 다른 시멘틱 role과 함께 사용하여, 시멘틱한 웹페이지를 만들 수 있습니다.

<!doctype html>
<html lang="ko">
<head>...생략...</head>
<body>

<table role="none">
    <tr>
        <td role="banner">banner는 HTML5의 header 태그와 동일합니다. 이곳은 배너 영역입니다.</td>
    </tr>
    <tr>
        <td role="complementary">complementary는 HTML5의 aside 태그와 동일합니다. 이곳은 보조정보 영역입니다.</td>
    <td role="main">값 이름과 같이 HTML5의 main태그와 같습니다. 주요 콘텐츠 영역입니다.</td>
    </tr>
    <tr>
        <td role="contentinfo">contentinfo는 HTML5의 footer태그와 같습니다. 콘텐츠정보 영역입니다.</td>
    </tr>
</table>

</body>
</html>

NVDA, 센스리더 할 것 없이 Chrome 기반의 브라우저에서 role="none"을 사용하여 테이블 요소 유형을 제거할 경우, 테이블 자식으로 어떤 셀을 사용하건 간에 마치 div 태그를 쓴 것과 같이 아무런 표 정보도 읽지 않는 것을 경험해 보실 수 있습니다.

다만, 이 방법의 경우, Internet Explorer를 지원하지 않는 문제점이 있다는 점을 주의하셔야 합니다.

pointer-event를 활용하여 ::after와 ::before를 디자인 요소로 활용해보세요.

Webacc NV | 2020-08-27 12:35:53

종종 CSS로 원하는 스타일을 만들기 위해 종종 after나 before 요소를 만들고, 요소 위에 덮는 경우가 있습니다.

특히 이미지 위에 오버레이 효과를 낼 때 자주 사용하는데, 이 방법을 사용합니다. 그런데, Javascript를 사용하여 Click 이밴트를 줄 때, 버그를 방지하기 위해 버블링(자손으로의 이벤트 전파)을 막는 경우가 있습니다.

이런 경우, after나 before 요소에 클릭 이벤트가 걸리지 않습니다. 만약 position:absolute와 z-index로 요소에 오버레이 효과를 줬을 경우, 버튼에 제대로 클릭이 닿지 않을 수 있다는 것이지요 이럴 때는 덮고있는 before나 after 요소에 'pointer-events'를 none 값으로 설정하면 z-index 계층상 해당 요소 밑에 있는 요소도 클릭할 수 있는 상태로 만들 수 있습니다.

 

[스크린리더] NVDA 음성 말하기 끔/켬 사용 관련

Webacc NV | 2020-08-27 09:36:35

PC에서 웹페이지를 테스트할 때 NVDA 스크린 리더를 사용하는 경우 말하기 끔/켬을 토글하는 단축키를 사용하면 편리합니다.

기본 단축키는 nvda key + s이며 nvda 설정 카테고리 중 키보드 섹션에서 nvda 키를 무엇으로 할 것인지 정의할 수 있습니다.

참고로 만약 caps lock 키를 nvda 키로 설정할 경우 대소문자 변경은 caps lock 키를 빠르게 두 번 누르면 됩니다.

해당 키를 누를 때마다 말하기 끔, 비프음으로 말하기, 말하기 켬 세 단계로 토글되는데 NoBeepsSpeechMode NVDA 추가기능을 설치하면 말하기 끔/켬 두 가지로만 사용이 가능합니다.

이 단축키를 잘 활용하면 nvda를 수시로 온/오프 하지 않더라도 간단한 테스트 및 추가 작업이 가능할 것이라 생각됩니다.

[iOS native] 미디어 플레이어 일시정지, 재생은 performMagicTap 펑션 오버라이드 적용 필수

Webacc NV | 2020-08-26 10:45:21

스크린 리더 사용자가 음악 감상을 하고 있다고 가정해 봅시다.

다음 곡으로 넘기거나 슬라이더를 조절하는 등의 기능을 수행하려면 각 요소를 찾아가서 이중탭을 하거나 슬라이더 조절 제스처를 해야 할 것입니다.

그런데 적어도 일시정지, 재생 버튼만큼은 일일이 요소를 찾아가서 이중탭을 하지 않더라도 특정 제스처를 하면 접근성 포커스가 어디에 있던 간에 무조건 그 기능이 실행되게끔 해준다면 어떨까요?

iOS VoiceOver에서는 두 손가락 두 번 탭 제스처를 통해 특정 화면에서 필요한 경우 해당 기능을 매핑할 수 있도록 지원하고 있습니다.

이것이 바로 accessibilityPerformMagicTap 펑션 입니다.

스크린 리더 사용자는 뮤직 플레이어에서는 두 손가락 이중탭을 하면 일시정지와 재생이 될 것이라는 일반적인 생각을 가지고 있습니다.

마치 두 손가락 문지르기를 하면 뒤로 버튼 기능이 동작할 것이라는 추측과 비슷한 것이라고 보면 되겠습니다.

그러나 이 accessibilityPerformMagicTap 펑션을 구현해 놓지 않으면 이 기능은 동작하지 않습니다.

따라서 다음 예시와 같이 뮤직 플레이어, 비디오 플레이어 등에서는 일시정지와 재생 펑션을 매핑해 주면 큰 도움이 됩니다.

override func accessibilityPerformMagicTap() -> Bool {
        let isPlaying = avplayer?.rate == 1
        if isPlaying {
            pause()
            playPauseButton.setImage(#imageLiteral(resourceName: "icPlay"), for: .normal)
            playPauseButton.accessibilityLabel = "재생"
        } else {
            play()
            playPauseButton.setImage(#imageLiteral(resourceName: "icPause"), for: .normal)
            playPauseButton.accessibilityLabel = "일시정지"
        }
        return true
    }

[iOS native] 한국어 네이티브 앱에서 화면 스크롤시 VoiceOver 메시지가 영어로 나온다면

Webacc NV | 2020-08-25 09:21:45

지난번에 HTML lang 속성을 정확하게 사용하지 않으면 모바일 스크린 리더 사용자는 사용자의 환경에 따라 한국어를 영문 음성 엔진으로 읽는 문제를 겪게 된다는 것에 대해 공유한 적이 있습니다.

그런데 iOS 앱을 개발할 때 base 언어가 영어로 되어 있는 상태에서 한국어 언어 로컬라이징 추가 없이 한국어로만 개발을 했을 때도 HTML의 lang 속성을 사용했을 때만큼은 아니지만 스크린 리더 사용자는 앱을 탐색할 때 약간의 어려움이 있습니다.

그것은 사용자의 언어 시스템이 한국어임에도 일부 보이스오버 음성이 영문 메시지로 출력된다는 것인데요.

특히 스크롤 제스처를 할 때 그렇습니다.

예를 들어 UIPageView로 여러 페이지를 구현했다고 가정합시다.

페이지를 전환하면 보이스오버 자체적으로 알림 이벤트 스트링을 언어별로 가지고 있어서 한국어의 경우 '5페이지 중 2페이지'와 같이 음성을 출력합니다.

그런데 사용자의 시스템이 한국어라 하더라도 base 언어가 영어이면 이러한 스트링이 다 영어로 출력되어 'page 2 of 5'와 같이 음성 출력하게 됩니다.

따라서 앱 개발 시에는 한국어는 반드시 한국어 로컬 언어 안에서 개발을 하는 것이 스크린 리더 사용자에게도 도움이 됩니다.

[iOS native] UISlider로 구현된 슬라이더 조절과 스크린 리더 접근성 해결

Webacc NV | 2020-08-24 10:23:42

여러 차례에 걸쳐서 안드로이드의 SeekBar에 대해 언급을 했었습니다.

그런데 안드로이드에 SeekBar가 있다면 iOS에는 UISlider가 있습니다.

이 UISlider 역시 뮤직 플레이어 같은 곳에서 흔히 사용되는 요소이며 VoiceOver에서는 이 슬라이더를 만나면 한 손가락 쓸어 올리기 또는 쓸어 내리기로 값 조절이 가능하게끔 해 줍니다.

그런데 문제는 iOS 역시도 안드로이드와 마찬가지로 슬라이더를 구현을 할 때 반드시 손가락으로 드래그를 했다가 손가락을 뗄 때만 밸류 값이 반영되도록 구현을 많이 합니다.

그 이유를 살펴보니 보조기술을 사용하지 않는 사람들이 예를 들어 미디어 플레이어에서 시간 조절을 하기 위해 드래그를 하면 드래그를 하는 동시에 바로 시간이 조절되는 것을 원하지 않기 때문입니다.

그러나 항상 이 문제는 또다시 접근성에 관점에서는 이슈를 야기합니다.

VoiceOver에서 제공하는 한 손가락 위 또는 아래 쓸기로의 슬라이더 조절은 드래그 동작이 아니기 때문에 밸류 값이 변경되더라도 이에 상응하는 이벤트는 발생하지 않습니다.

이를 해결하는 방법은 아주 간단합니다.

isVoiceOverRunning 펑션을 활용하는 것인데 VoiceOver가 꺼져 있을 때는 기존 방식대로 구현을 하고 VoiceOver가 켜지면 드래그를 하지 않더라도 밸류 값을 보내게끔 구현을 하면 됩니다.

2021 CSUN 접근성 컨퍼런스 온라인 대체 관련

Webacc NV | 2020-08-21 18:36:09

CSUN 컨퍼런스는 매년 3월에 개최되는 국제적인 접근성 컨퍼런스로 캘리포니아 주립대학에서 주최하고 있습니다.

접근성 향상을 위한 여러 노하우 및 소프트웨어 등을 공유하는 수많은 세션들과 보조기술 세미나, 박람회 등으로 구성되는 국제 행사입니다.

그런데 내년에는 코로나 이슈로 인해 본 행사를 온라인으로 대체하여 진행하게 되었습니다.

따라서 여러 여건상 참석하지 못하셨던 분들은 내년에 참여해서 접근성과 관련된 여러 정보를 교류하는 기회가 될 것이라 생각합니다.

이와 관련된 자세한 내용은 Virtual Conference Announcement 내용을 참고하시면 됩니다.

[Android native] 한 화면에 콘텐츠를 중첩하여 화면 전환 시 초점 대응

Webacc NV | 2020-08-20 17:58:16

얼마전에 화면 전환 시 overlay 형태로 중첩하여 전환되는 경우의 화면 전환 알림에 대한 접근성 팁을 공유했습니다.

오늘은 초점에 대한 이야기를 해 보려고 합니다.

MainActivity 레이아웃 파일 안에 A 화면 콘텐츠와 B 화면 콘텐츠가 들어 있다고 가정합시다.

A 화면이 표시될 때는 B 화면은 숨깁니다. 

그런데 A 화면에서 B라는 요소를 눌러 B 화면으로 전환되면 B 요소들이 A 요소 위에 중첩된 형태로 표시될 것입니다.

그러면 화면 위에 화면이 또 있기 때문에 시각적으로는 A 화면은 보이지 않습니다.

따라서 일반적으로는 A 화면은 숨기지 않습니다.

그러나 스크린 리더 입장에서는 어떨까요?

두 화면의 레이아웃이 다 표시될 것입니다. 

화면 상으로만 콘텐츠가 가려졌을 뿐이기 때문입니다.

따라서 이를 해결하려면 B 화면으로 전환될 때 다음 예시와 같이 A 화면 콘텐츠도 숨겨 주어야 합니다.

ConstraintLayout aScreen = findViewById(R.id.main_screen);
AScreen.setVisibility(View.GONE);

그리고 B 화면에서 A 화면으로 돌아올 때는 다음 예시와 같이 B 화면을 숨기는 것 뿐 아니라 A 화면이 다시 표시되도록 해야 합니다.

aScreen.setVisibility(View.VISIBLE);

[Android native] 뮤직 플레이어 재생 슬라이더에 포커스 했을 때 증가되는 퍼센트 읽지 못하게 하기

Webacc NV | 2020-08-18 16:41:53

미디어 플레이어 화면에서 곡을 재생하면 재생 슬라이더의 퍼센트는 계속 증가할 것입니다.

그런데 재생 슬라이더에 포커스 하고 있으면 퍼센트가 증가될 때마다 자동으로 수치를 읽게 되는데 이는 음악 감상에 상당히 방해가 됩니다.

물론 가장 좋은 것은 그 슬라이더에 포커스를 하지 않고 있으면 되긴 하지만 곡을 자주 넘겨야 하는 경우 일일이 포커스를 다른 곳으로 이동시켜 놓는 것도 번거로운 일입니다.

따라서 우리는 특정 접근성 이벤트를 블록시킴으로써 이 문제를 해결할 수 있습니다.

이 때 활용할 수 있는 메서드가 바로 onInitializeAccessibilityEvent 입니다.

지난 번 해당 메서드에 대해 몇 번 살핀 적이 있는데 말 그대로 기본적으로 적용된 접근성 이벤트를 초기화하거나 변경할 때 사용합니다.

따라서 우리는 해당 메서드를 가지고 슬라이드 퍼센트를 자동으로 읽는 것을 초기화 하면 됩니다.

다만 사용자가 슬라이드를 변경할 때에는 퍼센트를 읽도록 해야 하므로 progressChangeListener에서 제공하고 있는 isFromUser 변수가 트루일 때에는 퍼센트를 종전처럼 읽도록 하면 됩니다.

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

        mSeekBar.setAccessibilityDelegate(new View.AccessibilityDelegate() {
            @Override
            public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
                if (isFromUser || event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
                    super.onInitializeAccessibilityEvent(host, event);
                }
            }
        });
    }

 

[Javascript & LiveRegion] 웹에서 모바일 앱처럼 알림 제공하기

Webacc NV | 2020-08-14 10:59:30

웹사이트를 개발하다보면 스크린리더 사용자가 이해하기 어려운 상황, 화면, 컴포넌트에 대해 무언가를 설명하거나 안내가 필요할때가 있습니다.  모바일에서는 이와 비슷한 경우를 대비하기 위한 메소드들이 존재합니다.  Android에서는 View.announceForAccessibility(message), iOS에서는  UIAccessibility.post(notification:.announcement,message) 가 있습니다.

웹에서는 이와 비슷한 경우에 사용할 수 있는 수단으로 aria-live와 role="alert"이 있습니다.

role="alert" 은 display나 visibility 속성으로 숨겨져있던 요소가 표시되거나, role="alert"속성을 가진 요소가 DOM 에 추가되었을 때 스크린리더 사용자에게 "알림"이라는 고정된 텍스트와 함꼐 메시지를 제공합니다. 읽고있던 텍스트가 있었다면, 무시하고 알림부터 읽습니다.

aria-live="assertve / polite" 는 컨테이너 안의 텍스트가 변경되거나, alert처럼 자식 요소가 추가되거나 숨겨져있던 자식요소가 나타나면 업데이트된 요소를 읽습니다. 

assertive는 role="alert"과 같이 무시하고 변경된 내용을 알립니다. ajax나 React와 같은 Virtual-DOM 기술을 사용허는 SPA 페이지에서 비동기 프로그래밍을 통해 페이지 로드, 화면 타이틀 스크린리더로 제공하기 좋으며, polite는 스크린리더가 먼저 읽던 내용이 끝나기를 기다렸다가 알립니다. 영역에 대한 알림이나, 힌트를 제공할 때 좋습니다.

 

aria-relevant = "option1, option2 ...." 는 live region에서 변경을 감지할 유형들을 지정합니다. 여러 옵션이 있으며, 띄어쓰기로 구분하여, 여러 값을 적용할 수 있습니다. 기본값은 이 속성값을 주지않은 live region은 aria-live"additions text"가 기본 값으로 설정되있으며, removal, 즉 텍스트나 요소의 삭제 또는 숨김을 감지하는 옵션값이 있습니다. 

assertive는 어떤 상황이든 바로 읽으므로 자세히 다루지 않으며, 이번 팁에서는 aria-live="polite"를 사용한 요소 힌트를 만들어 보도록 하겠습니다.

HTML 마크업:

<main id="favorite_naverPages">
    <!-- 버튼들을 스크립트로 불러오기 위한 컨테이너에 아이디를 줍니다. -->
    <h1>사이트 이동</h1>
    <!--type="button"은 굳이 필요하지않지만 넣어줬습니다.
    중요한 것은 live지만 어찌되었건 페이지 이동 버튼이니
    value값을 홈페이지명으로 합니다.-->
    <button type="button" value="http://www.naver.com">NAVER 홈</button>
    <button type="button" value="http://cafe.naver.com">NAVER 카페 홈</button>
    <button type="button" value="http://blog.naver.com">NAVER 블로그 홈</button>
    <button type="button" value="http://news.naver.com">NAVER 뉴스 홈</button>
</main>
<!--aria-live 콘테이너 두 개 입니다. 급보처럼 읽던 것을 중단하고 알리는 assertive 라이브 리전과
읽던 것을 마치면 새로운 정보를 알리는 polite 라이브 리전 컨테이너입니다.
안에 있는 p태그가 숨겨젔다 나타나는 것으로 live 정보를 스크린리더 사용자에게 전달할 겁니다-->
<div id="assertive_announcer" aria-live="assertive">
    <p class="msgbox visible hide" id="assertive_message"></p>
</div>
<div id="polite_announcer" aria-live="polite">
    <p class="msgbox visible hide" id="polite_message"></p>
</div>

CSS 스타일링:

:root{
    --main-background-color:#efefef;
    --main-foreground-color:#1f1f1f;
    --announce-background-color:#7fafff;
    --announce-foreground-color:var(--main-foreground-color);
}
*{box-sizing:border-box; margin:0; padding:0;}
html,body{ position:relative; overflow:hidden; width:100%; height:100%;}
body{color:var(--main-foreground-color); background-color:var(--main-background-color);}

.msgbox.srOnly{/*.srOnly라는 클래스가 있으면 보이지 않는 live 알림을 제공합니다.*/
    border: 0; clip: rect(0 0 0 0);
    height: 1px; width: 1px;        
    margin: -1px; padding: 0;
    overflow: hidden; position: absolute;
}

.msgbox.visible{/*visible 클래스가 있으면 보이는 디자인을 제공합니다.*/
    position:absolute; padding:2rem; bottom:0; left:0;
    background-color:var(--announce-background-color);
    color:var(--announce-foreground-color); margin:1%;
    font-size:1.5em; max-width:40%; word-break:keep-all; word-wrap: break-word;
    border:solid 1px; border-radius:0.3rem;
}

/*classList.replace로 표시/숨김 처리할 때 쓸 class입니다. */
.show {display:block;}
.hide {display:none;}

Javascript:

/* (1) 요소 변수와 아래에서 쓸 timeout 제거용 변수 */
const favoritePages = document.querySelectorAll('#favorite_naverPages>button')
let hideAnnounce; let showAnnounce;

/* (2) 이벤트 핸들러 등록 */
for (let i = 0; i < favoritePages.length; i++) {
    const element = favoritePages[i];        
    element.addEventListener('click',function(){ location.href = this.value; })
    element.addEventListener('focusin',announceHintHandler)
    element.addEventListener('focusout',resetAnnounceHintHandler)
}

/*(3) 포커스 인/아웃 핸들러*/

//(3-1) 초점을 받으면 맨 아래 정의된 announcement 함수를 실행합니다.
function announceHintHandler(e){
    announcement('페이지로 이동하려면 Enter 또는 Space 키를 누르십시오.','polite')/*(3) 요소에
    초점이 가면, 요소명을 읽은 다음 안내메시지를 읽어야하기 때문에 polite를 사용합니다.
    */
}

//(3-3) 초점을 잃으면 announcement의 setTimeout들을 초기화시킵니다.
function resetAnnounceHintHandler(e){
    clearTimeout(showAnnounce);
    clearTimeout(hideAnnounce);
}

/*(4) aria-live로 스크린리더에게 알림을 줄 함수를 만듭니다.*/

function announcement(text,type,mils=4000){/*(4-1) type은 aria-live의 두 가지 속성값을 넣습니다. 
    mils는 기본값을 넣어 입력하지 않아도 기본값으로 작동하도록 합시다.*/

    type = (type === 'polite' ? 'polite' : 'assertive');/*(4-2) 삼항 연산자를 사용하여
    polite가 아니면 type의 기본값을 assertive로 설정합니다.*/
    
    const announcementElement = 'polite' ? document.getElementById('polite_announcer') 
    : document.getElementById('assertive_announcer'); /*(4-3) type에 따라 위에
    HTML에서 마크업한 요소를 삼항연산자로 announcementElement라는 변수에 담습니다.*/
    
    const msgbox = announcementElement.querySelector('.msgbox')/*(4-4) 메시지가 작성될 자식요소를
    announcementElement로부터 CSS Selector로 검색합니다. */
    
    showAnnounce = setTimeout(function(){/*(4-5) live 텍스트보다 요소 텍스트와 유형을 먼저
    읽게 하기 위해 메시지 출력에 약간의 딜레이를 줍니다.*/
        msgbox.classList.replace('hide','show')
        msgbox.innerHTML = text;
    },50)

    hideAnnounce = setTimeout(function(){/*(4-6) 눈에 보이는 안내 상자가 사라질 시간을
    지정합니다. 보이지 않는 알림은 mils를 짧게 주고, css로 보이지 않게끔 수정하면 됩니다. */
        msgbox.classList.replace('show','hide');
    },mils);
}

저는 눈에 보이는 알림을 만들었습니다. HTML에서 마크업된 버튼을 누르면, 네이버의 특정 사이트로 이동함을 알 수 있게끔, 스크린리더 힌트를 제공하는 예제입니다.

주석으로 내용을 거의다 적어놓았지만 핵심만 정리를 해보겠습니다.

focusout으로 setTimeout을 왜 초기화해야 하는가?

제가 원하는 버튼에 초점이 갔을때, 항상 일정하게 메시지 박스가 시각적으로 표시되고, 스크린리더가 읽게 하기 위함입니다. 이 작업을 하지 않는다면 메시지 박스가 매번 정해진 시간초동안 보여지지 않습니다. 만약, 오로지 스크린리더 사용자만을 위한 알림을 제공할 경우에는 이 작업이 필요없으나, Android의 토스트와 같이 음성과 시각적 안내를 모두 지원할 경우에는 이 작업이 필요합니다.

polite는 먼저 읽던 게 있다면 기다렸다 읽어주는데, 왜 setTimeout으로 딜레이를 줍니까?

live 메시지는 focus된 요소를 읽는 것보다 반응이 빠릅니다. 저는 키보드 초점이 버튼에 갔을 때, VoiceOver나 Talkback의 힌트 메시지처럼 읽어주는 알림을 원하는데, (4-5)번에서 0.05초를 주지 않았다면 힌트를 먼저 읽고, 초점이 간 요소를 읽게됩니다.

그러나, 위 코드와 같이 polite 영역의 텍스트를 늦게 업데이트하게끔 설정하게 되면, 마치 VoiceOver나 Talkback에서 버튼 요소에 초점이 갔을 때 안내되는 것 처럼 읽게됩니다.

[Android native] accessibilityPaneTitle 응용하여 구현해보기

Webacc NV | 2020-08-13 14:13:18

한 activity에 여러 fragment를 만들고 fragment로 화면 전환 시 accessibilityPaneTitle 속성 사용을 통해 접근성 개선이 가능하다는 것에 대해 공유했습니다.

그런데 접근성 테스트를 하다보면 여러 화면이 하나의 fragment에서 오버레이 형태로 전환되는 경우도 종종 보곤 합니다.

즉 각 화면이 activity, 혹은 fragment로 나뉘어져 있는 것이 아니라 하나의 activity 안에 중첩된 화면을 만들고 상황에 따라 표시/숨겨지도록 하는 경우를 말합니다.

이런 경우에는 화면 전환 시 접근성 구현을 어떻게 해야 할까요?

이 때도 accessibilityPaneTitle 속성을 활용할 수 있습니다.

사용자가 다른 화면으로 전환하는 버튼을 누르면(OnClick) 전환된 화면에 표시되는 한 view에 setAccessibilityPaneTitle을 추가하면 됩니다.

예시:

public void onBanana(View view) {
    //implement
    final TextView text = (TextView)findViewById(R.id.textView2);
    Hide();
    text.setText("바나나입니다");
    text.setAccessibilityPaneTitle("banana screen");
}

[Android native] 한 activity에 여러 fragment로 구성된 화면

Webacc NV | 2020-08-12 18:37:05

아래 내용은 예전에 널리 블로그 아티클로는 한번 포스팅한 적이 있지만 요약하여 해당 팁 코너에 다시한번 공유합니다.

한 activity 안에 여러 fragment들이 있어서 화면 전환이 fragment 단위로 이루어 지는 경우에는 추가적인 접근성 구현을 하지 않으면 TalkBack에서 화면이 전환되는 것을 읽어주지 못하고 이는 스크린 리더 사용자가 전체적인 화면이 전환되었다는 것을 알지 못하는 불편함으로 이어지게 됩니다.

따라서 각 fragment xml 파일의 LinearLayout과 같은 루트에 다음과 같이 마크업하여 스크린 리더 사용자가 화면 제목을 바로바로 인지할 수 있도록 도움을 줄 수 있습니다.

android:accessibilityPaneTitle = "검색 화면"

그러나 같은 레이아웃을 사용하면서 창 제목이 동적으로 변경되는 경우에는 코틀린이나 자바에서 이를 처리해 주어야 합니다.

단 탭레이아웃, ViewPager class를 활용한 멀티 페이지뷰의 각 fragment에는 accessibilityPaneTitle 속성을 추가하지 않아도 됩니다. 

아래는 화면 전환이 일어났을 때 페인 타이틀을 처리하는 예제 샘플 앱입니다.

코드를 복사하여 실제 테스트도 가능합니다.

참고할 것은 아래 예제에서는 과일 상세 화면으로 들어갔을 때 기존 레이아웃이 흐리게 처리되는 방식을 사용하였기 때문에 importantForAccessibility api를 활용해서 기존 화면은 스크린 리더가 접근하지 못하도록 처리하였다는 것입니다.

 

// activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

 

// fragment_fruit_list.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    android:gravity="center"
    android:importantForAccessibility="yes">

        <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Fruit Demo"
        android:textSize="24sp"
        android:layout_gravity="center_horizontal"
        android:layout_marginBottom="16dp"/>

    <TextView
        android:id="@+id/apple"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Apple"
        android:textSize="18sp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="8dp"/>

    <TextView
        android:id="@+id/strawberry"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Strawberry"
        android:textSize="18sp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="8dp"/>

    <!-- ... Other fruits similarly ... -->

</LinearLayout>

 

// fragment_fruit_detail.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp">

    <Button
        android:id="@+id/backButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Back"
        android:layout_alignParentTop="true"
        android:layout_alignParentStart="true" />

    <TextView
        android:id="@+id/fruit_description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Fruit description will be here"
        android:textSize="18sp"
        android:layout_centerInParent="true" />

</RelativeLayout>

 

// MainActivity.kt


import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.accessibility.AccessibilityEvent
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment

class MainActivity : AppCompatActivity() {

    private var lastClickedViewId: Int? = null  // Store the last clicked view's ID

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setTitle("Fruit Demo")

        if (savedInstanceState == null) {
            supportFragmentManager.beginTransaction()
                .add(R.id.fragment_container, FruitListFragment())
                .commit()
        }
    }

    fun displayFruitDetails(fruitName: String, clickedViewId: Int) {
        lastClickedViewId = clickedViewId
        val fragment = FruitDetailsFragment.newInstance(fruitName)
        supportFragmentManager.beginTransaction()
            .replace(R.id.fragment_container, fragment)
            .addToBackStack(fruitName)
            .commit()
    }

    override fun onBackPressed() {
        super.onBackPressed()
        Handler(Looper.getMainLooper()).postDelayed({
            findViewById<View>(R.id.fragment_container)?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
            lastClickedViewId?.let {
                findViewById<View>(it)?.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
            }
        }, 500)
    }

    class FruitListFragment : Fragment(R.layout.fragment_fruit_list) {
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)

            view.findViewById<TextView>(R.id.apple).setOnClickListener {
                (activity as MainActivity).displayFruitDetails("Apple", it.id)
            }

            view.findViewById<TextView>(R.id.strawberry).setOnClickListener {
                (activity as MainActivity).displayFruitDetails("Strawberry", it.id)
            }
            // ... Similar onClickListeners for the other fruits.
        }
    }

    class FruitDetailsFragment : Fragment(R.layout.fragment_fruit_details) {
        companion object {
            private const val FRUIT_NAME_KEY = "fruit_name_key"

            fun newInstance(fruitName: String): FruitDetailsFragment {
                val args = Bundle().apply {
                    putString(FRUIT_NAME_KEY, fruitName)
                }
                return FruitDetailsFragment().apply {
                    arguments = args
                }
            }
        }

        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            val fruitName = arguments?.getString(FRUIT_NAME_KEY) ?: return
            val textView = view.findViewById<TextView>(R.id.fruit_description)
            val description = when (fruitName) {
                "Apple" -> "Apples are rich in dietary fiber, antioxidants, and flavonoids."
                "Strawberry" -> "Strawberries are a great source of vitamin C, manganese, and antioxidants."
                // ... Add cases for other fruits
                else -> "This fruit is delicious and nutritious!"
            }
            textView.text = "$fruitName: $description"

            view.accessibilityPaneTitle = fruitName

            // Hide the fruit list from accessibility tools
            view.findViewById<View>(R.id.fragment_container)?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS

            view.findViewById<Button>(R.id.backButton).setOnClickListener {
                activity?.onBackPressed()
            }
        }
    }
}

 

[iOS native] 같은 화면 상에서 다른 요소로 초점 보낼 때 참고 사항

Webacc NV | 2020-08-11 19:11:40

같은 화면 상에서 modal이 표시/닫히거나 화면 상의 레이아웃이 변경되었을 때 접근성을 고려하기 위해 초점을 해당 view로 보내주는 경우가 많습니다.

이 때 다음의 두 메소드 중 하나를 사용하게 됩니다.

UIAccessibility.post(notification: .screenChanged, argument: 초점 보내고자 하는 요소)

혹은

UIAccessibility.post(notification: .layoutChanged, argument: 초점을 보내고자 하는 요소)

 

그런데 이미 view가 표시된 상태에서 해당 메소드를 사용하면 별 문제가 없지만 숨겨졌던 view가 다시 표시되는 경우에는 시간차로 인해 초점이 제대로 이동하지 못하는 경우가 종종 있습니다.

이런 경우에는 약 0.7초 정도 딜레이를 주면 대부분의 문제는 해결이 됩니다.

예시:

DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {

UIAccessibility.post(notification: .layoutChanged, argument: object)

}

[Android native] activity에 setTitle="" 추가와 관련하여

Webacc NV | 2020-08-10 20:46:23

안드로이드에서는 각 화면을 activity로 구현하면 activity가 실행되면서 TalkBack이 activity label을 읽는다고 말씀드렸습니다.

그런데 만약 특정 activity에서 onCreate 메소드 안에 setTitle="" 라고 코드를 작성하면 해당 activity가 실행되더라도 TalkBack은 화면 제목을 말하지 않을 뿐더러 화면 변경 사운드도 출력하지 않게 됩니다.

거의 대부분 해당 메소드는 사용할 필요가 없지만 예외적으로 음성 검색 화면에서는 이 방법을 사용할 수 있습니다.

왜냐하면 음성 검색이 실행되면서 마이크가 바로 켜 지는 경우에는 TalkBack이 화면의 제목을 읽는 것은 오히려 방해가 되기 때문입니다.

다음 팁에서는 음성 검색에서의 초점 처리에 대해 다루겠습니다.

[Android native] performAccessibilityAction 간단히 살펴보기

Webacc NV | 2020-08-06 17:47:21

요 며칠 SeekBar의 접근성에 대해 다루면서 performAccessibilityAction 메소드를 언급했습니다.

오늘은 이 메소드의 역할에 대해 알아보려고 합니다.

웹개발 관점에서 접근하면 설명은 간단해집니다.

우리는 접근성을 고려할때 마우스 사용자 뿐만 아니라 키보드 사용자도 고려해야 합니다.

버튼, 링크, 라디오버튼, 체크박스 등을 구현할 때 HTML의 네이티브 컨트롤을 사용하면 키보드 접근성을 따로 구현할 필요가 없습니다.

이는 각 태그들을 적용하면 브라우저에서 자체적으로 키보드 접근성을 지원하고 있기 때문입니다.

그러나 버튼을 div 요소로 구현을 한다고 가정해 봅시다.

그러면 스타일 및 기능은 버튼이지만 브라우저 입장에서는 버튼이 아닌 div일뿐입니다.

따라서 접근성 구현을 위해 스크립트로 버튼에 대한 키보드 액션을 반드시 추가해 주어야 합니다.

마찬가지로 안드로이드에서도 TalkBack을 사용할 때 네이티브 컨트롤을 사용하면 각 컨트롤마다 그 컨트롤들을 사용자가 조절할 수 있는 자체 액션들이 추가됩니다. 

그러나 SeekBar에서 다룬 바와 같이 표준 컨트롤을 사용하지 못하거나 기본적인 접근성 API가 지원하는 부분이 아쉬운 경우 접근성 액션에 따른 추가 이벤트를 구현할 수 있는데 이것을 performAccessibilityAction이라고 합니다.

여기서의 action은 사용자가 TalkBack을 통해서 수행하는 모든 제스처들입니다. 

예를 들어 SeekBar 컨트롤에서 사용자가 볼륨키로 값을 조절하면 TalkBack에서 scroll forward 및 backward 액션을 실행하도록 되어 있습니다.

따라서 볼륨 키를 조절할 때 값이 변경되지 않는다면 이 scroll forward, backward에 대한 acton 이벤트를 구현해 주어야 하는 것입니다.

[HTML Native & Screen Reader] 제목 태그에 다른 태그를 넣으면 생기는 일

Webacc NV | 2020-08-05 15:30:31

웹 페이지 중에는 제목에 많은 정보를 담고자 span이나 em 등의 태그를 넣는 사례를 자주 볼 수 있습니다.
제목 태그에 자식 태그를 넣는 것이 금지된 것은 아니나, 자식 태그가 있는 헤딩 요소와 없는 요소를 스크린리더로 탐색할 때는 매우 다르게 인식하게 됩니다.

<h3>OO 검색 결과 <em>59</em></h3>

다음의 코드는 시각적으로 아무 문제 없이 한 줄로 표시될 것입니다. 하지만 스크린리더 사용자가 가상커서를 켠 상태로
위/또는 아래 화살표 키로 웹을 볼 때, "검색결과, 헤딩 레벨3"과 "59, 헤딩 레벨3"이 따로 읽힐 것입니다.

이는 스크린리더의 특성중 하나로, span과 같이 네이티브 HTML 태그를 넣었을 때, 마치 헤딩이 두 개인 것처럼 읽히는 현상을 보입니다.
웹을 스크린리더로 오랫동안 사용해왔던 사람은 이에 대해 금방 인지할 것이나, 웹을 사용한지 얼마 안된 초심자의 경우에는
한 개의 헤딩 태그임에도 마치 두 개의 헤딩이 제공된 것처럼 인지할 수 밖에 없습니다.

이를 해결하기 위해서는 자식 요소로 사용될 태그에 role="none" 속성을 삽힙해야 합니다.
우선 em이나 strong은 시멘틱 태그로, role="none"으로 숨기는 것 자체가 그리 바람직하지 아닙니다.
하지만 role="none"으로 숨기지 않은 경우에는 스크린리더와 브라우저 환경에 따라 버그가 발생할 수도 있습니다.
따라서, 두 줄로 읽히는 문제 이전에 버그를 방지하기 위해서는 이와같이 role="none"을 반드시 적용해 주어야 합니다.

[Android native] SeekBar 구현 시 stopTrackingTouch 메소드와 TalkBck 관련

Webacc NV | 2020-08-05 11:15:00

스크린 리더 사용자가 SeekBar 컨트롤에서 볼륨키를 사용하여 값을 조절해도 실제 반영이 안 되는 경우를 보곤 합니다.

이것은 어떤 이유 때문일까요?

SeekBar 구현시에는 일반적으로 3개의 메소드를 오버라이드 합니다.

1. onProgressChanged

2. onStartTrackingTouch

3. onStopTrackingTouch

위의 3개의 메소드를 활용해서 사용자가 값 조절을 시도하면 이에 대한 액션 이벤트를 구현하게 됩니다.

보조기술을 사용하지 않는 사용자는 SeekBar를 터치한 상태로 값을 조절합니다.

그러나 보조기술을 사용하는 경우에는 보조기술에서 지원해주는 방법을 통해서도 값을 조절합니다.

그런데 문제는 사용자가 값을 터치한 상태로 조절할 경우에만, 즉 onStart 혹은 onStop 메소드(터치 시작 혹은 끝 동작)이 실행되는 경우에 이벤트가 발생하도록 구현하는 경우가 많다는 것입니다.

이렇게 구현하면 보조기술을 사용하여 값을 조절하는 것은 터치 동작이 아니므로 값 반영이 안 됩니다.

따라서 특별한 경우가 아니라면 터치 동작이 실행되거나 종료될 때가 아닌, 값이 변경될 때 이벤트가 발생할 수 있도록 구현이 필요합니다.

그럼에도 불구하고 반드시 터치 동작이 실행, 혹은 종료될 때 이벤트를 발생시켜야 한다면 이 때도 performAccessibilityAction 메소드 안에서 이를 해결할 수는 있습니다.

[웹브라우저] 최근 변경된 크롬의 초점 표시 변경 관련

Webacc NV | 2020-08-04 10:43:55

웹페이지를 탐색할 때 사용자의 상황에 따라 마우스 뿐만 아니라 키보드 탭 혹은 스위치 디바이스를 사용하여 페이지를 탐색하는 경우도 많습니다.

따라서 링크, 버튼과 같이 상호작용이 가능한 요소에는 반드시 키보드로 접근하더라도 초점이 시각적으로 표시되어야 하는 것은 접근성의 기본 중의 기본입니다.

예전 크롬 버전까지는 기본 초점을 연한 파란색으로 표시를 했는데 이때 비슷한 계열의 배경색을 사용하는 경우에는 사용자가 초점을 찾기 어렵다는 문제가 있었습니다.

이를 해결하기 위해 최신 크롬 버전부터는 기본 초점이 두 겹의 윤곽선으로 변경되었습니다. 안쪽 윤곽선은 검정색이며, 바깥 윤곽선은 흰색으로, 가장 많이 사용하는 검정색 배경을 사용하는 웹페이지와 흰색 웹페이지에서 문제 없이 초점을 확인할 수 있게 되었습니다.

웹 접근성 향상을 위한 기획의도 분명히 하기

에어류 | 2020-08-03 18:31:08

안녕하세요! 에어류입니다. 

오늘은 웹 접근성을 적용할 대상에 대해 정확하게 기획자가 의도에 맞추어 콘텐츠를 제공해주는 지침 외의 이야기를 소개할까합니다. 

 

우선 아래 그림을 한번 보시겠습니다.

제8회 삼성전기 인사이트 엣지 논문대상 공모 포스터

위에 이미지에 대한 대체텍스트를 제공하려고 할 때 갑자기 헷갈리실 수 있습니다.

'어? 안에 내용을 다 써야 하나?"

 

대체텍스트를 제공할 때 생략하다가 각종 평가에서 쓴맛을 보신분들은 되도록 보여지는 텍스트에 대해서 다 기록하려고 하실 것 같습니다.

결론부터 말씀드리면 해당 이미지의 대체텍스트는 "제8회 삼성전기 inside Edge 논문대상 공모 포스터"로 제공하시면 됩니다. 

물론 추가적인 부연설명이 있으시다면 title로 제공하실 수 있습니다. 

 

그런데 이 때 몇가지 글씨가 눈에 보이니 해당 텍스트가 정보를 주고 있다면 대체텍스트를 제공해야하지 않느냐 하는 논란이 있을 수 있습니다. 

사실 포스터의 이미지 크기가 애매하게 자리를 차지하고 있고, 보이는 텍스트도 있고, 보이지 않는 텍스트도 있습니다. 

그래서 기획자의 입장에서 웹 접근성 향상과 함께 여러가지 혼란을 막기 위해서는 이미지 크기를 정말 썸네일처럼 활용하셔서 아예 작게 하시거나 모든 포스터 내용의 정보를 보여주고자 한다면 더 크게 보실 수 있게 하시고 해당 대체텍스트를 제공해주시는 것이 좋습니다. 

기획자는 콘텐츠를 제공할 때 그 콘텐츠가 단순하게 포스터의 장식적인 이미지로 제공할 것인지 구체적인 내용을 보여주기 위해 제공할 것인지를 먼저 판단하고 그 크기에 맞게 제공하실 때 접근성을 적용하고자 하는 그 취지를 쉽게 이해할 수 있어 웹 접근성 향상으로 이어질 수 있습니다. 

 

색상으로 콘텐츠간의 의미를 주는 것처럼 보이나 의도가 분명치 않은 녹색경영추진 연혁 콘텐츠(연도별로 옅은 회색, 회색, 짖은회색, 녹색으로 그려진 것 같으나 콘텐츠 세부 내용에 색상이 통일되지 않아 결국 아무 색상적인 의미가 없는 연역 연대기)

 

 이 녹색경영 추진 연혁은 자세히 살펴보지 않으면 마치 연도별로 옅은 회색에서 회색, 짙은 회색에서 녹색으로 녹색경영이 강화되고 있는 것처럼 보입니다. 만약 그런 의도로 이미지가 제작되었다면 색상으로만 녹색경영이 강화되는 정보를 추가로 주고 있어 웹 접근성 준수 기준에 부합하는지 고민을 해야할 이미지가 될 수 있습니다. 그러나 자세히 보면 녹색의 표시가 연혁 전반에 있고, 해당 정보를 제공하는 텍스트가 모두 녹색으로 표시되어 색상이 주는 정보는 없습니다. 그럼에도 불구하고 평가오류를 일으킬만한 색상 사용이 있습니다. 

기획자 또는 디자이너가 부분 강조를 위해 녹색을 사용하고 있지만 의미 부여가 혼란스럽네요. 

콘텐츠의 의도를 분명히 하기 위해서 색상의 사용에도 조심스럽게 접근하면 더욱 좋겠습니다. 

 

 

[Android native] SeekBar 클래스에 AccessibilityManager.interrupt() 메소드 적용 시 주의사항

Webacc NV | 2020-08-03 16:35:27

며칠 전에 SeekBar를 스크린 리더 사용자가 조금 더 효율적으로 사용할 수 있는 접근성 팁을 공유했습니다.

오늘은 interrupt 메소드 적용 시 주의해야 할 사항에 대해 공유하려고 합니다.

지난 글에서 언급했듯이 progressBar가 증가 혹은 감소하는 이벤트가 발생할 때 interrupt 메소드를 사용하면 스크린 리더 사용자가 빠르게 퍼센트를 조절할 때 좀 더 자연스러운 조작이 가능합니다.

그런데 조금 더 곰곰이 생각해 보면 한 가지 의문이 생깁니다.

SeekBar는 퍼센트를 사용자가 직접 조절하는 경우도 있지만 음악 재생과 같이 자동으로 퍼센트가 증가하는 이벤트가 발생하는 SeekBar도 있습니다.

이런 요소에 interrupt를 적용하면 어떻게 될까요?

스크린 리더 사용자가 SeekBar가 아닌 다른 요소를 탐색하다가도 퍼센트가 변경되면 읽던 것을 interrupt 시켜 오히려 곤란한 상황이 발생합니다.

그럼 이것에 대한 해결 방법은 무엇일까요?

바로 performAccessibilityAction 메소드를 활용하는 것입니다.

정리하면 재생과 같이 자동으로 SeekBar 퍼센트가 변경되는 요소에는 사용자가 퍼센트를 조절하는 경우에만 interrupt 이벤트가 실행되게끔 해야 합니다.

기회가 될 때 performAccessibilityAction에 대해 다루어 보겠습니다.

[HTML] 버튼 텍스트 레이블 변경과 aria-live에 관하여

Webacc NV | 2020-07-31 13:38:16

얼마전에 안드로이드 접근성 이벤트에 대해 기술하면서 사용자의 액션에 의한 텍스트 변경에 대해 살펴본 적이 있습니다.

웹페이지에서도 스크린 리더 사용자가 키보드를 이용하여 버튼을 누르면 버튼의 텍스트가 변경되는 경우들을 종종 보곤 합니다.

예를 들어 뮤직 플레이어에서 반복 컨트롤 버튼이 있다고 가정해 봅니다.

'반복 해제됨'이라는 버튼 텍스트가 있다면 그 버튼을 누를 때마다 '한 곡 반복 설정됨', '전체 반복 설정됨' 등으로 레이블이 변경될 것입니다.

기본적으로 스크린 리더에서는 버튼 요소의 속성 값(aria-label, value, aria-expanded 등)이 변경되면 특별히 접근성 구현을 하지 않더라도 변경되는 값이나 텍스트 정보를 읽어주게 됩니다.

그러나 버튼 요소 자체의 텍스트가 변경되는 경우에는 변경되는 텍스트를 사용자에게 바로바로 읽어주지 못합니다.

<button id="test">반복 해제됨</button>

따라서 키보드 조작 등에 의해 버튼의 텍스트 정보가 변경되는 경우에는 aria-live 속성을 추가하여 변경되는 텍스트 정보를 바로바로 들을 수 있도록 접근성에 대한 고려가 필요합니다.

그렇지 않으면 버튼을 눌러도 스크린 리더 피드백이 없으므로 버튼의 텍스트가 변경되었는지조차 알기 어렵습니다.

물론 해당 구현을 하는 경우 스크린 리더와 브라우저의 특성에 따라 약간의 버그들이 있습니다.

예를 들면 NVDA에서 크롬 브라우저로 테스트 했을 때 변경되는 aria-live 텍스트 값을 두 번식 읽는 경우가 발생하였으며

페이지 로드 후 변경되는 텍스트 버튼에서 엔터를 눌러 aria-live 속성이 처음 추가되는 시점에는 변경된 텍스트 정보를 읽지 못하는 버그도 있습니다.

그럼에도 불구하고 변경되는 텍스트를 읽어주도록 구현하는 것은 매우 중요하므로 해당 부분을 고려해 보시면 좋겠습니다.

단 aria-live 속성을 사용하지 않으려면 위에서 언급한 aria-label을 사용할 수 있으며 aria-label="전체 반복 설정됨" 등과 같이 마크업 할 수 있겠습니다.

[Android native] SeekBar와 AccessibilityManager class의 interrupt 메소드

Webacc NV | 2020-07-30 12:55:42

앞으로 두 번에 걸쳐 SeekBar에 대한 팁을 공유하려고 합니다.

SeekBar는 접근성에 문제가 없는 이상 TalkBack 사용자는 볼륨키를 사용해서 밸류 값을 조절하는 경우가 많습니다.

그런데 볼륨키로 값을 아주 빠르게 조절하게 되면 볼륨 키를 누를 때마다 기존에 읽던 내용을 멈추고 새로 갱신된 값을 읽어주어야 하는데 기존에 읽던 밸류 및 레이블 정보를 다 읽고 갱신된 정보를 읽습니다.

따라서 갱신된 정보를 듣기 위해 스크린 리더 사용자는 어느 정도 기다려야 하는 불편함이 발생합니다.

게다가 SeekBar에 적용된 대체 텍스트 혹은 labelFor 정보가 긴 경우에는 이러한 불편함은 더 크게 느껴질 것입니다.

이를 해결하기 위해 우리가 사용할 수 있는 메소드가 interrupt 입니다.

interrupt 메소드를 적용하면 이벤트가 연속적으로 발생할 때 접근성 서비스가 기존에 읽던 내용을 말 그대로 가로채고 새로 갱신된 내용을 읽도록 하는 것입니다.

따라서 이것을 다음과 같이 적용할 수 있겠습니다.

public void onProgressChanged(SeekBar seekBar, int progresValue, boolean fromUser) {
final AccessibilityManager accessibilityManager = (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
    accessibilityManager.interrupt();
    progress = progresValue;
}

HTML] aria-live, display:none, 그리고 display:block 관련

Webacc NV | 2020-07-29 16:15:41

웹페이지 환경에 따라 사용자의 키보드 엔터와 같은 액션이 있을 경우 페이지가 새로고침되지 않고 display:none으로 숨겨진 '전송 완료'와 같은 텍스트가 display:block 되면서 화면에 표시되는 경우가 있습니다. 

이때 접근성을 구현하기 위해서 aria-live 속성을 적용하게 됩니다.

물론 role="alert" 속성을 사용해도 되지만 센스리더 사용자의 환경까지 고려한다면 aria-live 속성이 더 범용적입니다.

그렇지 않으면 스크린 리더 사용자가 화면에 나타난 텍스트가 있는지 알지 못하기 때문입니다.

그런데 이때 유의해야 할 것은 반드시 aria-live는 display:none에서 block이 되는 요소 상위에 있어야 한다는 것입니다. 

만약 display:block으로 변경되는 요소 자체에 aria-live 속성이 있으면 이를 스크린 리더가 읽어주지 못합니다.

다음은 접근성이 잘 적용된 예시라 할 수 있습니다.

<div aria-live="polite">

    <p id="paragraph" style="display: none;">This is a new paragraph.</p>

   </div>

[Android native] 확장/축소 접근성 구현 관련

Webacc NV | 2020-07-28 18:39:46

앱을 구현하다보면 특정 요소를 탭했을 때 이에 해당하는 하위 콘텐츠가 확장되거나 축소되는 객체들을 만드는 경우들이 있습니다.

예를 들어 과일 리스트 버튼을 탭하면 그 아래로 사과, 오렌지, 배 등의 과일들이 표시되고 다시 한번 더 과일 리스트 버튼을 누르면 하위 요소들이 숨겨지는 UI 입니다.

그런데 이때 확장/축소가 되는 버튼에 추가 접근성 구현을 하지 않으면 스크린 리더 사용자는 이중탭했을 때 해당 요소가 확장이 되는 것인지 축소가 되는 것인지를 알 수 없습니다.

안드로이드에서는 이를 지원하기 위한 메소들 제공하고 있는데 

(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE) 혹은 EXPAND 입니다.

이것을 AccessibilityNodeInfo 정보에 추가해 주면 축소된 경우에는 접음, 확장된 경우에는 펼침 이라고 음성을 출력해 주게 됩니다.

마치 HTML에서 aria-expanded 속성을 추가한 것과 비슷합니다.

다만 이 AccessibilityAction을 제대로 적용하려면 performAccessibilityAction 메소드를 오버라이드 하여 expand, collapse 속성에 따른 확장/축소 구현을 한 다음 이를 onClick과 같은 요소에 적용해 주어야 합니다. 

조만간 이에 대한 아티클을 발행해 보도록 하겠습니다.

[메일] Windows 오피스 365 아웃룩에서 제공하는 대체 텍스트 삽입

Webacc NV | 2020-07-27 14:53:26

스크린 리더 사용자 입장에서 여러 회사에서 발송되는 메일을 받았을 때 느끼는 접근성 이슈 중 하나는 대부분 메일의 이미지에 대한 대체 텍스트가 삽입되어 있지 않다는 것입니다.

메일도 하나의 웹페이지에 해당하므로 이미지에 대한 대체 텍스트 삽입은 웹피이지 뿐만 아니라 메일에서도 중요합니다.

그런데 메일 작성 시 오피스 365 아웃룩을 사용하게 되면 메일 프로그램 내에서 이미지에 대한 대체 텍스트 삽입이 가능합니다.

첨부한 이미지를 선택하고 마우스 오른쪽 버튼 혹은 팝업키를 누르면 대체 텍스트 편집 혹은 삽입이 있습니다.

오피스 아웃룩을 사용하신다면 해당 기능을 이용해 보시기 바랍니다.

[HTML Native & Screen Reader] 가상커서와 title 속성

Webacc NV | 2020-07-23 11:52:11

input, button과 같은 form의 컨트롤 요소, 링크 등과 같이 키보드의 tab 키를 눌러 이동할 수 있는 요소에는 title 속성을 작성할 수 있습니다.

title 속성이 제공된 컨트롤에 마우스 포인터가 올라가면 말풍선을 제공하며, title 속성에 작성된 텍스트가 표시됩니다. 이처럼 스크린리더를 사용할 때, Tab 키로 title이 있는 컨트롤로 초점을 이동하면 스크린리더가 타이틀 텍스트를 읽게됩니다.

이 title 속성은 국내에서는 링크의 새 창 안내와 같은 이슈로 인해 앵커 태그에 "새 창" 이라는 텍스트로 많이 사용중인 것을 볼 수 있습니다. 하지만, title 속성에는 맹점이 있습니다. 

HTML에서 컨트롤 요소가 아닌 기본 요소는 초점이 없습니다. 따라서 커서 브라우징과 같은 별도의 키보드 접근성 기능을 사용하지 않고서는 키보드로 텍스트에 접근할 수 없습니다. 이와 마찬가지로 스크린리더 사용자들은 가상커서를 통해 텍스트에 접근하는데, 당연히 이 가상 커서로 버튼과 같은 컨트롤에 접근할 수 있습니다.

하지만, 시스템 커서와 스크린리더의 가상 커서는 별도의 프로그램이기 때문에 방향키나 스크린리더의 탐색 단축키로 이동하면 스크린리더 특성으로 인해 Tab 키로 컨트롤에 접근한 것과 다르게 읽습니다.

그 특성때문에 생기는 대표적인 문제 중 하나가 화살표 키나 스크린리더의 빠른 탐색 단축키를 사용하여 컨트롤에 접근하면 title 속성을 읽지 않는다는 것입니다. 추가로 aria-describedby도 이와 동일한 문제를 갖고 있습니다.

따라서 중요한 내용을 스크린리더 사용자에게 전달하고자 할 때에는 title 속성이나 aria의 describedby보다는 요소의 특성에 따라 숨김 텍스트나 live region 같은 대체 수단을 사용하는 것이 바람직힙니다.

[Android native] setSelected 메소드와 스크린리더 관련

Webacc NV | 2020-07-22 17:08:10

일반적으로 setSelected 메소드는 여러 요소 중 하나가 선택되었을 때 사용합니다. 

예를 들어 과일 리스트, 채소 리스트, 육류 리스트 중 하나가 선택되어 있다면 선택된 요소에 setSelected="true" 속성을 사용하게 됩니다.

setSelected true가 적용되면 TalkBack에서 해당 요소에 포커스 했을 때 '선택됨'이라고 읽어줍니다.

그런데 setSelected 속성이 선택됨, 즉 상태정보를 표시하는 요소가 아닌 경우에도 사용되는 경우를 가끔 보곤 합니다.

그렇게 되면 스크린 리더 사용자는 해당 요소에 포커스 했을 때 선택됨 이라는 상태정보를 듣게 되어 레이아웃 파악이 혼란스럽습니다.

따라서 setSelected 속성은 상태정보를 나타내는 영역에만 사용하는 것이 좋습니다.

만약 부득이 상태정보를 나타내는 요소가 아님에도 사용해야 한다면 접근성 API가 해당 속성을 무시할 수 있도록 AccessibilityNodeInfo 정보 수정이 필요합니다.

AccessibilityNodeInfo 정보를 수정하려면 수정하고자 하는 객체에 setAccessibilityDelegate 객체를 만들어 onInitializeAccessibilityNodeInfo 메소드를 선언한 다음 info.setSelected="false"와 같이 수정할 수 있습니다.

[iOS native] 화면 상단의 메인 제목을 머리말로 읽어주지 않는다면

Webacc NV | 2020-07-21 18:52:59

안드로이드 플랫폼과 달리 iOS에서는 각 화면에 제목이 표시되는 경우 제목 요소는 VoiceOver가 '머리말'이라고 읽어주는 것이 스크린 리더 사용자에게 자연스럽습니다.

접근성을 테스트 하다보면 화면 메인 제목을 '머리말'이라고 읽어주지 않는 경우를 종종 보곤 합니다.

NavigationController를 사용하여 각 화면에 제목을 지정하는 경우에는 특별한 작업을 하지 않아도 VoiceOver가 제목 요소로 인식하여 '머리말'이라고 읽어줍니다.

그러나 UILabel을 사용하여 커스텀 제목을 삽입하면 당연하게도 UILabel은 일반 텍스트 요소이므로 화면의 제목임을 VoiceOver가 인식하지 못합니다.

따라서 화면 메인 제목이 UILabel이라면 UIAccessibilityTrait header를 사용함으로써 좀 더 의미에 맞는 요소 유형을 제공할 수 있습니다.

톡백 베타 버전에 추가된 multi finger gesture 도입 및 기타 기능 추가에 관하여

Webacc NV | 2020-07-20 18:53:28

현재 안드로이드 11 베타 버전이 공개되어 픽셀 단말기를 가지고 있다면 베타 버전을 사용해볼 수 있습니다.

현재 베타 버전에는 TalkBack 9점때 버전이 탑재되어 있는데 TalkBack 개발자 설정에서 multi finger 제스처 기능을 활성화 할 수 있습니다.

이렇게 되면 기존 아이폰에서 사용하던 두 손가락 두 번탭/세 번탭/두 번탭하고 길게 누르기, 세 손가락, 네 손가락 관련 여러 손가락 제스처를 활용할 수 있습니다.

갤럭시폰에 탑재된 VoiceAssistant에서 사용 가능했던 제스처들을 TalkBack에서도 사용 가능하다고 보시면 됩니다.

또한 할당할 수 있는 제스처에도 예전에는 없던 음성 피드백 중지, 미디어 일시정지 및 재생 기능이 추가되었습니다.

현재 안드로이드 베타 버전이 업데이트 될때마다 기능이 추가되고 있으므로 실제 버전이 출시되었을 때를 기대해 보아도 좋을 것으로 보입니다.

[HTML 공통] WAI-ARIA로 탭컨트롤 구현 시 탭패널 시작 콘텐츠가 텍스트인 경우

Webacc NV | 2020-07-17 14:29:52

탭키를 눌러 WAI-ARIA로 구현된 탭컨트롤에 포커스 하면 스크린 리더의 가상커서가 자동으로 꺼집니다.

이는 과일 리스트, 채소 리스트, 육류 리스트와 같은 각 탭 요소 이동은 탭키가 아닌 화살표 키를 이용하도록 하고 있기 때문입니다.

그리고 다시 탭키를 누르면 탭 본문 콘텐츠로 이동하고 스크린 리더 가상커서는 자동으로 켜집니다.

탭 본문 시작이 '사과 링크, 포도 링크'와 같이 포커스가 가능한 요소이면 탭을 눌렀을 때 링크가 탭 본문의 첫 번째 내용이므로 문제가 없습니다. 

그러나 탭 본문 시작이 '과일은 우리의 건강에 많은 도움을 줍니다'와 같이 텍스트라면 탭 컨트롤에서 탭키를 누르는 순간 탭 본문 콘텐츠의 첫 텍스트가 아닌 다른 영역의 링크 요소로 포커스 되기 때문에 본문 시작지점으로 이동하기 위해 여러번 키조작을 해야 한다는 문제가 있습니다.

기본적으로 텍스트 콘텐츠에는 탭키로 포커스 할 수 없는 것이 일반적이지만 탭 콘텐츠가 텍스트로 시작한다면 예외적으로 텍스트 시작 지점에 tabindex="0" 속성을 주어 포커스가 가능하도록 구현해 주는 것이 스크린 리더 사용자의 효율적인 웹 탐색을 도울 수 있습니다.

단 tabindex는 본문 콘텐츠가 시작되는 div 영역에 주기보다는 첫 번째 텍스트가 들어가는 p 요소에 주는 것이 좋습니다.

이는 현재 안드로이드 TalkBack의 경우 탭패널, 즉 탭 본문 콘텐츠에 aria-labelledby 속성이 포함되고 본문 전체를 감싸는 div에 tabindex를 주는 경우 탭콘텐츠 내의 텍스트 영역을 읽지 못하는 문제가 발생하기 때문입니다.

[Android native] 사용자의 이중탭에 의해 버튼의 텍스트가 변경되는 경우

Webacc NV | 2020-07-16 18:42:20

사용자가 특정 객체를 이중탭했을 때 이중탭한 요소의 텍스트가 변경된다면 스크린 리더가 변경된 내용을 바로 읽을 수 있도록 접근성을 구현해 주어야 합니다.

대부분 추가 작업을 해 주지 않아도 TalkBack에서 알아서 변경된 내용이나 상태정보를 읽어주는 것들이 많지만 그렇지 못한 상황도 있습니다.

재생/일시정지, 랜덤재생/순차 재생과 같이 사용자가 탭할 때마다 버튼의 텍스트가 변경되는 객체가 있다고 가정해 보겠습니다.

객체의 텍스트가 대체 텍스트, 즉 contentDescription을 사용하여 구현한 경우라면 스크린 리더 사용자가 이중탭할 때마다 바뀐 텍스트 내용을 자동으로 읽습니다.

그러나 화면에 보여지는 텍스트, 즉 android:text로 구현한 경우에는 이중탭을 해도 바뀐 텍스트 내용을 읽어주지 못합니다.

이를 해결하려면 화면의 텍스트가 변경되었을 때 sendAccessibilityEvent 메소드의 컨스턴트 중에서 TYPE_VIEW_SELECTED를 사용하면 됩니다.

지난 번에 소개해 드린 TYPE_VIEW_FOCUSED 컨스턴트와 다른 점은 SELECTED를 사용하면 변경된 텍스트만 읽게 된다는 것입니다.

즉 랜덤 재생 버튼에 포커스한 상태에서 이중탭했을 때 순차 재생으로 변경되었다면 스크린 리더 사용자에게 필요한 정보는 '순차 재생 버튼'이 아니라 '순차 재생'이므로 TYPE_VIEW_SELECTED 컨스턴트를 사용하는 것입니다.

[iOS native] iOS 14에서 TabBar 구현시 VoiceOver가 읽어주는 방식 변경 관련

Webacc NV | 2020-07-15 18:18:45

iOS 14에서는 TabBar를 구현할 경우 VoiceOver 포커스가 탭 영역에 진입하면 탭막대라고 음성을 출력하여 영역에 대한 정보를 조금 더 자세하게 알려줍니다.

예를 들어 하단 탭바에 과일 리스트, 채소 리스트, 육류 리스트가 있고 VoiceOver 포커스가 일반 콘텐츠에서 탭 컨트롤 영역에 진입하면 영역 정보가 변경되었으므로 '탭막대, 과일리스트'라고 음성을 출력합니다.

물론 아직은 개발자 버전이라 버그도 있습니다. 

일반 콘텐츠에서 탭바로 진입할 경우에는 현재 선택된 탭 정보의 선택됨 상태정보를 읽지 못합니다.

이 문제는 정식 업데이트 때에는 해결되길 바라봅니다.

[iOS native] VoiceOver 포커스가 시간, 배터리 등이 표시되는 상태바 영역으로 날아가는 접근성 이슈 해결하기

Webacc NV | 2020-07-14 12:13:34

iOS 앱 접근성을 테스트하다보면 가끔 VoiceOver 포커스가 의도치 않게 배터리, 시간 등을 표시하는 상태 막대 영역으로 튀어버리는 경우를 발견하곤 합니다.

해당 이슈에 대한 원인은 기존에 포커스하고 있던 A 요소가 숨겨져서 VoiceOver에서 더 이상 A 요소에 포커스를 할 수 없게 되었을 때 VoiceOver 포커스가 이동할 곳에 대한 포커스 이벤트 처리를 하지 않았기 때문에 발생하는 것입니다.

기본적으로 특정 요소가 화면에서 사라지면 VoiceOver는 포커스를 잃어버리게 되는데 이때 바로 위의 포커스 할 곳을 찾아 이동합니다.

이때 다행히 사라진 요소 위에 ㄷ른 포커스할 요소가 있다면 다행이지만 그렇지 않으면 있을 곳이 없어 산태바 영역으로 포커스가 이동하는 것입니다.

따라서 사용자의 인터랙션이나 혹은 자동으로 특정 요소가 화면에서 사라지는 경우에는 layoutChangedNotification 메소드를 활용하여 VoiceOver가 새롭게 포커스할 대상을 지정해 주는 것이 좋습니다.

[HTML] 여러 <ul> 및 <li> 요소를 사용한 내비게이션 형 링크 제공 시

Webacc NV | 2020-07-13 17:09:24

웹문서에서 일반적으로 탭키를 누르면 링크나 버튼, 편집창, 라디오버튼과 같은 상호작용이 가능한 컨트롤에만 포커스 됩니다. 

따라서 스크린 리더 사용자는 내비게이션 위주로 페이지 탐색시 탭키를 주로 사용하며 텍스트를 읽을 때는 스크린 리더에서 제공하는 가상커서를 사용하게 됩니다.

그런데 특정 페이지에 과일, 채소, 육류 리스트와 같은 여러 <ul> 그룹이 있고 각 ul의 li 요소는 링크를 포함하고 있다고 가정할 때 본 영역은 내비게이션 영역이므로 주로 탭키를 사용하는 경우가 많습니다.

 

예:

<p>과일리스트</p>

<ul>

 <li><a href="http://www.example1.com">사과</a></li>

 <li><a href="http://www.example2com">오렌지</a></li>

</ul>

<p>육류 리스트</p>

<ul>

 <li><a href="http://www.example3.com">삼겹살</a></li>

 <li><a href="http://www.example4.com">등심</a></li>

</ul>

 

그런데 위의 마크업 예제로 된 목록을 탭키를 눌러 탐색할때 스크린 리더가 읽어주는 내용은 

1. 각 링크의 레이블과 링크라는 컨트롤 요소.

2. 목록 안에 있다는 정보. 

3. 그 목록에서 몇 번째 요소인지에 대한 정보

입니다.

문제는 어떤 목록인지, 즉 과일 리스트인지, 육류 리스트인지 등에 대한 목록 제목을 알 수 없다는 것입니다.

어떤 목록인지 알기 위해서는 탭키로 탐색하다가 다시 위쪽 화살표키를 이용하여 목록의 제목 레이블을 확인해야 합니다.

이를 해결하는 것은 간단합니다.

목록의 제목에 해당하는 요소에 id를 부여한 다음 각 ul 요소에 aria-labelledby="부여한 아이디"와 같이 연결만 해 주면 됩니다.

이렇게 하면 탭키를 누르는 것만으로 목록의 첫 링크를 읽을 때 어떤 목록인지에 대한 제목을 함께 읽어주어 스크린 리더 사용자에게 편리합니다.

샘플 페이지에서 테스트해보기

[Android native] ImageView의 접근성 처리에 관하여

Webacc NV | 2020-07-10 18:23:33

HTML에서 <img> 요소를 활용한 화면의 레이아웃 장식을 위해 사용된 이미지에는 대체 텍스트를 삽입할 필요가 없으며 이를 보조기술에서 숨기기 위해 일반적으로 alt="" 속성을 사용합니다.

레이아웃을 위해 표시되어 콘텐츠 맥락과는 아무런 관계가 없는 이미지를 일일이 스크린 리더가 읽는 것도 탐색의 효율성이 떨어지기 때문입니다.

이것은 모바일앱에도 그대로 적용되며 각 플랫폼에 따라 구현 방법이 다릅니다.

그런데 안드로이드의 경우 클릭 이벤트가 없고 ImageView 요소에 contentDescription 텍스트를 삽입하지 않으면 TalkBack에서 해당 ImageView에는 포커스 자체가 되지 않습니다.

따라서 장식용 이미지를 표시하기 위해 ImageView가 사용되었다면 contentDescription 자체를 삽입하지 않는 것만으로 보조기술에서 숨기는 것이 가능합니다. 

[iOS native] 하나의 버튼에 이미지와 텍스트가 분리된 경우 접근성 이슈 및 해결방법

Webacc NV | 2020-07-09 11:12:51

스크린 리더 사용자가 한 손가락 오른쪽 혹은 왼쪽 쓸기 제스처를 통해 앱을 탐색하다보면 버튼과 텍스트의 초점이 두 개로 분리되어 탐색되는 경우를 종종 보곤 합니다.

연필 모양의 버튼과 편집 텍스트의 스크린 리더 포커스가 분리되는 캡처화면

예를 들어 편집 버튼이 있다고 가정한다면 첫 번째 요소는 버튼, 두 번째 요소는 편집에 포커스가 되는 것입니다.

이런 경우 '버튼'이라고 읽어주는 요소와 '편집'이라고 읽어주는 요소가 하나가 아닌 두 개의 다른 요소로 인식할 수 있으며 버튼 자체에도 대체 텍스트가 없으므로 문제가 됩니다.

이를 해결하는 방법에는 두 가지가 있습니다.

스토리 보드 사용 시:

1. 텍스트 객체, 즉 UILabel은 identity inspector의 accessibility 섹션에서 AccessibilityElement 속성을 체크 해제합니다. 이렇게 하면 VoiceOver에서 더 이상 해당 레이블은 포커스 하지 않게 됩니다.

2. 이미지 버튼 요소의 identity inspector > accessibility 섹션에서 accessibilityLabel 입력란에 초점이 가지 않게 된 UILabel의 텍스트를 입력합니다.

위와 같이 구현하면 '편집 버튼'과 같이 하나의 초점으로만 포커스 되므로 접근성 이슈가 해결됩니다.

Swift 코드를 사용하는 경우:

1. 스토리보드와 마찬가지로 UILabel 객체를 스크린 리더가 포커스 하지 못하도록 하기 위해 isAccessibilityElement = false 속성을 삽입합니다.

2. 이미지 버튼 객체에 accessibilityLabel을 추가합니다.

CSS3의 filter속성으로 간편하게 사용불가(disabled) 스타일을 만들어보세요.

Webacc NV | 2020-07-07 18:03:10

disabled나 aria-disabled 속성은 이름만 봐도 알 수 있듯 무언가 동작이 불가능하다는 의미를 가지고 있는 상태 정보입니다.

한국판 NVDA나 센스리더에서는 "사용 불가", 영문판에서는 "Unavailable"이라고 스크린리더 사용자에게 친절하게 안내해줍니다.

게시판 하단의 "이전 페이징" 또는 "다음 페이징" 버튼을 예로 들 수 있는데, 더 이상 이전 또는 다음 페이징 링크 목록이 없으면 사용자에게 "더 이상 뒤로 갈 수 없어요!"라고 알려줘야할 때 사용합니다. 만약, 기능을 사용할 수 없는 상태의 요소에 이 속성이 없다면, 스크린 리더 사용자는 사용가능한 컨트롤으로 인식하고 누르게됩니다.

반대로, disabled나 aria-disabed를 사용하여 스크린리더 사용자를 고려하였으나, 한 눈에 두드러지는 스타일을 사용하지 않아서 구분할 수 없는 페이지가 종종 보입니다. 이러한 페이지는 시각적으로 페이지를 사용하는 모든 사용자에게 적절하지 않습니다.

그래서 활성화된 버튼과 비활성화된 버튼은 두드러진 차이가 있어야 합니다. 일일히 디자인을 새로 줄 수도 있지만, CSS3의 filter 속성과 함수를 사용하면 손쉽게 disabled 상태를 시각적으로 구현할 수 있습니다.

메인: filter 속성으로 disabled를 손쉽게 표현하기

filter 속성에는 여러 함수가 있습니다. 이 함수중에서 disabled 상태의 요소를 만들 때 유용하게 사용할 수 있는 요소는 다음과 같습니다.

  • contrast() : 값은 int값으로 0부터 소수점단위로 설정가능합니다. 높을 수록 색상의 대비가 높아집니다. 기본값은 1입니다.
  • brightness() : 역시 위와 같이 int값을 사용하며, 기본값은 1입니다. 높을수록 밝아집니다.
  • opacity() : 알파(투명도) 필터입니다. int를 사용하며, opacity로 대체 가능합니다. 최대값은 1이며 기본값이기도 합니다.
  • grayscale() : 그레이스케일(색조) 필터입니다. int를 사용하며, 위의 함수들과 반대로 높을수록 회색에 가까워집니다.

이 중 한 가지만을 사용하여도 충분히 disabled 상태와 걸맞는 디자인을 줄 수가 있으며, 다음과 같이 동시에 여러 개의 함수를 쓸 수 있습니다.

.filter:disabled,.filter[aria-disabled=true]{
    filter: grayscale(0.3) opacity(0.4) brightness(1.2) contrast(0.7);
}

이를 적절히 사용하여 버튼이나 라디오 버튼과 같은 컨트롤 요소에 disabled 상태를 색상을 손대지 않고 만들 수 있습니다.

filter 속성은 대부분의 브라우저에서 지원하기 때문에 웹 디자이너는 간편한 방법으로 만들 수 있다는 점에서 만족할 수 있으며, 사용자는 더 양질의 서비스로 도움을 받을 수 있을것입니다.

번외: 비활성화된 요소는 반드시 커서 속성을 수정해주세요!

CSS의 cursor 속성은 매우 중요합니다. 분명 위 방법으로 disabled 속성을 멋지게 만들어주었음에도 커서 속성을 변경해주지 않으면, 사용자에게 혼란을 줄 수 있습니다. 기본값으로 마우스를 올렸을 때, cursor값이 pointer로 제공된 링크나, 임위로 cursor를 pointer로 준 요소는  마우스 포인터의 모양이 손 모양으로 변경됩니다. 사용자로 하여금 누를 수 있는 요소로 오인할 수 있는 여지를 주며, 머릿속에 자연스럽게 물음표가 떠오릅니다.

따라서, disabled 상태인 요소에 마우스 포인터가 올라갔을 때는 cursor값을 아래와 같이 default로 만드는 것이 좋습니다.

.filter:disabled:hover,.filter[aria-disabled=true]:hover{
    cursor:default;
}

disabled 요소 만들기 체험: [링크]

[Android native] TalkBack에서의 activity_label, setTitle 텍스트 처리 방식

Webacc NV | 2020-07-07 10:44:36

TalkBack을 활성화하고 접근성을 테스트 하다보면 화면이 전환될 때 대부분 화면 제목을 자동으로 읽는 것을 경험하셨을 것입니다.

그것은 TalkBack에서 activity_label 텍스트 혹은 setTitle 텍스트를 가지고 와서 제목으로 읽어주는 것인데 화면 제목을 자동으로 읽어주면 일일이 제목을 터치해서 확인하지 않아도 화면의 내용을 대략 유추할 수 있어 큰 도움이 됩니다.

문제는 UI에 따라서는 화면 제목을 사용하지 않거나 커스텀으로 제목을 사용하는 경우에는 화면 상의 제목 표시줄은 숨기기 때문에 activity_label 텍스트를 삽입하지 않는 경우가 많습니다.

어차피 화면에서 사용하지 않기 때문입니다.

그런데 그렇게 되면 스크린 리더 사용자에게는 문제가 있습니다.

activity_label 텍스트가 없으므로 TalkBack이 화면 제목을 읽을 수 없어 애플리케이션의 제목을 읽게 되고 직관적인 피드백을 받을 수 없게 됩니다.

따라서 화면에서 제목을 숨기더라도 스크린 리더 사용자를 위해 activity_label 텍스트를 각 activity마다 삽입하거나 혹은 setTitle 메소드를 추가해 주는 것이 큰 도움이 됩니다.

웹페이지 다크 모드 만들기 with CSS 변수

Webacc NV | 2020-07-06 12:28:44

 2019년부터 UI에는 한가지 큰 열풍이 불었습니다. 바로 다크모드라고 하는 멋진 친구입니다. 다른이름으로는 야간 모드라고도 부르지요. iOS 13, Android 10, Windows 10 1903 버전 등에서 이 다므코드를 지원하기 시작했습니다.

웹에서도 이러한 사용자 환경에 따라 웹페이지의 색상을 다르게 표시할 수 있는데요. 오늘은 다크 모드를 지원하는 방법에 대해 짧게 설명하고자 합니다.

 

어떻게 다크 모드를 브라우저가 인식하게 할까요?

다크 모드는 아래의 미디어 쿼리 속성으로 지원할 수 있습니다.

@media(prefers-color-scheme:dark){
    .selector {
       properties
    }
}

지난 아티클에서도 소개드린 적 있는 친구, prefers-color-scheme 입니다.  이 친구를 부르면 Android 10, iOS13, 1903 이상의 Windows 10, 그리고 모하비 이상의 Mac OS 환경에서의 최신 브라우저에서 어두운 테마를 별도로 제공할 수 있습니다.

 

더 효율적인 미디어쿼리 관리를 위한 CSS 변수를 함께 사용하기

조금 더 와닿을 수 있도록 짧게 한번 레이아웃 페이지를 만들어보도록 할텐데요. 그 전에 소개시켜드릴 친구가 더 있습니다. 어두운 테마를 작성할 때 조금 더 편리하게 처리해주는 친구인 CSS 변수로, Chrome이나 Firefox 등 다크모드를 지원하는 최신 브라우저에서 대부분 사용이 가능합니다.

CSS 변수는 아래와 같이 선언할 수 있고, 사용할 수 있습니다.

#wrapper {
   --landmark-BgColor:ivory;
   --landmark-FtColor:black;
}

#wrapper header{
   background-color:var(--landmark-BgColor);
   color:var(--landmark-FtColor);
}

동일한 색상 코드를 반복적으로 사용하는 것이 편하게 보일 수도 있으나, 조금더 의미를 부여하여 CSS 코드를 작성하는 대에 도움을 주고, 변수는 재활용이 가능하기 때문에 내가 원하는 곳에 적절하게 사용할 수 있으며, 선택한 색상이 영 마음에 들지 않을 경우, 위에 선언한 변수만 바꾸면 되는 장점이 생깁니다. 이는 CSS 코드가 길어질수록 더 빛납니다.

CSS 변수는 이렇게 특정 요소부터 종속시킬 수도 있지만, 아래와 같이 전역 변수로 만들 수도 있습니다. 전역변수로 만들려면 :root이라는 가상 클래스 선택자(pseudo class)를 사용하면 됩니다.

:root{
  --Global-CSS-Variable:linear-gradient(#00f, #a0f, #f0a);
}

전역이라는 이름에서 알 수 있듯, 이렇게 등록된 변수는 어떠한 곳에서든 사용이 가능합니다. 이번 다크테마 만들기에서는 이 전역변수를 활용할 것입니다.

그럼 본격적으로 페이지를 어둠으로 물들여봅시다.

:root{
 /*Body*/
  --body-bgColor:#fafafa;
  --body-ftColor:#2c2c2c;
 /*Landmark*/
  --landmark-bgColor:#5cba8f;
}
*{padding:0;margin:0; box-sizing:border-box;}
html{width:100vw; height:100vh;  max-width:100%;}
body{
  width:100%; height:100%;
}

/*중요하지 않은 부분입니다. 레이아웃 관련*/
.wrapper{display:grid; width:100%; height:100%;grid-template-columns: 2fr 6fr 2fr;grid-template-rows: 1fr 8fr 1fr; gap:1rem;padding:0.5rem;}
.wrapper>*{padding:0.5rem;}
header{grid-column: span 3;} nav{grid-column:1/2;}
main{grid-column: span 2;} footer{grid-column: span 3;}
header,footer{
  display:flex; flex-flow:column wrap;
  align-items: center; justify-content: center;
}
.CopyLeft{
  display:inline-block;
  transform:rotate(180deg);
  vertical-align: middle;
  user-select:none;
}
.Light .sun, .Dark .moon{
  display: inline-block; width:2rem; height:2rem;
} .Light, .Dark{ font-size:200%; }


/*** 주요 코드! ***/
body{ /*바디에 색상을 줍시다*/
  color:var(--body-ftColor); background-color:var(--body-bgColor);
}

.Dark{ /*밝은 화면(기본화면)에서 다크모드의 인사가 나오지 않도록 합니다*/
  display:none;
}

header,nav,footer{ /*배너 랜드마크나 내비게이션같은 부수적인 랜드마크에 색상을 다르게 줍시다*/
background-color:var(--landmark-bgColor);
color:var(--landmark-ftColor);
border-radius:0.5rem;}

.Light .sun{ /*라이트 모드에서 보여질 햇님 아이콘을 추가하고, 글자와 똑같은 색을 입혀줄게요~*/
  mask:url(https://image.flaticon.com/icons/svg/869/869818.svg) 100%;
  -webkit-mask:url(https://image.flaticon.com/icons/svg/869/869818.svg) 100%;
  background-color:var(--body-ftColor);
}

@media (prefers-color-scheme:dark){ /*이제 어두운 화면에서 보여질 색상들을 입혀줍시다*/
    :root{
    --body-bgColor:#2c2c2c;
    --body-ftColor:#fafafa;
    --landmark-bgColor:#191919;
/*각각의 색상을 변수로 다시 지정해줍니다. 어두운 화면인 동안에만 이 색상으로 모두 교체되므로 아래에 중복된 코드를 입력할 필요가 없습니다.*/
  }

  .Dark{display:block;} /*라이트 모드에서 숨겼던 다크모드의 인사말을 표시해요!*/
  .Light{display:none;} /*반대로 라이트 모드에서 인사했던 햇님 친구를 숨겨줍시다.*/

  .Dark .moon{ /*라이트 모드에서 보여질 달님 아이콘을 추가하고, 글자와 똑같은 색을 입혀줄게요*/
    mask:url(https://image.flaticon.com/icons/svg/899/899555.svg) 100%;
    -webkit-mask:url(https://image.flaticon.com/icons/svg/899/899555.svg) 100%;
    background-color:var(--body-ftColor);
  }
}

한 코드에 모든걸 담아놓아 조금 복잡하지만, 핵심만을 말씀드리자면, prefers-color-scheme에서 변수의 값만 다시 대입해준다면, 중복된 코드 없이 손쉽게 다크모드 페이지를 지원할 수 있다는 점입니다. 이 둘을 함께쓴다면 조금도 효율적으로 색상 팔레트를 관리할 수 있겠지요?

 

단순히 색상반전을 사용하면 안 되는 건가요?

이 방법은 반전 기법과 비교했을 때 조금 복잡하고 불편해 보일 수도 있지만, CSS의 Filter를 이용한 반전 기법에도 단점이 있습니다.

간편한 만큼 사용자와 디자이너의 욕구를 만족시키지 못한다는 점인데요. filter 프로퍼티의 Invert라는 반전 CSS함수를 사용하면, 손쉽게 밝은화면과 어두운 화면을 구현할 수 있지만, 하위에 위치한 모든 요소의 색상이 반전되므로 원하지 않는 색상이 표시되기도 합니다. 웃자고 쓰는 표현으로, 사람 얼굴이 나온 사진이 있다면, 스머프 처럼 보이게 됩니다.

물론, 이에 대한 대처법도 존재하지만, 페이지에 따라서 오늘 소개해드린 prefers-color-scheme 만큼 또는 그 이상의 작업이 필요할 수도 있습니다. 따라서 어두운 화면을 구현할 때는 Invert보다 색상 팔레트를 따로 구성하여 만드는 것이 더 아름답고 깔끔할 수 있습니다.

 

팁 치고는 긴 글 읽어주셔서 감사드리며, 아래는 위의 코드를 적용한 참고용 레이아웃 페이지입니다. 필요한 분께 좋은 자료가 되길 희망합니다.

[적용 페이지]

[iOS native] UILabel을 VoiceOver가 처리하는 방식

Webacc NV | 2020-07-03 16:40:12

UILabel은 일반적으로 클릭 이벤트가 없는 텍스트를 화면에 표시할 때 사용하는 요소입니다.

그래서 VoiceOver에서도 UILabel 요소로 된 텍스트는 요소 유형이 따로 지정되어 있지 않습니다.

그런데 텍스트 자체를 클릭하여 다른 화면으로 이동하도록 구현하거나 기타 상황에 따라 UILabel에 클릭 이벤트를 삽입하는 경우도 있습니다.

일반적으로 UILabel에 클릭 이벤트를 추가할 때는 다음과 같은 두 속성을 사용하게 됩니다.

1. UITapGestureRecognizer(target: self, action: #selector((customClick_:)))

2. object.UserInteractionEnabled = true

그런데 클릭 이벤트를 추가하는 순간 VoiceOver는 해당 UILabel에 자동으로 버튼이라는 컨트롤 속성을 추가합니다.

즉 정리하면 tapGestureRecognizer 이벤트를 통해서 UILabel에 클릭 이벤트를 주게 되면 UIAccessibilityTrait를 button 등으로 변경하지 않더라도 기본적으로 VoiceOver가 버튼이라는 컨트롤 유형을 삽입한다는 것입니다.

UILabel에 클릭 이벤트 구현 시 참고가 되었으면 좋겠습니다.

 

[HTML 공통] 링크나 버튼 요소에 스크린 리더용 숨김 텍스트 제공시 주의사항

Webacc NV | 2020-07-02 11:27:07

링크나 버튼 요소에 의미를 가진 아이콘과 텍스트가 함께 삽입된 경우 스크린 리더 사용자를 위한 아이콘에 대한 설명을 숨김 텍스트로 링크나 버튼 요소 내에 제공해 주는 경우를 종종 보곤 합니다.

또한 새창으로 열리는 링크에도 '새창' 문구를 링크 내에 숨김 텍스트로 삽입하기도 합니다.

그런데 스크린 리더 사용자가 이러한 요소를 탐색할 때 불편한 부분은 대부분 이러한 숨김 텍스트 앞 혹은 뒤에는 문맥에 맞는 띄어쓰기 혹은 문장부호가 없다는 것입니다.

예를 들어 화면에 보여지는 링크 텍스트는 구매하기 이고 숨김 텍스트가 새창 이라면 문장부호나 띄어쓰기에 대한 고려를 하지 않을 경우 '구매하기새창 링크'라고 읽어주게 됩니다.

문맥에 따라서 띄어쓰기 혹은 문장부호가 삽입되지 않은 숨김 텍스트는 듣기에 상당히 어색할 수 있으며 특히 음성과 점자로 함께 정보를 읽는 스크린 리더 사용자는 더 어색함을 느낄 것입니다.

따라서 화면에 보여지는 레이블과 추가 설명 텍스트가 자연스럽게 읽어줄 수 있도록 띄어쓰기 및 문장부호 적용이 필요하겠습니다.

좋은 예시:

<a href="http://nuli.navercorp.com">널리로 가기<span class="blind">, 새창</span></a>

<a href="http://nuli.navercorp.com" class="pdf"><span class="blind">pdf&nbsp;</span>다운로드</a>

[Android native] EditText 요소에 contentDescription 사용은 금물

Webacc NV | 2020-07-01 12:26:24

안드로이드에서는 EditText를 사용하여 여러 형태의 편집창을 구현합니다.

EditText에는 android:hint 속성을 통해 무엇을 입력해야 하는지에 대한 레이블을 지정할 수 있는데 레이아웃에 따라 android:hint 속성을 사용하지 않는 경우도 종종 있습니다.

문제는 이때 스크린 리더 사용자를 고려하기 위해 EditText에 contentDescription을 통한 보이지 않는 레이블을 삽입하는 경우를 종종 보곤 합니다.

그러나 위와 같이 구현할 경우 접근성 API에서 해당 객체를 편집창으로 인식하지 못하게 되어 TalkBack에서 제공하는 여러 편집 옵션을 사용할 수 없게 됩니다.

따라서 EditText에 android:hint 속성을 사용할 수 없는 경우에는 labelFor를 통해 인접한 객체의 텍스트를 가지고 오거나 이 또한 불가능한 경우에는 AccessibilityNodeInfo class에서 setHintText 메소드를 사용하여 스크린 리더 사용자를 위한 레이블을 삽입하는 것이 바람직합니다.

보조기술 이해하기

에어류 | 2020-06-30 15:28:44

안녕하세요! 에어류입니다.

국내의 웹 접근성 관련 표준 및 지침은 국가에서 표준으로 정한 한국형 웹콘텐츠 접근성 지침 2.1이 공식표준입니다.

물론 표준대로 웹 접근성을 구현해준다면 보다 많은 정보소외계층이 웹에 접근하기 수월해지겠죠?

 

그래서 준비한 이번 팁은 국내 지침에서 주로 스크린리더에 관련된 검사항목이 많이 포진해 있기 때문에 콘텐츠를 제공하실 때 고려하실 부분에 대하여 예시와 함께 준비해봤습니다.

 

단순히 규격만 준수하기 이전에 보조기술을 이해하고, 이러한 이해를 바탕으로 웹 접근성을 구현한다면 더욱 좋은 웹 사이트가 되시리라 믿습니다.

 

띄어쓰기를 하지 않은 텍스트에 대한 대체텍스트 낭독 KDBWealth의 경우 크드브웰스로 낭독

만약 이런 영어메뉴가 있는데 띄어쓰기를 하지 않은 경우라면 스크린리더에서 어떻게 낭독할까요?

 

띄어쓰기를 하지 않은 텍스트에 대한 대체텍스트 낭독 KDBWealth의 경우 크드브웰스로 낭독

보시는 것처럼 원래 의도한 발음대로 낭독되지 않는 것을 보실 수 있습니다. 

그렇다면 정상적으로 띄어쓰기가 되었을 때의 낭독내용도 함께 확인해보겠습니다.

띄어쓰기를 한 경우 텍스트에 대한 대체텍스트 낭독 KDB Wealth의 경우 케이디비 웰스와 같이 메뉴를 제대로 낭독

보시는 것처럼 띄어쓰기 하나만으로 스크린리더 이용자들에게 더욱 편리하게 이해할 수 있는 텍스트를 낭독하여 접근성을 높힐 수 있습니다. 

 

그래서 우리가 습관처럼 사용하는 -> 와 같은 화살표 조합도 스크린리더로는 "대쉬 그래이터 댄"과 같이 낭독하게 되므로 실제 오른쪽 화살표인 → 특수문자를 그대로 사용하시는 것이 좋습니다. 그러면 "오른쪽 화살표"로 낭독하게 됩니다. 

센스리더에서는 캐럿, -는 대쉬, =은 이퀄, \은 백슬래쉬, |는 버티클바, <는 레스댄, >는 그레이터댄으로 낭독하고, NVDA에서는 캐럿, -는 다시, =은 등호, \은 백슬래쉬, |는 바, <는 레스, >는 그레이터로 낭독하고, 한글죠스11에서는 캐럿, -는 마이너스, =은 이퀄, \은 백슬래쉬, |는 버티클바, <는 레스댄, >는 그레이터댄으로 낭독하고, 실로암 보이스에서는 캐럿, -는 대쉬, =은 이퀄, \은 백슬래쉬, |는 버티클바, <는 레스댄, >는 그레이터댄으로 낭독함을 안내하는 표

 

[HTML 공통] 스크린 리더 사용 시 각 폼 컨트롤의 추가 설명을 바로 들을 수 있도록 구현하기

Webacc NV | 2020-06-30 14:38:53

회원가입, 신청서 작성과 같은 폼 컨트롤을 사용할 때는 편집창, 라디오버튼, 체크박스와 같은 각각의 필드를 탭키로 이동하는 경우가 많습니다. 

그런데 각 필드에 상황에 따라 추가 설명 텍스트가 삽입되는 경우가 있습니다.

비밀번호 입력창에는 '8-16자 내외로 대문자를 반드시 섞어 사용하세요'와 같은 안내 문구가 들어가는 것이 한 예입니다.

그런데 탭 키로만 폼 컨트롤들을 이동하면 각 필드에 해당하는 레이블 및 사용자가 입력한 값만 읽어주므로 부가 설명에 대한 텍스트를 놓치는 경우가 많습니다.

이를 해결하기 위해 각 필드에 aria-describedby="sampleid" 속성을 추가하여 추가 설명이 들어 있는 요소와 연결시켜주면 스크린 리더는 탭키로 각 필드에 포커스 하는 경우 레이블과 함께 추가 설명 문구도 함께 읽어주게 됩니다.

[Android native] 한 화면에 여러 <ListView> 혹은 <GridView>가 있다면 contentDescription을 통해 영역 제목 지정을 고려해 보세요.

Webacc NV | 2020-06-29 10:03:05

며칠전 iOS UITableView에서의 accessibilityLabel 지정시 VoiceOver가 어떻게 동작하는지를 공유하였습니다.

그런데 안드로이드에서도 ListView, GridView 속성에 contentDescription을 추가하면 스크린 리더로 목록뷰, 그리드뷰 영역에 접근 시에 어떤 목록 혹은 그리드 영역인지를 읽어주므로 한 화면에 여러 영역이 나뉘어져 있을 경우 레이아웃 파악이 훨씬 쉬워집니다.

대체 텍스트이므로 화면에는 보이지 않으며 android:contentDescription="연락처 리스트" 등과 같이 삽입할 수 있습니다.

 

[iOS Native] 커스텀 보안 키패드 사용시 VoiceOver 사용자를 위해 keyboardKey UIAccessibilityTrait 사용을 고려해 보세요.

Webacc NV | 2020-06-26 18:21:43

아이폰에서 제공하는 네이티브 키보드는 사용자의 선호도에 따라 각 키보드 키에서 손을 떼면 바로 입력이 되게 하거나 혹은 두 번 탭하여 입력하게 하는 등의 키보드 타이핑 옵션 설정을 할 수 있습니다.

그런데 간편 결제 비밀번호, 공인 인증서 비밀번호와 같은 키패드는 네이티브 키보드를 사용하지 않는 경우가 많습니다.

그래서 사용자가 키를 입력하려면 무조건 이중탭을 해야만 입력이 됩니다.

그러나 이러한 보안 키패드에 keyboardKey UIAccessibilityTrait 속성을 적용하면 사용자의 선호도에 따라 이중탭 혹은 한 번 탭 방식으로 각 키 입력이 가능하게 됩니다.

[WEB 공통] 라디오 버튼 구현시 fieldset을 사용할 수 없을 경우에는 role="radiogroup" 사용을 고려해 보세요.

Webacc NV | 2020-06-25 11:06:34

라디오 그룹의 캡션을 나타내는 fieldset, legend 태그를 사용하지 않을 경우 스크린 리더 사용자가 탭 키를 눌러 라디오 버튼에 포커스 했을 때 겪는 어려움 중 하나는 어떤 그룹의 라디오 버튼인지를 바로 알 수 없다는 것입니다.

fieldset, legend 태그를 삽입하는 경우 탭키로만 라디오 버튼에 접근해도 '과일 라디오그룹, 사과 라디오버튼 체크됨'과 같이 읽어주기 때문에 어떤 종류의 라디오버튼인지 바로 알 수 있습니다.

그런데 화면 상의 공간이 너무 좁은 경우나 기타 이유로 인해 fieldset 태그를 사용할 수 없는 경우가 있습니다.

이런 경우에는 role="radiogroup", aria-label="과일 리스트"와 같이 마크업하여 본 문제를 해결할 수 있습니다.

이렇게 하면 fieldset, legend 태그를 사용할 수 업는 경우라도 라디오 버튼에 탭키를 눌러 접근 시에 어떤 종류의 라디오버튼인지 바로 알 수 있어 웹 탐색이 더욱 편리해 집니다.

iOS 14에서 제공될 back tap 기능에 관하여

Webacc NV | 2020-06-24 11:24:50

현재 코로나로 인해 애플 개발자 회의의 여러 세션들이 온라인으로 진행중입니다.

올 해에 배포될 iOS 14 버전에도 많은 접근성 기능들이 업데이트 될 것으로 예상하는 가운데 한 가지 주목할 기능 중 하나가 'back tap' 입니다.

손쉬운 사용 섹션에서 해당 기능을 활성화 하면 아이폰 뒷면을 두 번 혹은 세 번 두드리는 동작을 특정 기능에 연결하여 사용할 수 있도록 하는 것으로 앱 전환기, 접근성 기능 실행, 제어센터 열기 등 다양한 기능을 연결하여 사용이 가능하다고 합니다.

자세한 내용은 10월달 정도에 발행될 널리 아티클을 통해 공유하도록 하겠습니다.

[Android native] 시간 제한이 있는 팝업 구현 시 getRecommendedTimeoutMillis 메소드 적용을 고려해 보세요.

Webacc NV | 2020-06-23 11:07:17

안드로이드 버전 10 이상부터는 접근성 서비스에 팝업이나 토스트 메시지 등의 시간 제한이 있는 콘텐츠가 출력되었을 때 접근성 서비스에서 강제로 이를 제어할 수 있는 기능이 추가되었습니다. 

쉽게 말해 기본적으로 팝업이나 알림 메시지가 3초 후에 사라진다고 가정하면 접근성 서비스에서 강제로 표시되는 시간을 2분까지 늘려서 사용할 수 있도록 한 기능입니다.

문제는 안드로이드의 토스트와 같은 시스템 자체의 알림을 활용하지 않고 대화상자를 통한 팝업을 띄우거나 커스텀으로 잠깐동안 알림을 띄우는 경우는 개발 시에 접근성 서비스에서 강제로 시간을 늘릴 수 있도록 구현을 해 주어야 합니다.

이 때 사용하는 메소드가 바로 getRecommendedTimeoutMillis 입니다.

자세한 활용방법은 추후 널리 아티클을 통해 다루도록 하겠습니다.

[iOS native] UITableView에는 상황에 따라 accessibilityLabel 속성을 추가해 주세요.

Webacc NV | 2020-06-22 14:00:41

iOS에서 UITableView를 활용하여 화면에 테이블 형태로 콘텐츠를 표시하는 경우가 많습니다.

그런데 스크린 리더 사용자는 한 화면에 여러 영역의 레이아웃이 표시되고 있을 경우 테이블에 접근 시 현재 읽고 있는 영역 정보가 없어 레이아웃 파악이 어려운 경우가 발생하곤 합니다.

HTML에서는 데이터 테이블 구현 시 caption 태그를 통해 테이블 제목을 제공해 줄 수 있는데 iOS에서도 UITableView 자체에 accessibilityLabel 속성을 통해 스크린 리더용 테이블 제목에 해당하는 정보를 삽입할 수 있습니다.

예: tableView.accessibilityLabel = "recipe list"

이렇게 하면 VoiceOver 포커스가 테이블 콘텐츠에 들어갈 때 테이블 제목을 먼저 읽게 되어 현재 어떤 영역에 포커스 하고 있는지 쉽게 파악이 가능합니다.

따라서 화면의 제목만으로 표 영역에 대한 유추가 가능하다면 문제가 없으나 한 화면에 표를 두 개 이상 제공하거나 화면 제목과 관련이 없는 표를 제공할 때는 accessibilityLabel 속성 추가를 고려해 보시기 바랍니다.

[WEB 공통] aria-label은 기존에 있던 레이블을 덮어씁니다.

Webacc NV | 2020-06-19 11:09:48

스크린 리더 사용자를 위해 x(닫기 버튼), 5(알림 아이콘 포함) 버튼과 같이 화면에 보여지는 텍스트만으로는 해당 요소의 의미를 이해하기 어려운 경우 aria-label을 사용하는 경우를 종종 보곤 합니다.

그런데 aria-label은 기존에 표시되는 레이블을 덮어씁니다.

예를 들어 <button class="noti" aria-label="개의 알림 있음">5</button> 과 같이 마크업을 한 경우 스크린 리더 사용자는 화면에 표시되는 5라는 숫자는 읽지 못하게 됩니다.

따라서 aria-label은 x, ?, 아이콘 등으로만 요소를 표시하는 경우이거나 <div contenteditable="true"> 와 같이 HTML의 기본 label 태그를 사용할 수 없는 경우 등에서만 사용해야 합니다.

이것은 id를 연결하여 스크린 리더에서 읽을 수 있는 레이블을 표시하는 aria-labelledby 속성에도 그대로 적용됩니다.

물론 화면에 보여지는 텍스트를 aria-label에 포함한다면 문제는 없겠습니다.

[Android native] TalkBack 8.2에서부터 적용된 ListView 안의 항목개수 처리 관련

Webacc NV | 2020-06-18 14:36:39

안드로이드 접근성을 대응하다보면 아쉬운 점 중 하나는 iOS와 달리 라디오버튼, 목록 리스트뷰와 같은 그룹화된 항목에 TalkBack이 포커스하더라도 몇 개 중 몇 번째 요소에 포커스하고 있는지를 알려주지 못한다는 것입니다.

그런데 최근 업데이트 된 TalkBack 8.2 버전에서는 ListView로 그룹화 된 요소의 경우 목록 안의 요소에 포커스 하면 1/5, 3/5와 같이 총 개수 중 현재 포커스한 요소가 몇 번째임을 음성 출력하도록 수정되었습니다.

따라서 우리가 유의해야 할 사항은 바로 개수에 대한 대체 텍스트 문제입니다.

탭컨트롤의 접근성 구현 시 접근성 대응을 위해 대체 텍스트로 개수에 대한 정보를 주는 경우가 많습니다.

예: 홈 탭(3개 중 첫 번째), 혹은 홈탭(1/3) 등.

그런데 레이아웃에 따라 탭 컨트롤이 ListView로 그룹화되는 경우가 있는데 이 때는 TalkBack에서의 ListView 처리 방식의 변경으로 인해 개수에 대한 정보를 중복 읽게 되는 문제가 발생하게 됩니다.

그렇게 되면 중복 정보로 인해 스크린 리더 사용자가 탐색할 때 불편을 겪게 될 것입니다.

이 부분에 대해서도 이후 널리 아티클을 통해 깊이 다루어 보도록 하겠습니다.

[PC web] NVDA로 웹페이지 접근성 테스트 시 브라우즈모드 포커스 하이라이트 기능을 사용해 보세요.

Webacc NV | 2020-06-17 10:48:46

PC 웹접근성을 테스트 하기 위해 스크린 리더를 사용하는 경우 겪는 어려움 중 하나는 현재 스크린 리더가 읽고 있는 줄이 어디인지 찾기 어려울 때가 있다는 것입니다. 

브라우저는 <input type="text">와 같은 편집 영역이 아닌 이상 커서를 표시하는 캐럿이 없기 때문에 스크린 리더가 가상으로 커서를 만들어서 마치 문서를 탐색하듯이 사용자가 읽을 수 있도록 기능을 제공합니다.

이 커서를 스크린 리더 제조사마다 가상커서, 브라우즈모드 등으로 부르고 있습니다.

그런데 말 그대로 가상커서다보니 스크린 리더에서 자체적으로 현재 읽고 있는 줄을 하이라이트 해 주지 않는 이상 시각적으로 현재 가상의 커서가 있는 위치를 알 수 없습니다.

 

이를 해결하기 위해 NVDA, JAWS 등에서는 나름대로의 방식으로 가상커서 하이라이팅 기능을 추가하였습니다. 

NVDA에서는 NVDA 설정 > vision > 브라우즈모드 커서 하이라이트 기능을 켜서 사용하시면 접근성 테스트 시에 현재 읽고 있는 부분이 노란색으로 표시되므로 조금 더 쉽게 시각적 구분이 가능합니다.

브라우즈모드커서 포커스 하이라이트가 체크된 NVDA 환경설정 화면 스크린샷

[iOS native] 음성 검색 시 마이크가 같은 화면에서 켜지는 경우네는 접근성 적용을 위해 startsMediaSession trait를 사용하세요.

Webacc NV | 2020-06-16 15:21:09

지난 번에는 음성 검색을 누르면 화면이 전환되면서 마이크가 켜졌을 때 VoiceOver 소리가 들어가지 않도록 하기 위한 접근성 팁을 공유했습니다.

이번에 공유할 팁은 음성 검색을 눌렀을 때 화면이 전환되지 않고 바로 마이크가 켜지는 경우입니다.

이때 접근성 대응을 해주지 않으면 VoiceOver가 포커스 하고 있는 같은 레이블을 다시한번 더 읽기 때문에 해당 음성이 함께 들어가게 됩니다. 

이를 해결하는 방법은 너무 간단합니다.

사용자가 음성검색 버튼을 이중탭할 때 레이블을 다시 한번 읽지 못하도록 음성검색 버튼에 startsMediaSession trait를 추가하는 것입니다.

이렇게 하면 사용자가 이중탭하더라도 아무런 내용도 읽지 않게 됩니다.

[WEB 공통] <ul> 요소에 WAI-ARIA 마크업을 통한 tab 컨트롤 구현 시 <li> 요소에는 role="none"을 추가해 주세요.

Webacc NV | 2020-06-15 16:38:41

웹페이지를 탐색하다보면 WAI-ARIA를 활용하여 tab 컨트롤을 구현한 사이트를 종종 보곤 합니다.

그런데 <ul> 요소로 되어 있는 탭컨트롤을 WAI-ARIA로 마크업하는 경우 흔히 하는 실수 중 하나가 각각의 <li> 요소에 role="none"을 추가하지 않는 것입니다. 

<ul>은 role="tablist", <li>는 role="none", 각 a 요소에는 role="tab"을 삽입해 주어야 tab 컨트롤을 제대로 읽을 수 있게 됩니다.

즉 tab 컨트롤은 tablist, tab, tabpanel로 구성되는데 <li> 요소는 tab 컨트롤과 관련이 없으므로 접근성 API에게 관련이 없는 요소라는 것을 알려 주어야 합니다.

[Android native] 한 화면에 여러 제목이 있을 때는 accessibilityHeading="true" 속성을 넣어주세요.

Webacc NV | 2020-06-12 17:59:12

안드로이드에서는 iOS와 달리 제목을 TalkBack에서 따로 인식할 수 있도록 API를 지원한 지가 몇 년 되지 않습니다. 

그러다보니 접근성이 잘 되어 있는 안드로이드 앱에서도 하위 제목을 TalkBack에서 '제목'으로 인식하도록 되어 있는 앱이 드문 것 같습니다.

안드로이드 API 28 이상을 사용하여 앱을 개발하신다면 한 화면에 여러 제목이 존재하는 경우 accessibilityHeading 속성을 추가하면 iOS와 같이 스크린 리더 사용자가 제목 단위로 빠르게 이동할 수 있고 제목 요소임을 빠르게 이해할 수 있습니다.

[Android native] ProgressBar 요소 사용시에는 퍼센트에 해당하는 대체 텍스트를 넣어주세요

Webacc NV | 2020-06-11 11:23:17

안드로이드 앱에서 진행률을 보여주어야 할 때는 ProgressBar를 사용하는 경우가 종종 있습니다.

70%의 진행률 표시줄을 가리키는 스크린샷

그런데 이 ProgressBar는 

android:progress="70"과 같은 속성이 포함되더라도 TalkBack에서 해당 요소에 포커스 하지 못합니다.

즉 진행률이 TextView 형태가 아닌 막대 그래프 형태로만 표시되고 있을 경우에는 스크린 리더 사용자는 이를 읽을 수가 없는 것입니다.

이를 해결하려면 화면에 보여지는 각 progress에 해당하는 값을 contentDescription을 통해 대체 텍스트 형태로 추가해 주어야 합니다.

예: 

android:contentDescription="70%"

이렇게 하면 스크린 리더에서 진행률 표시줄에 포커스 가능하며 퍼센트 값도 정확하게 읽어주게 됩니다.

주의하실 점은 해당 대체 텍스트 안에는 숫자와 % 기호만 들어가야 하며 퍼센트에 해당하는 텍스트(예: 설문 진행률)가 화면에 표시된다면 해당 TextView를 labelFor로 연결하여 어떤 진행률인지 읽어주도록 할 수 있습니다.

[Android web] 버튼 요소에 ontouchstart 이벤트를 사용하면 TalkBack에서 이중탭해도 실행이 안 됩니다

Webacc NV | 2020-06-10 11:11:03

모바일 네이티브 앱과 마찬가지로 모바일 웹에서도 터치 이벤트를 구현할 수 있도록 ontouchstart, touchmove, touchend 등의 이벤트를 제공하고 있습니다.

그런데 버튼 태그에 터치 이벤트를 주게 되면 안드로이드 TalkBack 사용자는 이중탭해도 터치 이벤트가 실행되지 않습니다.

이는 버튼 요소를 사용하는 순간 접근성 API에서는 클릭하는 요소로 인식하고 있어서 이중탭이 클릭으로 동작하기 때문입니다.

물론 p 요소와 같은 곳에 터치 이벤트를 주게 되면 접근성 API에서 클릭 요소로 인식하지 않기 때문에 이중탭하면 터치 이벤트가 실행됩니다.

사실 이것은 웹뿐만 아니라 앱에서 버튼 요소에 onTouchListener 이벤트를 사용해도 마찬가지 결과가 나옵니다.

따라서 스크린 리더 사용자를 고려하여 터치 이벤트는 드래그와 같이 꼭 필요한 곳에서만 사용해야 하며 단순히 버튼을 실행하는 등의 요소에는 사용하지 않도록 주의가 필요합니다.

온터치 이벤트 샘플(모바일에서 테스트)

[iOS web] 숨김 텍스트에 민감해요!

Webacc NV | 2020-06-09 11:50:05

숨김 텍스트(Image Replacement)은 스크린 리더 사용자를 위해서만 제공하는 보조 텍스트 정보를 숨김 처리하는 기법으로 줄여서 IR라고 부르며, 단순하게 숨김 텍스트라고도 부릅니다. 수많은 웹 개발 라이브러리에서 이 IR기법용 클래스를 제공하고 있는데, 대표적으로 Bootstrap의 sr-only 클래스가 있으며, ir이나 blind 등의 클래스로 이를 제공하고 있습니다.

IR 기법은 말 그대로 기법이기 때문에 텍스트를 숨기는 여러 가지 방법을 담고 있습니다. 많이 알려진 기법으로는 text-indent를 큰 음수값으로 제공하여, 화면 밖으로 텍스트를 빼 버리는 방법, text-indent처럼 position을 absolute로 제공하고, 화면 밖으로 left와 top 속성을 통해 내보내는 포지셔닝 방법이 있습니다.

이 두 개의 방법은 분명히 스크린 리더가 읽을 수 있는 숨김 텍스트를 제공하지만, 화면 밖으로 텍스트를 빼는 것이기 때문에 의도하지 않은 X축 스크롤이 생기거나, 모바일 스크린 리더의 초점이 해당 텍스트를 감쌀 경우, overflow와 관계없이 x축 스크롤이 이동되는 단점도 명확하며, 구글의 검색 엔진에서는 스팸으로 분류되는 등의 단점도 있습니다. 또한, 해당 요소의 초점이 엉뚱한 곳에 나타나 미관상으로도 보기 좋지 않습니다.

 

탐색 중 아이폰이 제 멋대로 숨김 요소를 건너뛰어요!

위 두 가지 방법은 그나마 나은 편에 속합니다. PC 스크린 리더와 안드로이드의 스크린 리더는 읽을 수 있지만, iOS의 아이폰에서는 중요하지 않은 콘텐츠로 인식돼 읽지 않고 건너뛰는 현상을 발생시키는 기법도 있습니다.

바로 크기 0을 사용하는 기법입니다. font-size, width, height를 0, overflow:hidden으로 렌더링한 요쇼는 iPhone의 VoiceOver에서 마치 img 태그의 alt를 빈 값으로 두는 것과 같이 읽지 않는 현상이 발생하게 됩니다.  마치 CSS에서 display:none, visibility:hidden으로 숨기거나, HTML 태그에서 aria-hidden="true"를 쓴 것 같이 말이지요.

iOS의 VoiceOver에서 요소를 읽을 때, 터치할 수 있는 1px 이상이여야 올바르게 동작합니다. 따라서, iPhone의 VoiceOver 를 사용하는 사람을 고려하기 위해서는 반드시 이 기법을 사용해서는 안 됩니다.

그렇다면 iOS, iPad OS를 위해서는 어떠한 방법을 사용해야 하나요?

clip을 사용한 1px 크기의 숨김 요소를 사용하는 것이 가장 바람직합니다. CSS Tricks - 숨김 텍스트 기법 박물관의 첫 번째 항목인 2014년에 H5BP가 채택한 방법입니다. 이 방법을 사용하면 입맛이 까다로운 iOS에서도 잘 읽는 숨김 텍스트를 제공할 수 있습니다.

부트스트랩의 sr-only에 사용되는 기법도 이 기법이며, 별도의 작업이 필요하지 않다면, 부트스트랩을 이용하여 간편하게 iOS를 고려한 숨김 텍스트를 제공할 수 있습니다.

 

누를 수 있는 요소에 대한 숨김 텍스트에 대하여

버튼과 같이 누를 수 있는 요소에 대해서는 용도에 따라서 ir기법보다는 aria-label을 사용하면 더 손쉽고 깔끔하며 미관을 해치지 않는 디자인을 제공할 수 있습니다. aria-label은 센스 리더에서도 지원하며, 대부분의 스크린 리더에서 잘 읽습니다.

특히 커스텀 웹 미디어 플레이어 등에서 사용되는 재생/정지 버튼과 같이 아이콘으로 되어있는 버튼에는 aria-label을 사용하는 것이 편리합니다. 다만, WAI-ARIA는 접근성의 부족한 부분을 채워주는 보조적인 기술일 뿐, 주가 되어서는 안 되며, aria-label은 요소 내의 텍스트를 덮어씌워 버리므로 아이콘 또는 요소에 텍스트가 존재할 경우, aria-label에도 동등하게 텍스트를 제공해야 합니다.

 

 

[Mobile web] HTML의 lang 속성에 더 예민합니다

Webacc NV | 2020-06-09 09:11:57

iOS android에서 스크린 리더 사용자가 웹페이지 혹은 웹뷰를 탐색할 때 난감한 것 중 하나가 한글을 영문 TTS로 읽는 경우가 종종 있다는 것입니다.

이것은 HTML에서 lang 속성이 없거나 "en"으로 잘못 마크업했기 때문인데요. 

PC 스크린 리더의 경우에는 비교적 자동 언어 감지에 대한 설정을 사용자가 커스터마이징할 수 있거나 TTS가 여러 개 설치되지 않은 경우에는 영향을 받지 않는 반면

모바일에서는 기본적으로 여러 TTS가 설치되어 있어 언어 명시에 대한 영향을 더 크게 받게 됩니다.

따라서 모바일 웹페이지(앱 내에 포함되는 WebView 포함)의 정확한 주 언어 명시는 접근성을 고려하기 위한 필수 조건 중 하나라고 할 수 있겠습니다.

[Android native] 초점을 특정 요소로 보내야 할 때는 sendAccessibilityEvent를 사용하세요

Webacc NV | 2020-06-05 17:50:09

웹페이지와 마찬가지로 안드로이드 앱에서도 상황에 따라 접근성 초점을 특정 요소로 보내 주어야 하는 경우가 발생합니다. 

대표적인 예가 신청서 등을 작성하는 화면에서 특정 입력 필드의 값이 유효하지 않거나 혹은 레이어를 닫았을 때의 포커스 처리를 해 주어야 할 때 등입니다. 초점 처리를 해 주지 않으면 스크린 리더 사용자가 에러가 난 입력 필드를 찾기 어렵거나 혹은 레이어가 없어지면서 초점이 화면 상단으로 튀어 버리는 문제가 발생할 수 있습니다. 

초점을 다른 곳으로 보내주고자 할 때는 초점을 보내 주어야 하는 요소에 sendAccessibilityEvent 메소드를 사용할 수 있습니다. sendAccessibilityEvent 안에는 여러 옵션들이 있는데 초점을 보내줄 때는 TYPE_VIEW_FOCUSED 옵션을 사용하면 됩니다.

[iOS native] 음성 검색 구현할 때 VoiceOver 소리 들어가지 않게 하기

Webacc NV | 2020-06-04 18:53:49

요즘에는 많은 앱에서 음성검색을 제공하고 있습니다. 음성 검색은 조금 더 빠르게 정보를 검색하거나 받아쓰기를 할 수 있어 시각장애인에게도 큰 도움이 됩니다.

문제는 음성검색을 실행하면 화면의 특정 내용을 자동으로 VoiceOver가 읽기 때문에 난감한 경우가 많다는 것입니다.

이것을 해결하려면 두 가지 경우의 수를 두고 생각을 해 보아야 합니다.

1. 음성 검색을 누를 때 화면이 전환되는 경우

2. 화면이 전환되지 않고 현재 화면에서 바로 마이크가 켜지는 경우

오늘은 1번에 대한 해결방안을 제시해 드리려고 합니다.

세 가지만 해 주시면 됩니다.

a. 음성 검색이 실행되면 포커스를 임의의 요소(예컨대 듣고 있어요 등)으로 보내기. 이 때는 screenChangedNotification 메소드를 사용합니다.

b. 해당 요소에 accessibilityLabel을 추가하되 "" 로 추가합니다. 그러면 포커스된 곳의 대체 텍스트가 없기 때문에 음성 검색을 하는 동안에 VoiceOver가 아무 말도 하지 않게 됩니다.

c. 마지막으로 효과음이 출력되지 않는다면 VoiceOver가 켜졌을 때만이라도 시작음과 종료음이 출력되도록 하며 이때는 isVoiceOverRunning 메소드를 사용합니다.

두 번째 방안에 대해서는 다음에 설명하겠습니다.

이 역시 구체적인 구현 방법은 블로그 포스팅을 통해 올리도록 하겠습니다.

[iOS native] TabBar trait에 관하여

Webacc NV | 2020-06-03 14:53:42

안녕하세요.

iOS 네이티브 앱을 개발하다보면 접근성을 구현하기 위하여 accessibilityTrait 속성을 사용하는 경우가 종종 있습니다.

Trait는 커스텀 view로 기능을 구현해야 할 때 각 요소가 가진 정확한 의미를 VoiceOver에 전달하는 역할을 할 수 있도록 하는 것으로 HTML과 비교하자면 WAI-ARIA와 비슷하다고도 할 수 있습니다.

그런데 많은 분들이 trait 중에서도 TabBar trait가 있다는 것을 잘 모르시는 것 같습니다. TabBar 구현 시에 네이티브 컨트롤을 사용할 수 없을 경우에는 TabBar trait를 사용해서 네이티브 TabBar와 똑같이 VoiceOver에서 음성 출력할 수 있도록 할 수 있습니다. 

자세한 구현 방법은 이후에 발행될 널리 블로그를 참고해 주세요.

감사합니다.

접근성을 고려한 표를 기획하기~!!

에어류 | 2020-06-02 16:18:29

안녕하세요! 에어류입니다.

많은 분들이 웹사이트를 기획할 때 국내에서는 게시판, 표와 같은 규격적인(?) 기획을 많이 해왔습니다.

과거 우리는 테이블 형태의 웹사이트를 만들어 위치 기반으로 콘텐츠를 집어넣곤 했었습니다.

그러나 웹 접근성을 이해하시는 분들은 좀 달라져야 합니다. 충분히 제목으로 사용할 수 있는 부분도 다단으로 또는 병합된 형태로 표를 사용할 때가 참 많습니다. 웹 접근성을 위해 되도록 제목으로 분리할 수 있는 부분은 제목으로 분리하거나 여러 제목셀로 만들어진 표의 제목셀의 단수를 줄여주시면 보조기기에서 표를 읽어나갈 때 쉬워지니 참고해보시면 좋겠습니다.

제목셀이 3단으로 구성된 표를 2단으로 줄여 기획한 표 예시

또 가능하다면 표에 넣지 않고 표의 제목, 즉 캡션을 마크업으로 제공해주신다면 더욱 접근성을 높이는 기획이 될 수 있겠습니다.

표안의 제목을 표 밖에 제목으로 제공한 표 예시

자. 이제 우리가 실천할 때입니다. ^^

 

iOS 모바일앱 스크린리더 사용 시 닫기 버튼 제공

하루 | 2020-05-27 14:50:25

Android 단말기는 보통 소프트웨어 버튼이나 하드웨어 버튼으로 이전 버튼이 제공됩니다.
반면 iOS는 이전 화면으로 돌아가기 위한 버튼이 없어 모바일 스크린리더(VoiceOver)를 사용할 때 이전 화면으로 돌아가기에 어려움이 있습니다.

VoiceOver를 사용할 때 레이어팝업으로 펼쳐지는 메뉴에 닫기 버튼이 없으면 홈화면으로 되돌아가지 못하게 됩니다.
사실 아이폰에서는 '두 손가락으로 문지르기' 제스처로 알림을 닫거나 이전 화면으로 돌아갈 수 있는 기능을 제공하고 있습니다.
이 제스쳐를 활용하면 이전 화면으로 돌아갈 수 없는 문제를 해결할 수 있는 것으로 보입니다.

다만 이 제스쳐가 보편적인 제스쳐인지 고민해봐야 합니다.
접근성이란 특별한 지식 없이도 이용이 가능해야하므로 해당 기능이 보편적인 경우에만 적용하는 것이 적절합니다.

가장 좋은 방법은 닫기 버튼을 제공하는 것입니다.
하지만 불가피한 경우에는 해당 제스쳐를 대체수단으로 사전에 안내해주는 것이 가장 확실한 접근성 적용이라고 할 수 있습니다.

'닫기'버튼과 제스처 제공에 대해 정리하면 4가지의 제공 방법이 있습니다.
  1) ‘닫기’버튼과 ‘Z 제스처 안내’ 동시 제공
  2) ‘닫기’버튼 제공
  3) 해당 페이지에 진입 시 “Z 제스처를 사용하여 팝업을 닫을 수 있 습니다." 와 같은 안내 제공
  4) 앱내 ‘접근성 안내’ 페이지에서 제스처 동작에 대해 안내 제공

전문가 진단과 사용성을 고려하여 '닫기'버튼을 기본적으로 제공하고, 
이를 대체할 수 있는 '제스처'를 함께 안내해주신다면 가장 좋은 접근성 적용이 되겠습니다.

 

이외에도 다양한 VoiceOver 제스쳐를 확인할 수 있는 링크 공유해드립니다.

https://support.apple.com/ko-kr/guide/iphone/iph3e2e2281/13.0/ios/13.0

컨트롤 내 SVG 요소의 초점(focus) 접근성 향상 방법

에어류 | 2020-05-27 11:15:49

SVG기반의 그래픽이나 아이콘 등의 경우 기본적으로 컨트롤 요소가 아니기 때문에 키보드가 접근하지 않습니다. 
다만 SVG 요소에 기능을 제공하기 위해 컨트롤 요소, 즉 버튼이나 링크 요소 안에 포함하여 사용할 경우 
SVG 요소에도 보이지 않는 초점이 적용되는 문제가 발생합니다. 

이 경우 button 요소에 보이는 초점 한번, SVG 요소에 보이지 않는 초점 한번, 총 2번의 초점이 적용되는 문제가 있습니다. 

스크린리더에서도 위 컨트롤을 탭키로 접근 시 ‘OO 버튼’ 컨트롤이라고 읽어주고
다음 탭키를 선택하면 ‘그래픽’이라고 중복으로 읽어주어 스크린리더 사용자에게 불편합니다.

위와 같은 초점 중복 문제를 해결방법을 공유합니다.

예시)
<button type=”button”>
       <svg width="100" height="100" focusable="false"></svg>
</button>

이렇게 수정하게 되면 버튼과 SVG기반 이미지에 두번씩 초점이 적용되던 것이 한번씩만 접근하게 되어
일반적으로 컨트롤에 초점이 이동하던 방식대로 초점 순서가 간결해지게 됩니다.

다양한 컨트롤 <a>, <input>, <select>, <textarea>, <button> 등에 SVG를 사용하시게 되면
꼭 해당 방법을 활용해보시기 바랍니다. 

W3C silver mailing list 공유합니다.

Nts Nuli | 2020-05-26 20:49:04

W3C에서 준비중인 WCAG의 다음버전. Silver의 메일링 리스트를 공유합니다.

기간별 논의중인 주제에 대한 메일 thread, author, subect 들이 정리되어 있습니다.

[W3C silber mailing list]

https://lists.w3.org/Archives/Public/public-silver/

iOS 모바일 앱 접근성 진단 시, 음성출력표시

레드스카이 | 2020-05-26 13:42:55

모바일 애플리케이션 접근성 진단을 위해 보통 2가지의 운영체제에서 진단을 하게 되는데
그 특성이 달라 동일한 방법으로 진단하기 어려웠습니다.

 

먼저 개방적인 운영체제의 특징을 가지고 있는 안드로이드 운영체제에서는
모바일 스크린리더인 토크백(Talk back)의 음성낭독 내용을 확인할 수 있는 방법이 존재하고 있습니다.

 

VoiceOver 의 음성출력표시 기능이 없었기 때문에 그 동안은 낭독 내용을 클립보드에 복사하였다가
타 애플리케이션을 이용하여 확인하는 방법으로 안드로이드 운영체제의 음성출력표시 기능을 대체해왔습니다

 

VoiceOver 의 음성출력표시 기능의 이름은 ‘자막패널’입니다.

VoiceOver의 자막패널 기능을 활성화해주고, 이를 안드로이드의 음성출력표시 기능과 같이 진단 시에 활용하면
보다 정확한 낭독내용과 함께 진단 기록으로 남길 수 있는 방법이 있어 공유합니다.

 

iOS : 설정 > 손쉬운 사용 > VoiceOver > 자막패널 활성화

자막패널활성자막패널 활성 결과

접근성 디자인을 통해 배운 5가지 교훈(5 lessons I learned by designing for accessibility)

Nts Nuli | 2020-04-27 16:11:15

18개월 전, 저는 Google의 Android 팀에  Android 접근성 디자인 첫 리더로 합류했습니다.
 이제, 여러분은 제가 접근성에 대한 엄격한 자격이 있어야한다고 생각할지도 모릅니다. 약 30억 대의 Android 기기에 대한 접근 가능한 경험을 디자인하는 책임을 맡기려면 말이죠.

 솔직히 말하면, 저는 색상 대비 비율과 탭 대상 크기를 넘어 접근성에 대한 지식이 거의 없었습니다.  저는 사회적 이익을 위해 (내 경력 전반에 걸쳐 열렬한 무언가를) 디자인에 대한 열정을 가지고 있씁니다. 슬프게도, 그 팀은 그 이상의 자격을 갖춘 사람을 찾을 수 없었습니다. 즉 공허한 열정이죠.  이는 세계에서 가장 큰 기술 회사 중 하나에서도 접근성 리소스가 얼마나 부족한지를 말해줍니다.

  기술 담당자에게 물어 보면 “아, 물론 접근성은 중요합니다. 그것은 옳은 일입니다." 라고 대답할 것입니다. 마감 시간이 다가 오면 같은 사람들이 “이봐요, 저는 그것을 성공시키고 싶지만, 제약조건을 고려했을 때, 접근성 같은 최신 사례를 수용할 수 없습니다”라고 말하면서 머리카락을 뽑을 것입니다.
제가 접근성 디자이너가 되기 전에 이러한 유형의 사고 방식을 포기해도 괜찮았습니다.  더 이상은 아닙니다.

제가 이 일을 하고 18개월 후에 배운 내용은 다음과 같습니다.

 1. 모든 사람이 어떤 방식으로든, 어떤 시점에서든, 장애인이 됩니다.

 “장애”라고 말할 때 가장 먼저 떠오르는 것은 무엇입니까?  대부분의 사람들에게는 아마도 영구적인 장애일 것입니다. 실명 (시각 장애), 청각 장애 (청각 장애), 휠체어를 타는 사람 (운동 장애) 또는 알츠하이머 또는 다운 증후군과 같은인지 장애 일 수 있습니다.
 WHO에 따르면 장애에 대한 정의를 영구 장애로만 제한하더라도 7명 중 1명은 장애인입니다. 그것은 세계에서 10억 명이 넘는 사람들입니다.

 하지만 장애는 그보다 훨씬 넓습니다.  큰 레스토랑에서 누군가와 대화를 하려고 했는데 배경 소음 이상으로 들을 수없는 적이 있습니까?  그것은 상황적 난청입니다. 종이에 손가락을 베인 며칠 동안 그 손가락을 사용하지 않았습니까?  일시적인 운동 손상입니다.

 모든 사람은 매일 상황적, 일시적 장애를 경험합니다.

  몇 가지 예 :
 • 운전 : 상황에 따른 시각 장애, 이동성 및 주의력 저하
 • 울퉁불퉁 한 버스 / 기차 탑승 : 상황에 따라 손재주 기능 저하
 • 여행 중 현지 언어를 구사하지 못하는 경우 : 언어적 의사 소통 장애
 • 쇼핑 중 가방을 휴대한 경우 : 상황에 따라 이동 장애
 • 눈 검사 후 동공 확대 : 일시적인 시력 장애
  • 뼈 부상으로 인한 캐스트(깁스) 착용 : 일시적인 이동 장애
 • 아침에 커피가 충분하지 않음 : 일시적인 인지 장애

 이러한 상황에서 정상적으로 신체를 가진 사람은 제품 (물리적 또는 디지털)을 사용할 때 영구적인 장애를 가진 사람과 동일한 제한을 경험합니다.
모든 사람들은 날이 갈수록 꾸준히 나이를 먹고 있습니다.  나이가 들어감에 따라 신체  능력과  인지 능력이 자연적으로 저하됩니다.  시각 장애가 있는 사람의 약 65%가 65세 이상입니다 (WHO, 2012).  오늘날 완벽한  20/20 시력을 가지고 있다고해서,  영원히 그런 상태를 유지한다는 의미는 아닙니다.

 기술 제품은 기술에 정통한 젊은 층에 중점을 두는 경향이 있습니다.  하지만, 우리가 사용자 중심의 제품 디자이너로서 노인을 고려하지 않는다면 필연적으로 늙었을 때 누가 우리를 돌볼까요?

  2. 접근 가능한 제품 = 모든 단일 사용자를 위한 더 나은 제품입니다.

 접근성을 염두에 두고 디자인한다는 것은 영구적인 장애가 있거나 상황에 따라 일시적으로 장애가 있는 사람이든 모두를 포함한다는 의미입니다.  손쉬운 사용은 형태, 형태 또는 엣지케이스를 구성하지 않으며, 100 % 사용자에게 영향을 미칩니다.
전형적인 현상은 curb cut (연석 절단) 효과입니다. 이는 장애인을 위해 디자인된 것들이 종종 모든 사람을 돕는다는 사실을 의미합니다.

 curb cut(연석 절단)은 길을 만나는 보도로 깎인 경사로입니다.  여러분은 아마 그것들을 실제로 눈치채지 못하고 본 적이 있을 것입니다. 이것은 휠체어를 탄 사람들이 쉽게 돌아다닐 수 있도록 법에 의해 의무화되어 있습니다.  하지만 수하물을 옮기거나 유모차 또는 자전거를 밀거나 스케이트 보드를 타는 사람들에게도 유용합니다.

curb cut(연석 절단) 효과의 다른 예는 다음과 같습니다.

•  타자기 : 원래 이탈리아 발명가 펠레그리노 토리 (Pellegrino Turri)가 맹인 여자 친구가 읽을 수 있는 연애 편지를 더 많이 쓸 수 있도록 디자인했습니다 (Bodine, Cathy. Assistive Technology and Science. 2013).

•  자막 : 원래 청각 장애인이 TV 시청을 돕도록 디자인 되었습니다.

•  이메일 : 인터넷을 디자인 할 때 빈트 서프 (Vint Cerf)가 자신의 청각 장애와 서면 의사 소통 (CNET)에 크게 영향을 받습니다.

 또한 접근성은 볼 수 있는 근본적인 문제를 증폭시키는 돋보기 렌즈입니다.  손떨림이있는 사람이 UI에서 특정 버튼을 탭하는 데 문제가 있는 경우, 사용하지 않는 사용자도 해당 버튼을 사용하려고 할 때 잘못 탭하는 경우가 많습니다.  학습 장애가 있는 사용자가 제품의 언어 또는 레이아웃을 이해하는 데 어려움을 겪는 경우, 장애가 없는 사용자도 이해하기 어렵고 복잡 할 것입니다.

 장애가 있는 사람들을 위해 문제를 해결하기 위해 시간과 자원을 바치면 모든 사람들이 그 혜택을 누리게됩니다. 솔직히 말하면 우리는 때때로 약간의 도움을받을 수 있기 때문입니다.


출처 : https://uxdesign.cc/5-lessons-i-learned-by-designing-for-accessibility-65842a74f849

음수값차트는 표현이 되지 않나요?

요화 | 2017-05-23 11:02:15

음수값까지 들어가는 것을 표현하고 싶은데 음수값은 적용하는 법이 나와있지 않네요 설정에 음수값을 넣으도 표현이 되지 않구요 음수값까지 포함되는 차트를 알고 싶습니다~

널리 라이브러리 질문드립니다.

네버리안 | 2017-05-19 10:54:16

제가 이번에 만든 제품에 널리 라이브러리를 사용하여 제품에 넣어서 가독성을 높이고 싶은데요.. 라이선스규정과 라이선스를 어디에다가 첨부해서 넣어야하는지 궁금합니다.

지금만든 웹프로젝트는 폐쇄망/네트워크 환경에서 사용될것이며 jsp 기반이구요 자바 이클립스 Gradle 빌드입니다.

DB는 postgresql 인데.. 이 DB에 있는정보를 ajax로 받아서 도넛모양하고 칼럼 차트를 사용할수있을까요??

궁금합니다.. 그리고 상용 소프트웨어에 들어갈거라.. 비용을 따로 내야되는지도... 궁금하네요..

css formatter 변환이 안되네요

aeo**** | 2017-05-17 10:38:18

안녕하세요. 평소 널리로 css코드정렬을 애용하고있는데요. 갑자기 소스변환이 안되네요.(2주전에는 사용했었는데...) 변환된다고 로딩바가 뜨지만 변환된 파일은 0k입니다. 답변과 조치 부탁드립니다.

감사합니다.

N-MET 'Sprites Generator' 의 내보내기가 작동하지 않습니다.

삼초 | 2017-05-10 10:56:06

안녕하세요. N-MET을 이전부터 유용하게 사용해왔던 퍼블리셔입니다. :)

다름이 아니라, N-MET의 Sprites Generator 의 내보내기 기능이 이번 주 월요일 오후부터 작동하지 않고 있습니다. 이전에도 동일한 문제로 널리 포럼으에 문의드린 적이 있는데, 이번에도 서버에 문제가 생겨 이미지가 생성되지 않는 것인지요? 혹시 이 문제라면 언제쯤 정상적으로 사용할 수 있을지 알고 싶습니다.

시간 나실 때 답변 부탁드립니다. 좋은 프로그램 만들어 주셔서 감사합니다.

Line Chart 조언 부탁드립니다.

jesse | 2017-04-05 18:00:08

alt

위 그림처럼 Line Chart 를 활용해서 추이 그래프를 만들고자 합니다.

좌우로 스크롤? 스와이프? 해서 해당 날짜의 데이터를 볼려면 어떻게 하면 좋을까요?

조언을 부탁드립니다.

네이버 검색 - 서브메뉴

IT | 2017-03-17 09:40:39

안녕하세요.

alt

네이버 검색 시 사이트 영역에서 노출되는 서브링크를 등록하려고 하는데요.

네이버에서 제공하는 "네이버 웹마스터도구"에서 요구하는 사이트맵, RSS 등 다 올렸어요.

등록방법이나 신청을해야 한다면 신청방법 부탁드립니다.

자동으로 수집한다면 룰에 대해서도 알려주세요.

외부데이터 활용관련 질문드립니다.

너굴샷 | 2017-03-16 10:43:19

기존에 값을 직접 입력해서 테스트를 해보았는데요, 아래는 테스트한 코드구요..

var options = { legend:{ names: [3.09, 3.10, 3.11, 3.12, 3.13, 3.14, 3.15], hrefs: [] }, dataset:{ title:Playing time per day, //차트의 제목 values: [[9], [13], [16], [11], [16], [9], [4]], //각각의 데이터 colorset: [#30a1ce], // 라인컬러 fields:[] //범주 }, chartDiv : chart, // 차트 div 선택 chartType : line, // 차트 타입 chartSize : {width:600, height:300}, // 차트 크기 minValue : 0, // 차트 최소값 maxValue : 30, // 차트 최대값 increment : 5, // 차트 y축 범위 증가량 isGuideLineNeeded : false // 가이드라인 사용 유무 설정 }; Nwagon.chart(options);

이런식으로 직접입력하면 라인차트가 생성이되는데 위에서 직접입력을 하지 않고 만약 외부 json파일의 데이터를 파싱 후 그 값은 변수에 저장, 그 변수들을 위에 dataset에 입력이 가능한가요? 외부로부터 받은 값을 차트로 그려주고싶은데 그부분이 가능한지 궁금해 글써봅니다..

옵션설정중 질문드립니다.

너굴샷 | 2017-03-16 09:33:34

line 차트를 테스트용으로 구성해보는중에 dataset의 title부분이 차트의 타이틀설정이라고 나오는데 이 title을 설정한 텍스트는 어디에 표출이되는건가요? 옵션을 설정해도 어디에 쓰이는건지 찾을수없어서 질문드려봅니다..

모바일 작업하실대 psd 해상도 어느 사이즈로 작업하시나요??

김종성 | 2017-03-14 15:36:08

지금 1080*1920 해상도 psd를 받아서 퍼블리싱 중인데

다른 분들은 psd 어느 해상도를 받아서 작업하시는지요?

웹접근성 법률 적용

보리 | 2017-03-06 16:46:33

안녕하세요.

서비스 사업자가 국내 사업자이면, 국내가 아닌 해외 서비스만 하더라도 국내법에 적용해서 웹접근성 작업 진행을 하는게 맞나요? 아니면 해외(미국)법에 맞게 적용을 해야하나요?

textarea에 title 사용

koc**** | 2017-02-14 11:08:53

안녕하십니까.

접근성 프로젝트를 처음 접해 보고 있습니다. 자세한 내용도 몰라서 접근성 관련 교육 동영상을 보며 공부 하고 있습니다.

제 질문 내용은 다음과 같습니다.

<div>
    <textarea (다른 속성들 생략) title="채팅내용 입력strong text" placeholder="메시지를 입력하세요.">
</div>

이런식으로 사용하고 있는데요.

레이블이 시각적으료 표현되지 않을 경우에는 label 을 숨김처리 하거나 title을 사용할수 있음

이라는 접근성 관련 내용을 봤습니다.

여기서 레이블 이라는 것은

닉네임 [ 인풋 타입 텍스트]

이런 구조에서 닉네임 부분레이블 인가요?

저런 레이블이 없으면 title을 사용해도 된다는 건가요?

그러면 위에서 제가 textarea에 사용한 title 은 이상이 없는걸까요..?

네이버 모바일웹 로그인 페이지 오류 문제 질문드립니다.

rime | 2017-02-13 17:55:34

네이버 홈페이지를 아이폰으로 크롬이나 사파리로 들어갔을때 (안드로이드 폰으로는 테스트 해보지 못했습니다.

아래 동영상같은 문제가 발생합니다. input 창에 값 입력후 지울때

마지막 값을 지우게 되면 공백이 발생하는 버그가 발생하게 됩니다.,

동영상 링크 : link

혹시 이문제에 대한 원인을 알수 있을까요?

제가 구현하고 있는 사이트도 네이버와 같은 문제점이 있는데.. 혹시 해결방안이 있나 해서 질문드립니다.

차트 label 줄바꿈 질문입니다. (개행)

카인대학일 | 2017-01-23 15:16:09

legend:{ names: xxxx, xxxxx, ccccc

이런식으로 차트의 라벨을 선언해두고 사용하는데요. 문제는 라벨의 이름이 xxxxxxxx xxxxxxxxx, cccc 이렇게 길어질 경우 앞의 라벨 string이 뒤의 라벨 string을 가려버립니다. 이를 해결하기 위해 앞의 라벨에 공백에서 줄바꿈을 하면 해결 될 것 같은데요. 어떻게 하면 좋을까요??

xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
xxxxxxxx xxxxxxxx<br>xxxxxxxx

시도해본 목록입니다.

multi_column 챠트에서 배열 사용하기

헐랭이 | 2017-01-21 15:38:26
    <div id="chart18"></div>
<script>
    var options = {
        legend:{
            names: [1월, 2월, 3월, 4월, 5월, 6월, 7월, 8월, 9월, 10월, 11월, 12월],
            hrefs: []
                },
        dataset:{
            title:Playing time per day, 
            values: [[51, 17], [62, 45], [25, 52], [32, 45], [52, 15], [82, 51], [72, 58], [32, 25], [42, 52], [12, 15], [28, 52], [32, 15]],
            colorset: [lue, 
ed],
            fields:[신규, 해지]
            },
        chartDiv : chart18,
        chartType : multi_column,
        chartSize : {width:700, height:600},
        maxValue : 100,
        increment : 5
    };

    Nwagon.chart(options);
</script>

상기 코드의 names를 보시면 1월부터 12월까지 반복이 사용됩니다. 이런 경우 names안에 for문을 이용한 구문이 들어갈 수 없는지요? 예를 들면 for(v=1;v<=12;v++){ v; }

이런 형태로는 넣을 수 없나요? 1일부터 31일, 2010년부터 2016년도 도 마찬가지일 듯 한데...

nwagon 다각형 그래프 사용

바꾸기 | 2017-01-09 13:39:55

다각형 그래프 만들어 질때 시계방향으로 치면 3시방향에 있는게 start로 제일 첫번째 꺼잖아요

그걸 12시 방향으로 해서 시계방향으로 그래프가 생성되게 할라면 js 뭘 수정해야 하나요?

그러니깐 제가 가,나,다,라,마 {10,20,30,40,50} 이렇게 만들었을때

가는 오른쪽 끝인 3시방향에 나타나잖아요 이 시작 부분을 12시 방향 제일 위쪽으로 해서 만들고 싶어요

이미지 첨부 한거 보면 Perceivable가 제일 처음이예요 이걸 제일 위로 해서 반듯한 오각형을 만들고 싶어요

사각형이나 육각형은 모양이 반듯한데 오각형이나 삼각형 같은 경우 삐뚤어져서 보기 흉해요...

alt

모바일 페이지에서 첫화면부터 모바일 키패드가 보이게 가능한가요??

김종성 | 2016-12-21 18:49:21

모바일에서 index.html에 들어가자마자 input에 focus가 있으면서 키패드가 보여지게 가능한가요??

MAC에서 브라우저별 나눔고딕 font-weight 렌더링이슈

ggc3**** | 2016-12-18 16:04:11

OS : macOS Sierra(10.12.1)
테스트 브라우저 :
     1. Chrome (버전 55.0.2883.95 (64-bit))
     2. Safari (버전 10.0.1(12602.2.14.0.7))
     3. FireFox (버전 50.1.0)
     4. Opera (버전 41.0.2353.69 /)
이슈내용
    mac에서 나눔TTF를 설치하고 font-family:"나눔고딕", "NanumGothic"으로 선언할 경우 chrome/opera 브라우저에서 font-weight가 적용되지 않는 현상.
*네이버에서 맥용TTF 나눔고딕을 설치하여 서체관리자에는 나눔고딕(폰트페밀리) Regular/Bold/ExtraBold 3가지 굵기의 폰트가 설치되있습니다. 단, 서체관리자에서 Regular서체를 제외한 나머지 폰트를 삭제 후 확인하면 normal/bold 두가지 font-weight가 정용됩니다.
나눔OTF를 설치하고 font-family:"NanumGothicOTF"로 선언해주면 문제없이 적용됨.

css
    p{font-family:나눔고딕, NanumGothic;font-size:40px;line-height:.5}
마크업
<p style="font-weight:100">나눔고딕abcdef ABCDEF 1234567890 font-weight:100</p>
<p style="font-weight:200">나눔고딕abcdef ABCDEF 1234567890 font-weight:200</p>
<p style="font-weight:300">나눔고딕abcdef ABCDEF 1234567890 font-weight:300</p>
<p style="font-weight:400">나눔고딕abcdef ABCDEF 1234567890 font-weight:400</p>
<p style="font-weight:500">나눔고딕abcdef ABCDEF 1234567890 font-weight:500</p>
<p style="font-weight:600">나눔고딕abcdef ABCDEF 1234567890 font-weight:600</p>
<p style="font-weight:700">나눔고딕abcdef ABCDEF 1234567890 font-weight:700</p>
<p style="font-weight:800">나눔고딕abcdef ABCDEF 1234567890 font-weight:800</p>
<p style="font-weight:900">나눔고딕abcdef ABCDEF 1234567890 font-weight:900</p>
<p style="font-weight:lighter">나눔고딕abcdef ABCDEF 1234567890 font-weight:lighter</p>
<p style="font-weight:normal">나눔고딕abcdef ABCDEF 1234567890 font-weight:normal</p>
<p style="font-weight:bold">나눔고딕abcdef ABCDEF 1234567890 font-weight:bold</p>
<p style="font-weight:bolder"><span>나눔고딕abcdef ABCDEF 1234567890 font-weight:border</p>

Chrome 화면 Chrome 캡처화면

Opear 화면 Opera 캡처화면

font size 여백 문제 어떻게 하시나요

김종성 | 2016-12-12 12:04:10

보통 폰트를 쓰면 폰트위에 여백이 살짝 공간이 잡혀서 일반 폰트 크기보다 더 크게 잡히던데

이런부분들을 해결하시나요??

마우스 오버시 값이 안나타니지 않고 검정바탕이 뜨는현상

똥만이 | 2016-12-06 10:03:55

모든 그래프에 마우스 오버시 값이 나타나지 않고 검정바탕이 뜨는 현상이 일어나서 문의 드립니다.

가로 막대 차트는 어떻게 만드나요??

유준아빠 | 2016-12-02 14:51:41

가로 막대 차트는 만들수없나요? 차트 종류를 봐도 세로는 있는데 가로 막대 차트는 보이지 않아서요 답변 부탁드립니다 수고하세요.

파이차트 관련하여 문의드립니다

로체스터 | 2016-11-09 10:22:10

파이나 도넛차트에서 툴팁이 아닌 영역안에 퍼센티지 숫자를 넣을수 있는 옵션이 있을까요?

Nwagon option정리해놓은것은 없나요?

쳐부수자북괴군 | 2016-11-08 11:39:06

네이버에서도 멋진 오픈소스 차트가 나왔네요!

이번에 한번 적용해보려 하는데 구글차트처럼 옵션 및 API 정리해놓은곳은 어디있나요? 안보이네용..

line차트에 대해 문의드립니다.

카드값줘 | 2016-10-28 13:46:05

line차트를 사용중인데 그래프 값마다 나오는 점들을 지워서 하나의 선으로만 만들순 없나요??

Nwagon donut chart 질문입니다.

ih**** | 2016-10-05 14:30:01

현재 도넛 차트 커스텀 중니다. 궁금한 점이 차트에 마우스 오버 시 차트에 관한 데이터 들이 나오는데 아래와 같은 그림의 영역을 따로 제어하는 방법은 없는가요?

alt

그리고 차트 만드는 레이아웃 자체가 고정인데 유동적으로 사이즈에 맞게 변하는 레이아웃은 못잡는건가요?

차트 툴팁 관련하여 문의드립니다.

Kim Smc | 2016-10-04 18:08:18

http://nuli.navercorp.com/forum/post/68

목록을 살펴본 결과 이전에도 같은 문의를 하신 분들이 몇몇분 계신데 정확한 답을 못 얻은것 같아서 다시 문의드려요..

저는 라인차트를 쓰고있는데 마우스오버시에만 툴팁이 보여지게 되어있습니다.

그런데 아예 처음부터 각 포인트에 해당 값이 노출되도록 하고싶은데 어디 어떤 함수를 건드려야 할까요??

자체적으로 스크립트로 만들어 줘야한다는 글을 보았는데 어디다 만들어야할지를 모르겠습니다..

답변 부탁드려요.감사합니다.

Nwagon_no_vml 파일의 쓰임세?

똘이맘 | 2016-09-23 09:48:30

익스플로러 하위버전에 쓸수 있는 그래프를 검색하다 오늘 발견했습니다.

다운로드 후 파일을 보니 Nwagonnovm.js가 있던데

요파일은 언제 어떻게 쓰는지 궁금합니다.

그래프 x, y축에 단위 세팅방법 문의드립니다.

kal**** | 2016-09-21 12:27:13

아래 그림처럼... x축에 일, y축에 원 이러식으로 값을 넣을 수 있을까요?

alt

차트 컬러를 자동으로 결정하기?

rarose | 2016-09-08 16:09:17

차트 컬러를 자동으로 결정하는 좋은 방법이 있나요? 랜덤하게라도..

각 차트들 타이틀은 왜 표시가 안된나요??

구름 | 2016-09-06 10:50:45

소스내부에는 들어가있는거 같은데

HTML에서 표시가 안되네요 방법이 없나요??

적절한 제목 그 올바른 방법은

af**** | 2016-08-24 15:08:53

xhtml 독타입으로 웹접근성 사이트를 마크업할 때 <section>과 <aside>를 사용할 수 없습니다. 참고로 section과 aside는 컨텐츠 블록(outline)을 생성해 줍니다. 그래서 헤딩태그를 자유롭게 사용할 수 있습니다. 그렇지 않다면 헤딩태그의 레벨에 신경을 많이 써야 하죠.

그런데 xhtml에서 적절한 제목을 제공하기 위해서 헤딩태그를 사용할 때 여러분은 어떠한 기준으로 마크업을 하시나요? 문서의 중요도에 따라 헤딩태그의 레벨을 결정하시나요? 아니면 문서의 구조를 기준으로 마크업 하시나요? 제가 의문점이 든 것은 만약 gnb에 <h2>로 마크업을 하였고 메인 컨텐츠의 어떠한 영역에 또 <h2>로 마크업 했을때 과연 잘못된 것인가 하는 것이었고 로고에 <h1>으로 푸터쪽에 <h1>으로 마크업을 하였다면 이 역시 잘못된 것인가 하는 겁니다.

여러분의 의견이 궁금합니다.

웹 접근성 오류

마이구미 | 2016-08-16 10:07:46

웹 접근성 테스트 할려고 했는데 아래와 같은 텍스트가 떴습니다. You can check following schemas only: "http://", "https://"

해결 방안이 어떻게 될까요?

아래 테스트 링크입니다. https://livere.com/

모바일 3d 기법 배너

뽀로롱 | 2016-08-12 16:48:46

네이버 모바일 메인페이지를 보는데

광고배너중 터치슬라이드를 할때

위아래로 3d처럼 움직이는 배너를 본적이 있는데요

그걸 무슨기법이라고 하나요?

구현 방식은 무엇인가요???

Single Column Chart 문의

r*** | 2016-08-10 15:34:04

Single Column Chart에서 maxValue 값 이상의 값이 입력됐을 경우 alt

이미지의 두번째 막대그래프처럼 표시됩니다. 딱 maxvalue 만큼 그려지게 할 수 있는 방법이 있을까요?

또 tooltip에 글자 표시 가능한지 가능하다면 방법은 무엇인지 알고 싶습니다.

왜 나눔 폰트가 ie8에서 깨져나올까요.

af**** | 2016-08-01 18:58:16

네이버에서는 폰트가 안깨지고 잘 나오는데 왜 제가 만든 사이트의 웹폰트만 IE8에서 깨지는 지 모르겠네요. 왜 그런걸 까요.

웹폰트 적용방법 어떤게 올바른 방법일까요.

af**** | 2016-08-01 15:53:31

웹폰트 적용방법은 크게 두가지가 있는데요. 하나는 아래방식이고

@font-face { font-family: NanumBarunGothic;

src: url(../webfont/NanumBarunGothic/NanumBarunGothicWeb.eot);

src: url(../webfont/NanumBarunGothic/NanumBarunGothicWeb.eot?#iefix) format(embedded-opentype),url(../webfont/NanumBarunGothic/NanumBarunGothicWeb.woff) format(woff), url(../webfont/NanumBarunGothic/NanumBarunGothicWeb.ttf) format(ruetype); }

그리고 나머지 하나는 특수문자를 이용해서 사용하는 방법입니다.

@font-face{ font-family:nanum;

src:url(fonts/NanumGothic.eot);

src:local(※),url(fonts/NanumGothic.woff) format(woff)

}

코딩하는 입장으로써 이 두 코딩에서 가끔 햇갈리더군요. 제가 알기론 아래가 폰트 포멧을 브라우저에 따라 다운받게하는 최적의 코드라고 알고 있는데요. 여러분의 생각은 어떠신가요?

html5에서 사라진 table의 summary 속성과 웹접근성, 웹표준

af**** | 2016-07-05 16:49:36

html5에서 table의 summary 속성이 사라졌습니다. 웹표준 검사를 하면 오류로 표시가 됩니다.

그렇다면 html5로 웹접근성을 고려하여 마크업을 할때 summary 속성을 작성하지 않는게 맞는건가요? 아니면 작성해야 되나요. 만약 작성하지 않아도 된다면 어떻게 작성해야 되는지 궁금합니다.

line 차트도 multi가 아닌 한 가지의 값만 받고 싶어요 ㅠㅠ

mi**** | 2016-06-18 22:32:20

line 차트의 예시에서는 이차원 배열의 형식으로 값을 뿌려주고 있는데요, 저는 한 가지 종류의 값만 받아와서 일차원 배열의 형식으로 뿌려주고 싶어요. column 차트는 멀티와 싱글 차트가 따로 제공되고 있던데 line은 그게 아니라서 좀 어렵네요 ㅠㅠ 혹시 어떤 부분을 어떤 식으로 고쳐야 할 지 알려주실 수 있을까요?

charset이 html과 css가 다를경우

| 2016-06-08 20:34:36

얼마전에 html 은 utf-8 css는 euc-kr로 작업된 산출물을 관리하다가 ie에서 css가 적용이 되지 않는 현상을 발견한적있습니다. 당연히 동일하게 맞춰야하지만 다르게 될 경우 이런 현상이 나타나는 정확한 원인이 궁금하네요~

css에서 charset utf-8?

af**** | 2016-06-07 11:30:57

한글폰트명이 잘 인식되기 위해 @charset "utf-8"로 선언합니다. 그러면 파일 인코딩인 euc-kr일 경우 @charset "utf-8"로 선언 해도 되나요? 아니면 파일 인코딩에 맞춰서 euc-kr로 선언해야 되는 것인가요? 만약 euc-kr로 선언해야 된다면 한글폰트명 인식에는 문제가 없는지 궁금합니다. 조언 부탁드립니다.

Column Chart div 여백 질문

오죠사마 | 2016-05-19 19:17:31

alt

Nwagon을 사용하는 학생인데요 div에 border를 주어서 확인해보았더니 위쪽 여백이 너무 많아서 혹시 차트를 div의 top까지 올릴 방법이 있을까요?

csv파일로 부터 데이터를 읽어 차트를 구현해 보고 싶은데요

가영아빠 | 2016-05-11 18:16:41

csv파일을 읽어들여 chart로 나타내는 기능은 없나요? 검색해도 나오질 않아 문의 드립니다.

차트 툴팁

snrnsir**** | 2016-04-30 20:11:08

차트 라이브러리 소개에

Nwagon의 라이브러리는 자동으로 각각의 꼭지점에 마우스 이벤트를 생성하여 마우스 오버시에 각각의 항목에 대한 정확한 수치를 툴팁형태로 나타내 준다.

라고 나와있는데 아무리 마우스오버를 시켜봐도 툴팁이 생성되지 않아서요.

예제는 radar와 column 두가지 차트 소개에 나와있는 예제 그대로 사용하였습니다.

두 차트 모두 툴팁이 생성되지 않는데 자동으로 생성되는것이 아닌가요 ? 아니면 제가 무언가 잘못한걸까요 ?

레이더 차트 크기 질문

Artifex | 2016-04-28 11:52:12

함수 내에 넓이 높이가 있기는 한데, 그걸 건드리면 차트의 크기가 줄어드는 게 아니라

차트를 보여주는 넓이 높이가 줄어들더라고요!

이렇게 말이죠

어떻게 해야 차트 자체의 크기를 늘이고 줄일 수 있나요?

line Chart에서 legend 표시

천하무적 | 2016-04-28 01:06:44

안녕하세요. 궁금한게 있는데, line 차트를 불러와서 표시하는데 legend 수가 많아질 경우 하단의 label이 중복되게 나와서 알아 볼 수가 없는데요, label을 보이지 않게하는 것은 drawLineForeground 내에 Nwagon.line.drawLabel 부분에서 labels.appendChild(text) 을 제외하여 전체 label을 표시하지 않게는 알겠는데, 처음과 끝 부분만 label이 출력하게끔은 설정 못하나요?

var text = Nwagon.line.drawLabels(px + cw/2, 15, names[i], false, 0) 에서 i값 0과 data.length-1 이 처음과 끝 데이터를 받아오는 것으로 보이는데, 반복문 내에서 labels.appendChild(text)이 있다보니 전체를 출력하는것 처럼 보이네요..

제가 잘못 파악한건지 확인 부탁드릴께요 감사합니다.

radar 차트 데이터 표현 문의입니다.

직진이다 | 2016-04-18 09:54:14

현재 포인트에 오버시만 데이터가 표현 가능합니다.

마우스 오버가 아닌 경우에도 데이터를 표현 하고 싶습니다.

답변 부탁드립니다.

멀티 차트는 지원하지 않나요?

정주Yang | 2016-04-11 14:31:30

예제에 보니 단일차트만 예제로 되어 있던데 멀티차트는 지원하지 않는 건가요? 예를들어 area와 line이나 line와 Column의 조합처럼 동시에 한차트로 보여주는 것 처럼 말이죠.

널리 세미나 자료 언제쯤 올라오나요?

Hwayoun Lee | 2016-04-06 09:09:03

어제 세미나 잘 들었습니다. 모바일, 웹 이제 시작하는 저에게 뜻깊고 좋은 시간이었습니다.

잊어버리기 전에 어제 들었던 내용들.. 다시 보고싶은데요 언제쯤 공개될까요?

N_MET 안됩니다. ㅜㅜ

| 2016-03-09 18:10:26

스프라이트 이미지 만들고 json 저장이 안됩니다. ㅠㅠ

Nwagon은 더이상 버전업 되지 않는건가요?

Hun Yong Song | 2016-03-03 14:11:56

안녕하세요.

Nwagon에 관심히 생겨서 내용을 살펴보다가 릴리즈 노트가 2014년에서 멈춰있는걸 확인했습니다. 더이상 버전업이 될 계획은 없나요?

Radar Chart 중복 데이터 색상

퍽도잘났겠지 | 2016-03-02 11:44:06

수고하십니다.

Radar Chart 소개에서 한 공간에 여러개 차트 표현이 가능하던데요.

현재는 2가지 데이터를 겹쳐서 보여줄 때 동일한 색상에 투명도로 구분되어지는 것 같은데요. 혹시 fgColor을 데이터별로 적용할 수 있는 방법은 없는건가요?

Nwagon의 라이센스

lee narae | 2016-03-01 12:38:26

안녕하세요.

Nwagon을 도입해보고 싶은데 , 준거하는 라이센스를 찾지 못해서 문의 드립니다. 소속한 회사 방침이 라이센스가 확인 되지 않은 것은 이용할 수가 없어서 입니다.

웹페이지에서 고대비 색상 모드 옵션을 지원할 경우에..

포레스트 | 2016-02-22 17:04:18

웹페이지 내에서 고대비 색상 모드 옵션을 지원할 경우(옵션을 켜지 않을 경우에는 일반 색상 모드)에, 일반 색상 모드에서는 명도 대비 미 준수 항목을 위배 항목으로 잡아야 하는지 궁금합니다.

네이버 실시간 검색어 순위

Iris Buist | 2016-02-19 11:44:05

alt alt

1위에서 10위까지 나오는데, 순서가 지정된 목록이기 때문에 당연히 ol 을 쓰는 것 까진 알겠는데요...

ol을 쓰면 저절로 번호가 붙는데, li 안에 또 번호를 넣는다면, 스크린리더로 접근시나 의미상으로 중복되는게 아닌가 궁굼합니다. (물론, 제가 이 분야에 대해서 정확히 알지 못하기 때문에 스크린리더상에서 어떻게 읽어주는지는 모릅니다.)

답변 주시면 감사하겠습니다!

table에 궁금한점이 있어 문의드립니다.

zippo | 2016-01-27 17:03:22

테이블작업을할때 간혹 사선을넣어 작업해야할사항이 발생하는데 우선 레이아웃은 사선을 background로 깔고 text를 absolute로 배치를 시키면되는데 이렇게하면 접근성에 어긋날거같은데 사선으로 코딩할경우 접근성에 맞추는 방법이 있는지 궁금합니다.

접근성 관련 문의 하고 싶어서요

화랑댐 | 2016-01-05 14:07:30

안녕하세요 접근성 마크 획득을 해야하는 분양 사이트를 준비 중에 있습니다. 몇가지 문의 사항이 있어서요.

  1. 셀렉트 박스 ex) 전체 , 분양중 , 분양예정 이런식의 셀렉트 박스가 있어서

해당 내용이 셀렉트가 되면 보여주는 정보가 바뀝니다. ajax로 처리가 되겠지요.

이러면 해당 셀렉트 옆에 GO 버튼이 있어야하는건가요???

  1. 이미지 컨텐츠 분양정보로 테이블값이 많이 들어가는 부분이 있습니다. 이부분이 구축이 되고 나서 운영을 생각하면 직접 테이블로 작성하기에는 시간과 공수가 많이 들고 규칙성이 없습니다. 대신 해당 내용이랑 같은 PDF파일은 준비할수 있습니다.

이미지에 대한 설명이 너무 길었을때 롱디스크립션으로 이동해서 작성하는 원리처럼 이미지에 해당 내용은 PDF를 다운 받으면 안내 받으 실수 있다고 이런식으로 처리하면 문제가 될까요??

널리 블로그는 Rss 피드를 제공하나요?

김경만 | 2016-01-04 16:00:56

RSS 피드로 받아보고 싶은데, 페이지를 찾을 수 없네요. RSS를 지원하지 않는다면 지원 예정이 있는지를 알려주실 수 있으신지.. 또한 지원한다면 URL을 알려주신다면 감사하겠습니다.

N-Wax 문의

GilgaC | 2015-12-29 21:04:41

N-wax를 이용해서 검사를 해보려 하는데요, 아래 메세지가 나오네요.

"You can check following schemas only: "http://", "https://""

이 경우 어떻게 해결 가능할까요? 너무 궁금합니다~

Nwagon 라벨 문자열이 길면 짤리는 현상

존오웬 | 2015-12-16 16:15:04

alt

안녕하세요. 문의드립니다. 위 사진에 검은색 박스 부분의 문자열이 전부 다 안보이고 짤려 보입니다. 안 짤리고 다 보이게 할 수 있는 방법 문의 합니다.

NULI 웹 접근성에 대해 질문드립니다.

Soul | 2015-12-11 19:25:09

안녕하세요 널리 개발자분들, 저는 며칠전 네이버 그린팩토리의 도서관 2층에 위치한 접근성 체험부스를 보고나서 장애인과 비장애인 모두가 편하게 이용할 수 있는 웹 접근성에 대한 지침을 가진 NULI의 관점에 큰 인상을 받게 된 한 학생입니다. 접근성 체험을 직접 해보고서 궁금한 점들이 생겼지만 네이버 고객센터에는 이와 관련하여 따로 질문을 보낼 공간이 없어서 널리 포럼에 이렇게 글을 올리게 되었습니다. 만약 이 게시판에 이 글의 목적이 맞지않아 삭제해야한다면 말씀해주시기바랍니다..

먼저 전맹을 위한 네이버 웹 접근성 지침에 대한 것입니다. 화면을 읽어주는 소프트웨어인 스크린 리더같은 경우엔 개인적으로 다운을 받아야하는 것인지, 아니면 일정 보조기기가 설치되어있는 컴퓨터의 경우 네이버에서 자동적으로 제공하는 부분인지 알고 싶습니다.

저시력을 위한 웹 접근성 지침의 경우 확대 소프트웨어를 통해 화면을 크게 확대하여 볼 수 있도록 도와준다고 하였는데요. 확대 소프트웨어를 검색하였을 때 따로 제공되는 부분이 없는데 이는 어떻게 이용할 수 있는것인가요? 네이버 창을 보았을 때, 따로 확대를 할 수 있도록 가능하게 하는 부분이 있거나, 명도 대비를 하게끔 하는 버튼을 찾아볼 수 없어서 여쭈어봅니다..

또한 저는 접근성 체험부스에서 Special User가 웹을 이용할 때 도와주는 다양한 보조기기를 체험할 수 있었는데요, 직원분께서 말씀해주시기를 운동장애를 가진 장애인을 위한 특수 키보드의 경우는 네이버에서 직접 제작하였다고 들었는데. 이에 대해 제가 설명을 잘못 들은 것인지, 그게 아니라 실제 네이버에서 제작된 특수 키보드라면 상용화 되고 있는건지, 또는 될 가능성이 있는지 궁금합니다. 실제로 특수 키보드에 대해 검색하였을 때, 그러한 디자인을 찾아보지 못하였는데요. 운동장애를 가진 장애인이 사용하기에 적합한 키보드 배열을 가지고 있다고 생각하여 주의깊게 보았습니다.

마지막으로 마우스 대신 키보드 사용성을 높인다는 점에서 TAB키와 엔터, 스페이스바 등을 이용하여 원하는 곳을 선택하는 것에 대해 전맹 사용자의 경우 들리는 웹 이용을 위해선 한칸씩 버튼이 이동하여 소리가 들리도록 해야겠지만 운동장애 사용자의 경우는 키보드를 이용하여 버튼을 선택할 때 버튼이 이동하는 방향을 하나하나 보고있어야 한다는점에서 불편함을 느낄것 같다고 생각하였습니다... 실제로 제가 네이버 웹 페이지에서 키보드만으로 버튼을 이동할 때 파란색 체크박스를 계속해서 주시하고 있어야하였는데요. 그러한 인터페이스에 대해 개선방향이 있을지.. 아니면 왜 그렇게 해야만 하는지에 대한 설명을 해주시면 감사하겠습니다.

웹의 문턱을 낮추려고 노력하시는 널리 개발자분들 정말 멋지다고 표현할 수 밖에 없습니다! 긴글 읽어주셔서 감사합니다.

N-MET .json 파일 ? 에러..

이민욱 | 2015-10-06 13:12:11

.json 파일을 열면 등록했던 이미지가 파란박스 ? 로 나오는데 ..

왜그런건지 알수 있을까요 ..??

널리사이트 질문있어요~

정진택 | 2015-09-08 16:40:23

널리사이트 보다가 질문이 있습니다..

널리사이트의 아이콘들을 보면 image sprite 처리가 되있는데..

어떤 것은 아래 처럼 아이콘셋을 만들어 사용 하는 부분이있고

<span class="nuli-icon icon-back"></span>

푸터영역의 페이스북 아이콘 같은것을 보면 아래와 같이되어 있는데..

<a href="https://www.facebook.com/NULINTS" target="_blank" class="nuli-img"><span class="blind">널리 페이스북</span></a>

이부분에 대해서 어떤 기준을 가지고 처리하셨는지 궁금합니다. 있다면 단순히 사용 빈도의 차이일까요?

ㅠㅠ접근성체험 그래서 어디서 어떻게 할 수 있다는 건가요?

labrava | 2015-08-31 22:15:11

네이버 메인에서 배너를 보고 체험에 대해 알게 되었는데 일반인에게도 공개되는 체험인지 그렇다면 어떻게 할 수 있는지 알기가 어렵네요

저시력 적록색맹의 예시에 대해서

이육사 | 2015-08-19 13:41:20
 

적록색맹중에서 녹색이 약한... 녹색약자 입니다.

 

사진의 예시처럼 저렇게 보이질 않습니다.

 

적록색맹... 색약은 색의 구분이 어려운것이지 색의 농도가 바뀌는것이 아니라는 점입니다...

 

 

사용자의 이해를 돕기 위해 약간의 과장의 요소가 있을수도있겠지만

 

조금 더 세분하던가 사진을 보다 더 자세한 예시를 들면서 했으면 하네요

 

아시겠지만

 

같은 소리를들어도 절대음감을 알아내는 사람이 있는가하면 그렇지 않는 사람도 있듯이

 

같은 색이라도 농도차의 차이는 개개인마다 다르게 보여질수 있으나 아주 큰 차이 없다고 생각됩니다.  녹색은 녹색으로 보여진다는것이죠!

 

적록색맹이 적색과 녹색을 못알아 보지 못하는것이 아닌

 

적색과 녹색등 다른 비슷한 색과 섞여있을때 구분을 못하는것이므로

 

예시 사진처럼 색의 왜곡은 아닌듯합니다.  

체험하면서 개선했으면 하는점 제생각입니다

이산화탄소같은남자 | 2015-08-18 16:11:04

체험하면서 제 느낀점입니다

 

1. 간단한 마우스 모션을 등록 ( 원하는 모션을 실행 했을경우 프로그램실행 )

 

2. 마우스 휠을 -누르고- 돌릴경우 ↑↓ "마우스 포인터 돋보기 확대 축소 기능 추가" ( 손동작 하나로 다할수있도록 생각해보았습니다 )

물론 터치기능이 있다면 확대 축소는 더쉬울거라 생각합니다

 

3. 휠을 일반적으로돌릴경우 ( 색대비 반전 , 흑백반전, 흑백, 등 ) 수동 변환

 

등을 생각해보았습니다

 

column chart 질문

태랑 | 2015-08-12 15:22:55

안녕하세요.. 이제 막 사용해보려 하는 유저인데요...

column chart 이용해서 이제 겨우 하나 그려보고 있는데

 

스크립트를 보니

 

...

 

                column.onmouseover = Nwagon.showToolTip(tooltip, px+cw/2, -ch, tooltipText, 14, 7, 18);
                column.onmouseout = Nwagon.hideToolTip(tooltip);
...

이런 부분이 있더라구요.. 그래프 위에 마우스 올리면 값을 보여주고 내리면 감추고...
그런데 마우스와 상관없이 그래프 그려지면서 바로 값을 표시해줄수는 없나요?

이래저래 고쳐봐도 잘 안되네요.. ㅠㅠ

모바일웹 마크업 관련

레나 | 2015-06-16 18:15:05

 

UI 라이브러리내에 있는 예제를 굉장히 유용히 참고 하고 있는데요.

모바일웹 마크업 관련 하여  UI 라이브러리 처럼 다운받아 쓸 수 있는 모바일관련 소스는 없나요?

모바일 코딩이 처음이라.. 어떻게 마크업을 시작해야 할지 막막해서요..  

 

모바일 접근성으로 문의드릴게있습니다.

zippo | 2015-06-10 13:49:11

테이블을 짜는데

 아아아

  아아아1

  아아아2

  아아아3

 오오오

  오오오1

  오오오2

  오오오3

 어어어

버튼

 어어어1

 어어어2

 어어어3

 

 

 

 

 

보이스오버로 쭉읽어내려가다

어어어셀 쪽 버튼을읽으면서 3/1이라고 읽는데 셀의정보를 읽어주는걸로 보이긴하는데 이런경우 접근성으로 위배가되나요?

만약 위배가된다면 3/1이라고 읽는부분은 삭제하는방법이 있나요?

FE 개발도구 모음 - 소개

yhwcj | 2015-05-18 13:56:58

웹접근성과 관련한 FE 개발도구 모음에

제가 만든 소프트웨어를 소개하고 싶은데, 연락처가 따로 없어 광장에 문의 드립니다.

 

혹시몰라 제 메일주소를 남겨 드립니다.

yhwcj@naver.com / 유현욱

N-MET 에서 CSS Formatter 사용시 문제

물티슈 | 2015-05-14 10:50:46

 

Strict Standards: Non-static method csstidy::gvw_important() should not be called statically, assuming $this from incompatible context in /home1/nuli/public_html/www/N-MET/cssFormatter/csstidy/class.csstidy_optimise.php on line 217

Strict Standards: Non-static method csstidy::is_important() should not be called statically, assuming $this from incompatible context in /home1/nuli/public_html/www/N-MET/cssFormatter/csstidy/class.csstidy.php on line 986

 

jquery.ui.css 라던지를 N-MET 으로 돌리면 저런 문구가 상단에 반복적으로 몇번씩 붙고 포매팅이 되는데요

 

뭐가 문제인가요?

N-MET 저장이 왜 계속 됫다 안됫다하는걸까요 ?ㅠㅠ..

디자페 | 2015-04-27 12:07:02

N-MET 저장이 왜 계속 됫다 안됫다하는걸까요 ?ㅠㅠ.. 

서버 문제인가요 ?

 

막대그래프 이용 문의입니다

쇼핑지혜 | 2015-04-14 17:58:56

막대그래프에서 y축을 하나더 이용하고 싶은데요

 

하나는 지금처럼 왼쪽 y축으로 해서 이용하고

 

통계 나타날때 율(50%) 를 보여주기 위해서 오른쪽 y축에도 값을 추가 하고 싶은데 

 

어떻게 해야 되는지 알고 싶습니다

UI 라이브러리 탭메뉴 타입9 메뉴 할당시 제공 소스로 깨져 보임

따부 | 2015-04-01 13:38:13

  나눔>UI 라이브러리>UI Pattern>Menu>탭메뉴 타입 9에 이 기본 설정 css가 없으면 liststyle 기본값으로 깨져 나고 레이아웃 깨져 나오고 그러네요.   아래와 같이 제공해 주시는 코드를 따로 넣어야 제대로 보입니다.

 

/* NHN Web Standard 1Team JJS 100401 */

 

/* Common */

body,p,h1,h2,h3,h4,h5,h6,ul,ol,li,dl,dt,dd,table,th,td,form,fieldset,legend,input,textarea,button,select{margin:0;padding:0} body,input,textarea,select,button,table{font-family:나눔고딕,NanumGothic,돋움,Dotum,AppleGothic,sans-serif;font-size:12px} img,fieldset{border:0} ul,ol{list-style:none} em,address{font-style:normal} a{text-decoration:none} a:hover,a:active,a:focus{text-decoration:underline}          

 

그리고 소스 앞에 풍선이 짤려 보여서 ::before 태그를 사용해 추가 해야 말풍선이 제대로 보입니다. 이부분 수정 반영 해 주셨으면 합니다.

말머리 부분

.lst_type li em::before{display:block;float:left;height:17px;margin:-1px 0 0;padding:2px 5px 0 0;background:url(img/bu_listhead.gif) no-repeat 0px 0px;color:#fff;font-size:11px;font-weight:normal;vertical-align:top;content: }   

구글맵 접근성 관련 질문 드립니다.

Alice | 2015-03-20 11:45:03

현재 진행하고 있는 공공기관 프로젝트에서

클라이언트는.. 구글맵을 화면에서 보여주기를 원하고

또 웹접근성 인증마크도 따야합니다.

그런데 접근성도 그렇고 html 밸리데이터 오류검사나 카도와에서도 걸리네요..

 

이거 어떻게 해야합니까?

윈도우 8.1에서 스크린리더로 플래시 접근하는 방법

Jiyun Eom | 2015-03-18 17:57:33

스크린리더로 플래시 콘텐츠에 접근하고자 하는데요, 

윈도우 7에서는 가능했었는데 윈도우 8.1로 업데이트하면서 접근이 안 되네요ㅠㅠ 

 

제가 방법을 잘 모르는건지,, 혹시 접근 가능한 방법이 있을까요~?

(OS는 윈도우 8.1이고 센스리더 버전은 4.4입니다!)

1.4.4 Resize Text

Bj Kim | 2015-02-16 20:35:03

제가 작업하고 있는 사이트가 WCAG2.0 LEVEL AA를 모두 준수해야 합니다.

그래서 성공기준 하나하나 살펴보고 있는데요.

1.4.4. Resize Text에서 턱 막히더라구요.

 

내용을 보면 이미지를 제외하고 200% 확대했을 경우 콘텐츠의 손실이 없어야 한다는 내용인데,

구체적으로 이걸 준수하려면 어떻게 구현을 해야 할지 막막합니다.

 

브라우저의 기본기능은 이미지까지 확대가 되는데,

200%를 확대할 수 있는 컨트롤 키를 별도로 두어야 하는지도 고민이 되고,

overflow:hidden이 남용되고 고정 픽셀 단위를 사용한 사이트에서 이걸 만족하려면 어떤 식으로 하는 것이 가장 효율적일까요...?

 

막막한 초보에게 한 수 알려주세요.

Font Simulation 모바일 버전 업데이트 안내

Nts Nuli | 2015-02-09 14:31:58

안녕하세요.

 

 

널리 홈페이지의 Font Simulation Mobile이라는 개발도구에 대해 아시나요?

 

이전에 UIT 개발실의 양주희님께서 제공해주셨던 도구인데요.

 

모바일 디바이스들이 버전업을 하면서 지원되는 폰트가 늘어났습니다. 

 

지원 폰트 목록

  1. AppleSDGothicNeo-Regular
  2. AppleSDGothicNeo-Thin
  3. AppleSDGothicNeo-Light
  4. AppleSDGothicNeo-Medium
  5. AppleSDGothicNeo-SemiBold
  6. AppleSDGothicNeo-Bold
  7. 나눔고딕
  8. Helvetica
  9. HelveticaNeue-Thin
  10. HelveticaNeue-UltraLight
  11. HelveticaNeue-Light
  12. HelveticaNeue-Medium
  13. HelveticaNeue-Bold
  14. HelveticaNeue-CondensedBold
  15. Simsun
  16. MSPゴシック
  17. MS P明朝
  18. Sans-serif
  19. Sans-serif-thin
  20. Sans-serif-light
  21. Sans-serif-condensed
  22. Serif

 

마무리

앞으로도 많은 사용과 관심 부탁드립니다.

폰트 시뮬레이션 모바일 버전 웹 주소 QR코드 http://nuli.navercorp.com/sharing/fe/font/mobile

▶ Font Simulation 모바일 바로가기 

 

 

 

N-MET 관련 질문드립니다. [전체 사이즈 잘림현상?]

디자페 | 2015-01-19 19:35:58

안녕하세요.

한가지더,

N-MET 관련 질문드립니다.  

 

가끔, 기 test.json 파일을 열어, 이미지를 수정시,

 

해당 이미지 사이즈가 변경될경우,

내보내기로 출력된 출력물의 이미지 파일이 

잘려서 ? 저장되는 현상이 있습니다.

 

가로 500 세로 500의 기존 문서엿는데,

새로 작업된 파일이 가로 550 세로 500 으로  사이즈가 변환되었을때

 

이 50px이 잘려져 나오는 현상이 있습니다.

 

이미지를 움직여보아도, 실 작업문서 사이즈가 변환되지않는 문제인데요.

 

이럴때는 어찌해야하나요 ? 방법이 있을가요 ?

 

혹시나해서 json 파일을 에디터로 불러보아도, 

작업문서 사이즈 가이드라인을 지정해주는 부분은 보이지가 않더라구요..

 

다른분들은 이런현상 없으신가요 !?

 

 

 

정말 좋은 N - MET !!!!!!!!!!!!!!!!! 사랑해요!!!!!

 

N-MET 관련 질문드립니다!

디자페 | 2015-01-18 12:37:55
 

안녕하세요 
css- sprite 도구로 n-met을 항상 잘 사용중이던 유저입니다. 
그런데 갑자기.... 내보내기 기능이 작동을 안하네요.. 

왜그런가 문의좀 드려봅니다. 
저장은 잘되는데, 
내보내기시 0% 에서 계속 멈춰잇습니다... 
계속 지우고 다시설치해봐도...같네요 ㅠㅠ 왜그런가요?... 

 

기타 다른 sprite 도구를 다찾아봣지만.. n-met 만한게 없네요..

 

꼭좀 문제 해결하고싶습니다!!!

   

Nwagon 레이더차트 이용 문의입니다.

성준영 | 2014-12-22 14:27:30

현재 Nwagon 레이더 차트를 이용하려고하는데

음..

레이더 차트를 사용하면 5개의 항목을 사용할시 약 25도정도 기울여져서 효기가되던데 혹시 이거 조절하는부분이 어디쯤에 있는지 알수 있을까요??^^;

그리고 단위가 20, 40, 60, 80, 100 이렇게 되어있는데 이것을 1,2,3,4,5 이런식의 수정이 가능할까요..?ㅎㅎ

maxValue나 minValue 설정하는곳을 못찾아서 헤매고있네요..ㅠ

 

table태그 caption, summary 사용에 대해 질문드립니다.

Minsu Jo | 2014-12-19 15:05:03

안녕하세요~ 

매일 눈팅만 하다가 이렇게 글을 납겨봅니다. 

 

Table태그에 caption태그와 summary 태그가 있는데요~

 

요즘 작업을 하다보니 위 2개 태그가 정말로 필요한 것인지에 대한 의문이 들더라구요!!

분명 게시판같은 경우에는 테이블이 나오기전에 자신이 선택을 해서 들어온 것인데 caption태그는 왜 필요한 것이며,

summary 태그는 분명 th태그 등으로 컬럼명을 설명하고 내용이 이해가 될 것 같은데.

 

사용을 하다보니 정말 필요한 것인가에 대한 의문에 이렇게 글을 남겨봅니다!

체험사이트 방명록 깨짐..

권태성 | 2014-12-17 19:04:50

체험 사이트 (http://a11ybooth.naver.com/experience/a11y.nhn?type=guestbook&fromNuliParamVal=Y)

방명록의 최근에 올라온 글들이 깨져서 나오고있는데

네이버 고객센터에가니 유해게시물,저작권,명예훼손 신고만 받고있어

문의를 어디다가 해야하나 고민하다가 이곳에다가 올립니다..

 

한 table에 다수의 th 행이 존재할 수 있을까요?

셍상 | 2014-11-07 11:26:23

 

위와 같은 형태의 테이블일 경우, th행을 반복하여 코딩하는 게 맞는 방법인지 궁금합니다~!

 

Nwagon관련하여 문의드립니다

울ㅇㅡㅇ | 2014-10-28 15:54:17

안녕하세요 현재 Nwagon을 잘 이용하고있습니다. 

 

몇가지 기능상의 질문이 있어 문의드립니다.

 

1.  column 차트에서 values값을 넣으면 X축에 표시가 되는데 자동으로 45도 기울어져 표시됩니다.

    이를 기울어지지 않고 그냥 바르게 표시하는 방법은 없나요?

 

2. 현재 모든 차트가 마우스오버를 해야만 수치를 알 수 있습니다.

   마우스 오버 없이 화면실행시 바로 수치를 뿌려주고 싶습니다.

 

3. Pie 차트에서 fields값이 오른쪽에 표시되는데 실제로 그려진 Pie 와의 간격이 너무 벌어져있습니다.

   fields값과 :Pie의 간격을 조정할 수 있는 방법은 없나요?

 

Nwagon 질문드립니다.

만학도 | 2014-10-28 15:43:44
 
2가지 궁금한 사항이 있습니다.
1. 개인이든, 기업이든 무료로 사용할 수 있는 라이브러리인가요?
2. 샘플 소스가 key : value로 되어있는데 value가 고정값이 아닌 어떠한 값을 받아와서 넣어줘야 하는 경우
   값을 받아와서 상황마다 동적으로 보여줄 수 있는 기능이 가능한가요?
3. 혹시 API문서가 있나요?

nWagon관련해서 질문할게요

inbin611 | 2014-10-23 15:45:22

반응형 웹을 만드려고 하는데

차트를 그리려고 합니다.

 

nWagon를 사용해서 차트를 그리고 싶은데 

혹시 웹 페이지 사이즈에 따라서
차트가 잘리지않고 이쁘게 사이즈가 줄어드는 것도 가능한가요?

막대그래프 관련

wjdwogns912 | 2014-10-21 08:33:34

막대그래프(chart type : column) 관련해서 궁금한것이 있습니다.

 

1. 데이터 값들의 차이가 크면 작은값들은 아예 표시가 되질 않습니다.

    제가 입력한 데이터값은 37263942235000 /  1468731 이렇게 두개 입니다.

    작은수가 조금이라도 그래프에 그려지게 하려면 어떻게 해야하는지 궁금합니다.

 

 

2. 세로막대 그래프는 있는데 가로막대로 만들고자 하면 어떻게 해야하는지 궁금합니다.

 

 

3. 목적상 그래프를 %값으로 표시를 했지만 마우스 오버를 했을때는 %값이 아닌

    실제 데이터를 표시를 하고 싶은데요 가능한지 궁금합니다.

 

 

 

 

이런곳이 있는지 몰랐네요

웃는얼굴 | 2014-10-20 16:18:18

안녕하십니까 2010년부터 웹표준 및 웹접근성 진단 및 컨설팅 업무를 수행하고 있는 사람입니다.

 

네이버에서 접근성에 관련해 이런 일들을 하고있다니 정말 반갑네요.

 

사이트를 둘러보고 이런저런 정보를 보면서 아쉬운점이 있어 글을 쓰게 되었습니다.

 

 

제가 모바일앱 접근성 진단을 수행하면서 네이버 앱을 진단했었는데, 정말 쓸수가 있나? 라는 생각이 들정도로 형편 없었습니다.

 

하이브리드 앱을 사용한 네이버 앱의 경우 안드로이드 기종에서는 접근이 제한되어 사용할 수가 없다는 판단이었습니다.

 

올해도 수행하고 있는데 아직 결과는 모르겠네요. 아쉬운점이라는 것은 웹쪽에만 관심을 기울이시지 말고 모바일앱 에도 관심을 갖고 개선사업을 

 

하셨으면 어떨까 싶습니다.

 

또한, 현재는 PC/모바일 웹 접근성 지침이 하나로 되어있지만, 이는 스마트폰이 나오기 전 지침이라 모바일웹을 PC 접근성 지침과 같이 보는건

 

시대착오적인 생각이라 생각합니다. 네이버 같은 큰 기업에서 앞장서서 이를 해결하셨으면 합니다.

 

NWCAG 같은 지침도 자체 개발 하셨는데 이 지침에도 PC와 모바일 웹이 같이 섞여 있더군요 

 

모바일 웹을 사용자는 스마트폰을 쓰는데 진단은 PC에서 한다는거...앞뒤가 맞지 않는 거죠

 

이왕 시작하신거 주도적으로 이끌수 있는 기회가 되셨으면 합니다.

 

 

아..온라인 체험관에 소개 동영상 부분...자막은 잘 달려있는데, 초반에 자막으로만 나오는 부분은 시각장애인들에게는 정보가 전달되지 않습니다.

 

참고하셨으면 합니다.

그래프를 세로로 출력하려면...

김성용 | 2014-10-07 10:39:58
막대그래프나 꺾은선 그래프의 경우 세로로 출력(X축에 값)할 수 있는 지요?

차트에서 데이터 레코드를 출력

김남일 | 2014-09-29 13:16:43

차트에서 마우스 오버를 했을 경우가 아닌

 

차트내의 각 영역에서 데이터 값을 보여주는 기능추가가 가능한가요?

막대그래프의 x축 제목들의 길이가 길 경우 ie8에서 기울어지지 않습니다.

r*** | 2014-09-26 18:20:19

막대그래프의 x축 제목들의 길이가 길 경우 ie8에서 기울어지지 않습니다.

다른 브라우져에서는 정상적으로 기울어져서 조회됩니다.

뭔가 설정을 달리해야 하는지요?

 

pie 관련문의

쌍미 | 2014-09-22 14:09:16

안녕하세요 pie 형태의 차트를 커스터마이징 중인데.  이런 모양으로 정렬을 하려니 마우스를 올렸을때 값이 짤려 나오고 우측에 글자 밑으로 나옵니다.   

어디를 수정봐야 하나요? 그리고 혹시 id="chart" 이안에 파이가 position:absolute 인가요? 저는 div 안에 정 중앙에 오게 하고 싶습니다.

 답변 부탁 드립니다.   

 

감사합니다.

 

 

chartType: pie 관련문의

쌍미 | 2014-09-22 10:49:08

안녕하세요.

chartType: pie 관련문의 드리고 싶습니다.

fields에 위치를 수정하고 싶은데 어디를 수정해야 되나요? 

 

 

Nwagon colorset 기본요청 드립니다.....

hmarmalade | 2014-09-18 09:17:42

안녕하세요

Nwagon  잘 사용 하고 있습니다

쓰다 보니 color 같은 기본 값이 있었으면 합니다

bgColor, fgColor, colorset

같은 color 에 기본 값을 셋팅 하면

좀더 좋은 Nwagon 이 될꺼 같습니다

 

개발 요청 드려 봅니다.......

감사합니다

 

Nwagon ie9 createElementNS 에러 발생 합니다

hmarmalade | 2014-09-18 08:49:34
win7 순수 ie9 에서
SCRIPT438: 개체가 createElementNS 속성이나 메서드를 지원하지 않습니다. 
Nwagon.js, 줄 115 문자 9
이렇게 에러가 발생 합니다

확인 부탁드립니다......


Nwagon 버그 수정 안내

정인호 | 2014-09-15 16:35:05

안녕하세요 접근성팀 정인호입니다. 

 

추석연휴 이전에 Nwagon에서 몇몇 버그가 발생하여 수정 후 업로드하였습니다. 

 

버그 내용은

 

  1. IE8 이하, 표준모드에서 정상적으로 렌더링 되지 않음
  2. Donut 차트 사용 시 Field의 갯수가 많을때 Field의 위치로 인하여 다 보이지 않음

위 2가지를 수정하였습니다. 

버그 발생 시에 광장을 통해 알려주시면 검토 후 피드백을 드리겠습니다. 

 

저희 널리 광장을 통해 다양한 활용방법 또는 다양한 지식 공유도 부탁드립니다. 

 

감사합니다.

 

N-WAX 검사 후 가이드 링크가 유효하지 않습니다.

오버가이 | 2014-09-15 12:08:40

http://nuli.navercorp.com/accessibility/manual/7.1.1 

가이드 누르면 위 페이지로 이동하고 아래와 같이 페이지가 나옵니다.

 

헉! 들어오시면 안됩니다!

잘못된 접근이거나 페이지가 만료되었습니다.

 

Nwagon Donut Chart 사용 관련

체리인형 | 2014-09-03 16:58:56

안녕하세요. Webtoon Insight 라는 웹툰 관련 작은 웹진형 커뮤니티를 운영하고 있는 운영자입니다. (기회/개발 전부 1인이서 진행합니다.)

우선 정말 좋은 차트를 사용할 수 있도록 배포 및 개발하고 게산 모든 분들께 정말 감사의 말씀드립니다. 정말 잘 사용하고 있습니다.

NWagon Chart를 잘 사용 중 아래 사항으로 이렇게 문의를 드리게 되었습니다.

 

개발환경 / ASP.NET MVC 3.5

위와 같이 등록 정보가 많으면 높이 값을 높게 설정하지 않으면 항목이 출력되지 않고, 출력되기 위한 높이 정보를 수정하면 위 이미지와 같이 차트 위에 큰 공백이 생기는 것을 확인 하였습니다. 관련 css를 수정할려고 하지만 잘 적용이 되지 않아 이렇게 문의 드리게 되었습니다.
 
관련 내용 확인 부탁드리면서 리플 부탁드립니다. 감사합니다.
ps. 곧 추석이내요. 이글을 보시는 모두 즐거운 명절들 되시길 바라겠습니다.


nwagon 차트 ie8에서는 안되는데 확인되나요?

| 2014-09-02 16:52:56

<meta http-equiv="X-UA-Compatible" content="IE=edge" /> 

 

이메타태그가 들어가면 차트가안나오고

 

태그빼면 페이지가 깨지는데 어떤방법으로  써야될까요

방사형차트에 대해서 문의드립니다.

wordel | 2014-08-22 17:04:43

방사형 차트로 개발중 문의사항이 있어서 글을 남깁니다. 

 현재 방사형 차트로 구현중인게 있는데

그 구현 차트의 값의 범위가 0 ~ 1까지의 소숫점으로 이루어진 값(예 1, 0.3452, 0.622 등)입니다. 

방사형 차트 항목 중

var CONST_MAX_RADIUS = 100;
var CONST_DECREMENT = 20;

이 부분을

CONST_MAX_RADIUS = 1

CONST_DECREMENT = 0.2

 로 변경하였습니다.

 

그랬더니 차트가 정상적으로 출력이 안되고 있습니다.(아래 이미지 참조)

 

 
 var CONST_MAX_RADIUS = 100;
var CONST_DECREMENT = 20;
일때
 
 
20, 40, 60, 80, 100 대신 0.2 ~ 1까지 표시되고 그래프가 그려지게 구현하고 싶습니다.
 
소숫점으로 표시할 수 있는 방법을 알고 계시다면 알려주시면 감사하겠습니다

N-MET 내보내기가 되지 않습니다.

그남자 | 2014-08-21 23:01:07

N-MET에서 내보내기할 경우 업로드 중 에러가 발생합니다. 

(프로그램 자체에서는 에러가 발생하지 않고 0 byte 짜리 파일이 생성됩니다.)

 



응답결과 
Warning: Unknown: write failed: No space left on device (28) in Unknown on line 0
Warning: Unknown: Failed to write session data (files). Please verify that the current setting of session.save_path is correct (/dev/shm/php-sess) in Unknown on line 0

 

빠른 확인 부탁드리겠습니다!! 

 

p.s: 소스가 복잡하진 않을 것같아 필요하신 분들은 설치해서 사용하실 수도 있을 것같은데요. 

      upload.php와 download.php를 포함한 N-MET 관련 서버 설치본도 함께 공개해주심은 어떠신지요?  

      서버 개발을 잘 모르시는 분들도 같이 일하시는 분들 중 서버 개발자분이 한 두 분 정도 계실테니 설치 가이드만 있으면 

      직접 세팅해서 쓰실 수 있을 듯~ 

 

Nwagon 버그 수정안내

정인호 | 2014-08-19 16:03:44

안녕하세요 접근성팀 정인호입니다. 

많은 분들이 Nwagon을 사용하주시고 계실텐데요

Nwagon의 버그가 발견되어 수정 후 배포함을 알려드립니다. 

한 화면에 2개의 Donut이나 Polar Chart를 사용하고 

2개 이상의 Chart가 데이터의 값이나 비율이 비슷할 경우 

렌더링 시에 Chart가 겹쳐보이는 경우가 발생하였습니다. 

 

현재 이 버그에 대해 수정 후 재배포하였으니 다운로드 후 

확인해보실 수 있습니다. 

 

Nwagon은 오픈소스로 누구나 수정가능하며 

버그 발견 시에 알려주시면 최대한 빠른 시일내에 수정하여 

재배포하도록 하겠습니다. 

 

감사합니다.

 

폰트 시뮬레이션 없어졌나요?

d*** | 2014-08-05 14:58:00

유용하게 사용하고 있어서 별도로 북마크 해뒀었는데..

없는 페이지로 나오네요.

없어진 서비슨가요?

UI 라이브러리의 라이센스는 어떻게 되나요?

마토소 | 2014-08-01 00:04:28

UI 라이브러리, NWagon과 진도프레임 워크를 이용하여 커뮤니티 싸이트 구축중인데 상업적이용이 가능한가요?

오픈소스라고 되어 있는데 라이센스 규약을 찾을 수가 없습니다. 어느 선까지 사용이 가능한지 궁금합니다.

감사합니다.

Nwagon ie7,8에서도 완벽하게 구현되나요?..

스몰지원 | 2014-07-15 11:09:50

테스트 해봤는데.. 7,8은 보이는게 많이 차이나는거 같아서요.~ ㅠㅠ

chart에 대한 질문을 드립니다.

wordel | 2014-07-09 17:16:21

radar차트의 경우 한 차트에 두개 이상의 값을 표시 할 경우 같은 색상으로 폴리곤이 그려지는데 혹시 각 폴리곤마다 색상을 지정할 수 있는지 궁금합니다.

그리고 radar차트는 폴리곤을 제외한 배경색을 지정할수 있게 되어 있던데 다른 차트들도 배경색을 지정할 수 있는지 문의드립니다. 예제에는 나와있는게 없어서 궁금합니다.

혹시 차트들에 대한 API 문서가 제공되고 있는지요? 예제에 해당되는 내용만 보다보니 궁금한 점이 많습니다.

네이버 블로그에서 사용되고 있는 여러가지 차트 디자인들은 사용할수없는것인지 궁금해요``

퍼픕스 | 2014-07-08 15:43:10

위의 이미지들처럼 차트 구현을 하고 싶은데. 따로 css 수정을 거쳐야하는지 궁금합니다.``

그리고 혹시 nwagon관련 api는 없는가도 질문드려요..

 

 


 



네이버 뮤직을 보이스 오버로 사용할 때 궁금한 점 문의

njpaiks | 2014-07-08 01:26:54

여기에 올려도 되는 것인지 모르겠습니다.

네이버 뮤직을 보이스 오버로 사용할 때 궁금한 점 문의합니다.

  1. 1분 미리듣기 중입니다 가 화면의 하단에 나타난 경우 포커스가 안되고 음성 출력도 안합니다. 또한 어떻게 하면 없앨수 있는지요?

  2. 재생목록에 곡이 추가되면 뱃지가 나타나면서 추가된 곡의 숫자를 알려주는데 보이스 오버로 음성 출력 안됩니다

  3. 검색 후 노래 목록이 나타난 상태에서 미니 플레이어의 콘트롤에 포커스가 되면 세 손가락 위, 아래로 쓸기가 안됩니다.

  4. 검색 결과가 나타난 화면에서 맨 윗줄(검색 필드, 검색버튼, 음악 검색)과 5개의 옵션 사이에 빈 공간이 나오는데 왜인가요?

  5. 5개 옵션 다음에 전체선택, 전체듣기, 정확도순 다음에 숨겨져 있는 곡명(낭만에 대하여)을 음성 출력함. 왜 첫 번째 검색 결과 노래를 가리는지?

    백남중

오랜만에 들어왔더니 개편!!*0*

SungJin Jang | 2014-07-08 00:56:22

축하드려요~ ^0^ 너무 오랜만이라 전에 정들엇던 널리와는 많이 생소한느낌이네요~

웹접근성과 UI 라이브러리로 많은 도움을 받았엇죠.ㄳ(__)ㄳ

좀 간결하고 좀 없어진것들( UI 라이브러리 예제???)들이 좀 아쉽긴하지만

챠트 개발에 신경쓰시느라 고생하시겠죠~ 항상 배워갈수 있는 사이트 감사합니다.

원그래프 구현중 질문이 있습니다..

Jongsun Beak | 2014-06-20 19:02:53

안녕하세요.

nwagon을 처음 사용해보면서 이것저것 만지고 있는데요

마우스 오버시 툴팁 말고 그래프 위에 퍼센트를 항상 표시해주고싶은데

어디를 건드려야 되는지 도통모르겠네요;;;;;;;;;;;;;;;;

네이버 애널리틱스에 원그래프 처럼 바꾸고 싶은데 팁좀 주세요

원그래프로 작업중입니다..ㅠ

안녕하세요 chart 사용중에 궁금한게 있습니다.

장인성 | 2014-06-12 16:27:52

라이브러리 제작 매우 감사드립니다.

multi_column차트를 사용해서 개발중입니다.

이게 혹시 아직 방향전환된 형식의 차트는 구현이 안되어 있는지요?

가로 막대 차트로 변경이 가능한건지 궁금해서 문의드립니다.

예제 페이지에는 관련내용이 없어 매우 해매고 있습니다. ㅎㅎ

수고하십시요~

그래프의 fields(범례) 부분 숨기는 방법

임세진 | 2014-05-25 22:22:35

그래프의 fields(범례) 부분 어떻게 숨기나요?

차트의 데이터만 변경하려면 어떻게 하나요?

Joonha Lee | 2014-03-31 10:56:07

Nwagon.chart(sOptions); 을 호출하면 같은 코드가 계속 추가되는 것 같은데...

<div id="mainframe_childframe_form_Div00" tabindex="-1" 
    <ul class="accessibility">Web accessibility status
    <svg version="1.1" width="390" height="301"
    <ul class="accessibility">Web accessibility status
    <svg version="1.1" width="390" height="301"
    <ul class="accessibility">Web accessibility status
    <svg version="1.1" width="390" height="301" 
</div>

차트의 기본 속성은 그대로 놔두고 수정된 데이터만 보여주고 싶거든요.

데이터만 업데이트하는 함수가 따로 있나요?

Factory에 Layout을 쓰면 에러코드가 나오네요.

문태부 | 2014-03-26 10:20:11

style에 .aside class={blahblah} 또

.aside class=

이렇게 만들어 지네요.

가변폭은 언제 만들어지는지요?

max-width만 적용하시면 되는거 아닌가요?

Line Chart에는 hrefs가 적용되지 않네요.

이인욱 | 2014-03-07 15:08:18

var options = { 'legend':{ names: ['08-12', '08-19', '08-26', '09-02', '09-09', '09-16'], hrefs: ['http://naver.com','http://naver.com','http://naver.com','http://naver.com','http://naver.com','http://naver.com'] }, 'dataset':{ title:'Playing time per day', values: [[61,7, 66], [76,33, 66], [49,22, 45], [58,26, 76], [48, 15, 76], [56, 18,83]], colorset: ['#DC143C','#FF8C00', '#30a1ce'], fields:['Error', 'Warning', 'Pass'] }, 'chartDiv' : 'Nwagon', 'chartType' : 'line', 'chartSize' : {width:700, height:300}, 'maxValue' : 100, 'increment' : 10 }; Nwagon.chart(options);

위와 같이 적용 하였다고 하였을 시 08-12나 08-19 등 일자를 클릭하였을 때 hrefs에 정의 된 http://naver.com으로 페이지 이동이 되어야 될꺼같은데 아무 변화가 없네요. 클릭조차 되지 않아요.

테스트 browser는 chrome과 firefox입니다.

한 데이터가 너무 클 때 중간에 자를 수 있을까요?

JongHeon | 2014-03-07 12:39:58

우와! 우리나라에 이런 오픈소스가 있는줄 방금 알았네요. 유용하게 쓰겠습니다.

궁금한 점이 있는데, Bar chart 사용시 한 데이터가 엄청나게 클 때 중간수치를 건너뛸 수 있는 기능은 없을까요?

예를 들면 1, 2, 3, 4, 50 <- 이렇게 데이터가 있을 경우 20~40을 날려버리는 식으로...

한 화면에 같은 그래프를 여러번 그릴때

Park Min Su | 2014-03-05 11:10:52

아래와 같이 한화면에 같은 그래프를 여러번 그리면

그래프가 깨져서 보이는데요

어떤점이 문제가 될까요?

단순히 option값 3개 주고

options1 : { 'legend':{ names: ['male', 'female', 'etc'], hrefs: [] }, 'dataset': { title: '성별 집계', values: [[0,0,0]], bgColor: '#f9f9f9', fgColor: '#DC143C', }, 'chartDiv': 'div1', 'chartType': 'radar', 'chartSize': {width:500, height:300} },

Nwagon.chart(options1); Nwagon.chart(options2); Nwagon.chart(options3);

했습니다.

alt text

chart 의 legend값

강윤중 | 2014-02-18 10:49:10

안녕하세요. 먼저 Nwagon 개발에 감사 드립니다. 저는 Nwagon을 사용하여 mrtg그래프를 그려 보려고 시도 중입니다. 그런데 legend값을 dataset에 맞게 입력을 해야 하던데 이럴경우 dataset값이 많을 경우(288개=60초60분24시/5분) legend값을 모두 입력하고 그래프에 출력해야 하기 때문에 그래프를 정상적으로 볼 수가 없었습니다. legend값에 시작,끝,단계(또는 출력수) 설정하여 출력할 방법은 없는지요? 그리고 각 값에 동그라미(점) 표시가 되지 않게 설정하는 변수는 없는지요? 감사합니다.

Nwagon 데모의 Line Chart 결과값중 스크린리더로 읽을 시 의문나는 점에 관해

Aheu | 2014-02-14 13:51:50

좋은 라이브러리를 개발해 주셔서 감사드립니다. 스크린리더(NVDA)를 사용하여 데모 페이지 http://nuli.navercorp.com/nwagon 의 Line Chart를 읽읅시 의무나는 값이 있어서 질문 드리게 되었습니다. 차트 문서 맨 끝줄에 이런 내용이 있습니다.

0102030405060708090100ErrorWarningPass08-1208-1908-2609-0209-0909-16

위 값의 의미를 정확히 모르겠습니다. 복사해서 붙여 넣으니 공백문자도 없고 읽기도 힘들고요. 해당 값이 어떤 의미를 가지는지 알 수 있을까요? 그리고 차트 제목이 단순히 ul쪽에 입력되는 것 같던데 사용자가 제목임을 인지하기에는 좀 어려움이 있을 것 같단 생각이 들었습니다. 시맨틱한 tag 들로 마크업하면 좀더 용이할 것 같은데 이에 대해서도 어떤 생각을 가지고 계신지도 궁금하네요. 감사합니다.

nWagon 툴팁 문의

MuYul Hong | 2014-02-12 16:18:20

nWagon 사용을 하다가 multi_column 차트에 마우스 오버시에 보이는 툴팁 정보가 잘못된 것이 아닌가 해서요. 데모용 파일 기준으로 보면 'EunJeong' 위에서는 빨강/노랑/파랑 어느 곳이든 '5,7,2' 라고 나와야 할 것 같은데 노랑 위에 올리면 '2,5,7' 이라고 나오는 것을 보니 'Hansol' 쪽 값이 나오는 것처럼 보이네요.

외부인이 글을 남겨도 되는지 모르겠지만, 마땅히 남길 곳이 없어서 여기에 글을 남겨 봅니다. 보시고 지우셔도 됩니다 :)

네이버 스마트에디터를 키보드로 이용할 때 글쓰기 영역에서 빠져나오려면?

김동현 | 2013-12-18 13:51:19

접근성이 괸찮은 웹 에디터를 찾던 중 네이버 스마트에디터를 잠시 사용해 보고 궁금한 점 하나.

마우스를 사용할 수 없는 상지장애인 입장에서 네이버 스마트에디터 2.0을 키보드로 이용할 때 글쓰기 영역에서 빠져 나오기 위해서 도움말에 언급된 alt+.와 alt+,를 눌러도 초점이 이동하지 않던데. 혹 재가 모르는 다른 방법이 있는건가요? 다른 기능은 단축키만 조금 익숙해 지면 무리없이 사용 할 수 있을 정도로 키보드로 사용할 수 있던데. 글쓰기 영역에서 빠져나가는 기능은 안되네요^^.

테스트 했던 데모 페이지는 http://jindo.dev.naver.com/smarteditor/demo/SmartEditor2.html

감사합니다.

반복되는 콘텐츠(메뉴 등)가 있을 경우 이를 건너뛸 수 있는 링크를 제공하기에 대한 문제점

Hyunho Kim | 2013-10-23 16:19:18

접근성 작업을 하기에 앞서 먼저 하는 부분중에 하나가 건너뛰기 메뉴를 작업을 하는거죠. 그런데 브라우저중 Opera와 Webkit 계열에는 건너뛰기 대상 요소로 키보드 포커스 이동이 되지 않은 버그가 있다는 겁니다.

PC경우 Opera와 Webkit 계열 브라우저외에 작동을 하기때문에 사용을 할수가 있다고 생각은 되지만 모바일 경우 스마트폰의 인터넷에서 사용을 하다보면 전혀 작동을 하지않는다는 점입니다.

지난번 널리에서 스크립트를 이용해서 인식하게 하는 방법이 있었지만 그역시 어느 순간부터 동작을 하지 않더라구요.

인식하지않는 기능을 넣자니 사용자에게 혼동을 주지 않을 가 싶네요...

모바일접근성에 건너뛰기 메뉴는 필수인가요? 인식하게 하는 방법은 없는걸가요? 있으면 ..제발 지식을 나눠주세요

모바일 접근성검증관련해서 체크리스가 어쩔땐 너무 획일적이다 싶네요...

p요소와 dl요소의 적절한 쓰임

So Yeun Seo | 2013-09-12 10:46:01

안녕하세요. 마크업하다가 의문이 드는점이 있어서요~

dl 요소는 제목과 내용이 들어갈때 사용하는것으로 알고 있습니다.

예를 들어 " 날짜: 2013-06-06 " 일 경우 날짜가 제목이고 2013-06-06은 내용으로 판단하여 dl요소를 쓰려고 하는데 dl요소에다가 쓰는게 적절한것인지 p요소에다가 쓰는것이 적절한것인지 궁굼합니다.!

dl 대신에 p요소를 쓰면 왠지 용량이 줄어들꺼 같아서요..^^;; 그런데 p를 쓰려니 의미있는 마크업이 아닌거 같구..

위지윅(WYSIWYG) 에디터와 스크린리더 관련 문의

펄님 | 2013-08-20 14:59:09

최근에 위지윅 에디터의 스크린리더 접근성에 대해 살펴볼 일이 있었는데요, 신기한 사실을 하나 발견했습니다.

위지윅 에디터는 문서 구조나 스타일이 입력하는대로 바로바로 표현이 되어야 하기 때문에 일반적으로 iframe 을 넣고 body에 contenteditable="true"라는 속성을 지정합니다. contenteditable이라는 속성은 HTML5에서 생긴 것인데 input이나 textarea가 아니더라도 편집이 가능하도록 하는 속성입니다. 자세한 설명은 아래 URL을 참고해주세요.

http://html5doctor.com/the-contenteditable-attribute/

우리나라 포털에서 사용하고 있는 에디터들, 해외 에디터들이 모두 비슷한 방식으로 구현이 되어 있있습니다.


1. 네이버 메일 에디터

 <iframe src="/js_src/com/nhncorp/mail/write/se2_new/smart_editor2_inputarea.html" id="se2_iframe" name="se2_iframe" class="se2_input_wysiwyg" width="400" height="300" title="글쓰기 영역 : 글쓰기 영역에서 빠져 나오시려면 Shift+ESC키를 누르세요" frameborder="0" style="display: block; height: 500px;" tabindex="5">
iframe 내부
    <body class="se2_inputarea" style="height: 470px; font-family: 나눔고딕, NanumGothic, sans-serif; font-size: 9pt;" contenteditable="true">.....

2. 다음 메일 에디터

<iframe id="tx_canvas_wysiwyg1" name="tx_canvas_wysiwyg" tabindex="5" allowtransparency="true" frameborder="0" title="내용 입력" src="/hanmailex/pages/wysiwyg_html.html?prefix=1&amp;">
iframe 내부
<body onload="init()" contenteditable="true" style="background-color: transparent; padding: 8px;">.....

3. CKeditor

<iframe src="" frameborder="0" class="cke_wysiwyg_frame cke_reset" title="Rich Text Editor, editor1" aria-describedby="cke_49" tabindex="0" allowtransparency="true" style="width: 100%; height: 100%;">
iframe 내부
<body contenteditable="true" class="cke_editable cke_editable_themed cke_contents_ltr" spellcheck="false">.....

이 코드 만으로는 세 에디터의 차이를 잘 모르겠는데요, 스크린리더를 실행시키면 1, 2번 에디터는 body 영역 내에 텍스트 편집이 되지 않습니다. 센스리더의 경우 가상커서를 해제하고 텍스트 입력을 시도했을 때 다른 영역으로 포커스가 이동하며, NVDA 테스트 시에는 편집 가능한 영역이라는 것 자체를 인식하지 못했습니다.

이와 다르게, 3번 에디터는 센스리더와 NVDA 모두 정상 인식/입력이 가능합니다.

다르게 반응하는 두 에디터 간에 어떤 동작의 차이가 있는것일까요??

label 사용시 궁금한 점이 있습니다.

전아름 | 2013-08-16 18:29:19

예를 들어 아래처럼 하나의 제목 아래 두개의 라디오 버튼을 사용할 때에는 어떻게 레이블을 붙이는 것이 좋을까요?

성별: ◎ 남자 ◎ 여자 아래와 같이 했을땐 각각의 레이블은 붙었지만 그래서 그 선택지가 어떤 것의 선택지인지는 알 수 없을 것 같습니다.

   <label for="male"><input type="radio" id="male">남자
   <label for="female"><input type="radio" id="female">여자

title 속성을 이용해서 코드를 짜야 할까요?

그리고 아래와 같이 표를 만들었을때는 어떤 식으로 해야 하나요? alt

레이블 대신 title로 대체해도 되는것인지. 지금은 레이블을 적고 CSS로 안 보이도록 감추어 표현을 했는데.. 어떤 것이 접근성향상을 위해 올바른 방법인지 조언 부탁 드립니다.

웹 접근성 제작이슈

Kyoungtae Kim | 2013-08-02 17:53:45

현재 웹접근성 프로젝트를 진행하고 있는데 어려움이 있어 의견을 공유하고자 합니다.

현제 진행하는 웹사이트는 메뉴부분이 브라우저 하단에 붙어 있는 형태로 제작되어 있습니다. 백그라운드는 풀브라우저형태구요.

테스트 주소입니다. (보안상 사용이미지들은 노출안게 해놨습니다.) http://koramdeo.cafe24.com/ninebridges_modify/introduction/test.html

브라우저창을 늘렸다가 줄였다가 해보시면 아무문제 없이 잘보입니다. (브라우저 작아졌을시에 가로 세로 스크롤 생성)

이슈사항은 브라우저 %를 확대하면(Ctrl + '+'키) 당연희 모든 글씨나 이미지들이 커지면서 스크롤도 생기게 됩니다. 175% 확대를 해보시면, 세로 스크롤 생성되어 이상없이 컨텐츠 확인 가능합니다. 가로스크롤 생성되어 컨텐츠 부분 확인가능합니다.

단 가로 스크롤 이동시 확인해보시면 컨텐츠영역들을 이동하여 확인이 가능하나, 하단에 있는 메뉴들은 확대된 상태에서 브라우저를 넘어간영역은 안보이고, 스크롤로 이동하여 확인이 불가능합니다. 하단에 있는 메뉴 영역도 확대시에 스크롤하여 이동 확인 가능한지를 문의드립니다.

퍼블리싱 작업 수준은 절대 허접하지 않은 수준으로 작업되었습니다. 퍼블리셔분 역시 정말 잘하시는 분이라 자부할 수 있습니다.

이런저런 여러가지 방법들을 생각하여 작업을 해보았으나 쉽게 해결안을 찾지못하였습니다.

하여 유사한 현상을 가진 사이트들은 검토해 보았습니다. 네이버 메일페이지에서 상단 블랙톤 영역부분을 보시면 100% 일 경우는 아무 이상이 없이 잘보입니다만 브라우저%확대시에 (175% 이상) 우측상당에 있는 메뉴들이 브라우저창 에 안보여 잘리게되고, 크스롤 이동 불가능합니다.
alt text alt text

현제 이런형태의 제작방법을 해결할 수 있을까여?

  1. 접근성방침에 브라우저를 % 확대해서 보았을 경우의 이슈사항이 있나요?
  2. 네이버도 같은 문제를 발견하였는데요. 접근성 위배 사항아닌가요? (이 문제가 접근성에 위배되는건가여?)
  3. 이 부분을 해결할 수 있는 기술이 있을까여?

저희가 부족한 것인지 실제로 구현자체가 불가능한건지 고수님들의 조언이 절실히 필요한 시점입니다. 얼핏보기에는 css수정하면 될것같지만 그렇지 않더라구요;; 혹 저희가 제작중인 사이트의 문제점을 해결할 수 있는 안이 있다면 메일 간곡히 부탁드립니다. jungle666@naver.com

대메뉴는 접근성 준수 처리를 어떻게 해주는게 좋을까요?

Parkjiyeon | 2013-05-20 11:23:23

안녕하세요. 대메뉴를 마우스 오버롤 했을때 음영으로 컨텐츠를 구분해주는 거 말고도 볼드처리나 Underline을 적용해야 하는지가 궁금합니다. (ColorDoctor 툴을 통해 Gray Scale 적용시 음영 구분이 됩니다.) 참고 URL 주소입니다. http://www.doosan.com/kr/ir/stockinfo.jsp
메뉴표시를 할때 음영만으로도 컨텐츠 구분이 가능하다면 접근성에 준수했다고 볼 수 있는건지

타 사이트들 보면, 음영이외에도 메뉴명에 Underline을 적용한 사이트도 있어서, 어떻게 처리해주는 것이 접근성에 맞는 건지 궁금하네요.

h태그의 사용~

Keun Kim | 2013-05-13 17:26:32

시맨틱~ 이란 말만 들어도 생각나는것이 h태그인데

  • 사이트의 타이틀은 h1
  • 페이지의 제목을 h2
  • 내부 문단의 타이틀을 h3....

실제 코딩시에 h태그를 h7까지 쓰시는 분이 계실지 궁금합니다.

  • html -> h1
  • body,logo -> h2
  • div,table -> h3
  • ul,dl,ol -> h4....

만약 사용하신다면 h5이후부터는 어떨때 사용하시는지도 궁금합니다.

구분선 ' | ' 을 마크업할때 어떤 방식이 좋은 방법일까요?

최해진 | 2013-04-04 11:41:37
    제목1  |  제목2  |  제목3  |  제목4

구분선 ' | ' 을 마크업할때 어떤 방식이 좋은 방법일까요?

구분선을 텍스트로 사용할수도 있고 < img >태그로 사용할수도 있고 CSS background image로 사용할수도 있습니다.

1. 텍스트 사용

    제목 <span> | </span> 제목

2. < img >태그 사용 (대체텍스트 없음)

    제목 <img src="bar.gif" alt=""> 제목

3. < img >태그 사용 (대체텍스트 있음)

    제목 <img src="bar.gif" alt=" | "> 제목

4. CSS background-image로 사용 (대체텍스트 없음)

    제목 <span></span> 제목

5. CSS background-image로 사용 (대체텍스트 있음)

    제목 <span> | </span> 제목

제작/유지보수 측면에서 또는 웹접근성측면에서 보면 각 사용방법에 장단점이 있습니다.

스크린리더에서 구분선은 구두점자로 구분되어 기본 설정으론 음성 출력이 되지 않고 사용자가 '구두점 문자 읽기'를 추가로 설정해 놓는다면 구분선은 '버티컬바'라고 음성출력이 됩니다.

'버티컬바' 반복 청취를 최소화하기 위해 대체텍스트를 비우는게 좋을지 추가설정이기 때문에 반복청취에 문제가 없을것이라 판단하고 대체 텍스트를 넣는것이 좋을지 다른 분들의 의견이 궁금합니다~!

접근성 고려한 label 사용시 적절한 방법?

Jae Won Jang | 2013-04-01 11:38:08

접근성 공부를 이제 시작한 신입임니다. link 에서 접근성 가이드라인을 보고 공부를 하고있는데 label에 이미지를 사용하지 않는 것이 좋다고 나와있습니다. 하지만 이미지를 꼭 사용해야하는 경우가 있다면 어떠한 방법이 있을까요. label에 title속성을 적용하면 큰 문제가 없을런지요. 궁금합니다.

display, visibility, overflow에 대한 견해

신하철 | 2013-02-21 18:45:54

안녕하세요.^^ 이번에는 웹 페이지의 숨긴내용에 대하여 이야기 해 볼까 하는데요.

간단하게 HTML을 코딩하여 테스트를 해 보았는데 재미있는 결과가 나왔어요 먼저 센스리더의 버전은 PROFESSIONAL) V 3.0.7.0을 사용하고 있구요 Jaws : 14.0, NVDA : 2012.3.1을 사용하고 있습니다.

CSS를 이용하여 내용을 숨기고자 할 때 주로 아래의 것들을 이용하잖아요 1. display:none 2. visibility: hidden 3. overflow:hidden 동일한 콘텐츠를 위 3개의 소스를 적용시켜 각각의 스크린리더로 테스트 해 보았습니다. 센스리더의 가상커서 설정 > 숨긴내용 읽기는 off한 상태이구요 Jaws, NVDA는 모두 설치 후 기본셋팅 상태 입니다.

  1. display:none는 센스리더, Jaws, NVDA에서 모두 내용을 읽지 못합니다.
  2. visibility: hidden는 센스리더에서는 내용을 읽을 수 있었으나 Jaws, NVDA는 모두 내용을 읽지 못하였습니다.
  3. overflow:hidden의 경우 센스, Jaws, NVDA 모두 내용을 읽을 수 있었습니다. 널리 사이트 역시 이 값을 적용시키고 있네요.^^

하여 국내 스크린리더만을 생각한다면 visibility: hidden만 적용시켜도 내용을 읽는데 문제가 없으나 국외 스크린리더 사용자까지 생각을 한다면 overflow:hidden가 가장 좋은것 같습니다.

여기서 의문점이 생겨 한가지를 더 테스트 해 보았는데요. 순서는 위와 같은 방식으로 테이블의 caption에 각각 적용을 시켜 테스트 해 보았습니다. 이유는 센스리더의 경우 display:none이 적용된 caption값을 테이블 단위 이동시에는 읽을 수 있었기 때문에 테스트를 해본 것이구요 결과는 센스리더의 경우 3가지 설정값 모두 테이블의 제목을 읽을 수 있었고 Jaws, NVDA는 위와 동일 하였습니다.

근데 Jaws, NVDA는 테이블단위 이동시 caption값이 숨겨져 읽지 못하는 상황에서 summary값을 읽어 주더라구요. 그러한 기능은 좋은것 같네요

국내 웹 페이지를 둘러보면 테이블의 제목과 설명을 거의 동일하게 제공 하였거나 설명만 혹은 제목만 제공한 곳이 상당수를 차지 하는데요. 이러한 기능은 스크린리더 사용자들에게 도움이 될듯 합니다.

이야기가 약간 산으로 갔었는데요..^^ 결론은 웹 페이지 개발시 내용을 숨기고자 할 때 overflow:hidden값을 적용시키면 어떨까 싶어서 글을 올려 보았습니다.^^ 긴 글 읽어주셔서 감사합니다. 좋은 저녁 되세요~

저시력 사용자를 위한 웹상에서의 반전기능 이런건 어떨까요?

신하철 | 2013-02-21 12:45:46

안녕하세요 ^^ 널리에 토픽을 올리는건 처음이네요 다름이 아니라 저시력 사용자 분들을 위해 웹 페이지 내에서 제공하는 반전기능에 대한 논의를 해볼까 합니다. 기존에도 텍스트와 배경색의 반전 기능을 제공하는 웹 페이지는 종종 있었는데요 해당 프래임 영역만 적용이 되고 전체적으로 반영된 웹 페이지는 없었던것 같습니다.

웹 페이지에 제공된 색 반전기능 샘플

위의 URL에 접속하시면 반전기능이 페이지 전체적으로 적용되는 것을 보실수 있는데요

꼭 위의 URL과 같은 옵션만을 제공하기보다 사용자가 색을 선택할 수 있도록 옵션을 제공하여 웹 페이지에 적용시킨다면 어떨까요?

물론 저시력 사용자분들이 자신에게 맞는 설정값으로 컴퓨터를 활용하시는것은 알고 있습니다. 윈 7에서 테마로 설정 할 수도 있고 Alt + Shift + Print screen으로 윈도우 반전 기능을 이용할 수도 있는데요

초보자를 위해 위의 URL과 같은 방법을 제공하는 것이 어떨까 싶어 조심스럽게 글 올려 봅니다.^^ 다들 좋은하루 되세요.

1: 웹 페이지에 제공된 색반전 기능 샘플 : http://www.freedomscientific.com/tools/change-style.asp

aria에 관련된 정보

Hyongsop Kim | 2013-02-20 15:15:10

안녕하세요? 요즘 웹접근성 영역에 있어 aria 영역에 점점 관심이 많아지고 있는 것 같습니다. 저도 완전하게 다 알지는 못하지만 aria 기능을 잘 이용하여 페이지를 만들면 스크린리더에서 지원을 할 경우 페이지에 리전을 지정하여 랜드마크 이동키를 눌러 이동을 할 수 있고 업데이트 되는 요소를 방향키를 포커스를 가지고 가지 않고도 듣게 하거나 풀다운과 같은 메뉴 조작, 슬라이드 페이지에서의 접근성 등 많은 것을 제공하고 있습니다. 얼마전 Jaws for Windows 온라인교육에서 aria에 대해 교육을 진행하였습니다. 그래서 이에 관한 소스를 올려드리고자 합니다. 게시물 끝쪽에 있는 aria 테스트 페이지에 접속하시면 Aria에 대한 설명 및 테스트 페이지에 접속하시면 해당 내용을 보실 수 있고 각 aria에 대한 테스트 페이지와 소스코드도 제공을 하고 있으니 필요하신 분들은 참고하시면 되겠습니다. 특히 개발자 분들은 aria example 헤딩에 있는 테스트로 제공된 페이지들만 보셔도 될 듯 합니다. 센스리더도 얼마전부터 aria를 조금씩 지원하기 시작했으니 기대해 보아도 좋을 듯 합니다. 감사합니다. link text

Blockquote

명도대비 기준 4.5:1에 대한 생각

펄님 | 2013-02-18 20:08:43

KWCAG 2.0에 보면 저시력 사용자가 콘텐츠를 인식할 수 있도록 콘텐츠와 배경색의 명도 대비가 4.5:1 이상이 되어야 한다는 지침이 있습니다.

1.3.3 텍스트 콘텐츠와 배경간의 명도 대비는 4.5:1 이상이어야 한다

실제로 디자인을 하게 되면 4.5:1이라는 기준을 지키는 것이 매우 힘들 때가 있습니다.

색상을 선택하는 근거는 단순히 심미적인 것을 위해서가 아니라 정보의 중요도를 나타내기 위함도 있기 때문입니다. 명도 대비 기준을 만족하도록 색을 선택한다면, 흰색 배경일 때 가장 흐리게 표시할 수 있는 색상이 767676 정도가 되는데요, 000000로 지정된 두 텍스트를 비교하면 사실 어느것이 중요하다, 중요하지 않다를 가늠하기가 어려울 정도로 색이 진하게만 표시가 됩니다.

4.5:1이라는 기준은 WCAG 2.0(http://www.w3.org/TR/WCAG20/)에서 권장 항목인 LevelAA로 분류되고 있습니다.

1.4.3 Contrast (Minimum): The visual presentation of text and images of text has a contrast ratio of at least 4.5:1, except for the following: (Level AA)

또한 ISO 9241-3에서는 최소 명도 대비를 3:1로 규정하고 있습니다.(참고링크)

현실적으로 가능한 선에서, 그렇지만 저시력 사용자 고려해서 다음과 같이 명도 대비 기준을 준수하면 어떨까 합니다.

  1. 네비게이션, 제목, 본문과 같이 웹 페이지 중요한 콘텐츠에는 4.5:1의 기준을 적용한다.
  2. 비교적 중요도가 낮은 부가정보(카테고리, 날짜 등)는 3:1 이상이 되게 한다.

선택된 탭의 상태를 나타내는 방법, 뭐가 좋을까요?

Jiyun Eom | 2013-02-18 19:26:15

시각 장애인을 대상으로 사용성 테스트를 몇 번 진행해보니 선택된 메뉴의 상태를 알기 어렵다는 얘기를 많이 듣게 됩니다.

탭 메뉴를 제공할때 기본적으로는 다음과 같이 코드를 작성하게 됩니다.

<ul>
<li class="on"><a href="#">메뉴1</a></li>
<li><a href="#">메뉴2</a></li>
<li><a href="#">메뉴3</a></li>
</ul>

선택된 메뉴에는 on 이나 selected와 같은 클래스를 줘서 시각적으로 다르게 표현하기도 하죠. 그런데 이 정보는 시각 장애인이 인식할 수 없습니다. 스타일만 바꾼 것이니까요.

그렇다면 어떻게 해야 시각 장애인도 탭 메뉴가 선택되었다는 인식할 수 있을까요?

제가 생각해본 방법 중 첫번째는 선택된 탭의 링크를 제거하는 것입니다.

페이지 네비게이션에는 이렇게 마크업 하는 경우도 꽤 있는듯 합니다. 그런데 선택된 탭도 클릭할 수 있다고 생각하는 것이 거의 일반적이어서.. 이렇게 하는 것이 과연 좋을지는 모르겠습니다. 사용자의 패턴을 바꾸면 모를까요.

<ul>
<li class="on">메뉴1</li>
<li><a href="#">메뉴2</a></li>
<li><a href="#">메뉴3</a></li>
</ul>

두번째는 선택된 메뉴에 숨김 텍스트로 선택된 상태임을 알리는 것입니다.

(아래 코드에서 blind라는 클래스는 화면에 보이지 않지만 스크린리더에서 인식 가능한 방법으로 스타일을 지정한 클래스로 가정합니다.)

아이폰 앱 개발할때 사용하는 hint 개념을 차용할 수 있을듯 한데, 웹에서는 아직 hint라는 것을 구현하기에 적절한 요소나 속성이 없는것 같아 span 요소로 작성해보았습니다.

<ul>
<li class="on"><a href="#">메뉴1 <span class="blind">선택됨</span></a></li>
<li><a href="#">메뉴2</a></li>
<li><a href="#">메뉴3</a></li>
</ul>

아무래도 이러한 이슈를 고려하지 않았을 때 보다는 번거로울 수 있겠지만 탭의 선택 상태가 당연히 제공되어야 하는 정보라고 생각한다면 고민해볼 가치가 있는것 같습니다. 더 좋은 의견있다면 알려주세요~

(seleted="selected" 속성을 아무대나 사용할 수 있다면 좋을텐데요....;; )

RUM (Real User Measurement) 데이터, 활용하고 계신가요?

Insook choe | 2013-02-18 16:03:42

Web 성능최적화를 진단할 때 가장 중요시 여겨야 할 것이 RUM(Real User Measurement) 에서 얻어지는 데이터라고 볼 수 있습니다.

Google Analytics나 그 밖에도 Mixpanel, Torbit, 루비기반의 서버쪽의 성능체크를 포함하는 New Relic등 해마다 툴을 만드는 업체가 늘어가고 또 이러한 서비스를 이용하는 업체들이 늘어나는 데서도 알 수 있듯이 현재 미국 내 인터넷업계에서 다루는 RUM 데이터는 성능최적화를 다루는 데 있어서 빼 놓을 수 없는 중요한 요인이 된지 오래입니다.

실제로 RUM데이터를 모니터 하고 사용자들의 웹 사이트 이용패턴을 분석하고 문제가 되는 부분을 수정하여 매출을 늘리고 bounce rate를 낮춘 사례들을 어렵지 않게 찾아볼 수 있습니다. 몇가지 실례를 들자면...

  1. Airbnb는 사용자들이 페이지를 떠나는 곳을 집중 분석하여 매출을 5배나 늘리는 데에 성공했습니다. 원문:link
  2. 온라인 판매만을 제공하는 미국의 GILT그룹도 로드테스트와 사용자 데이터 분석결과를 활용하여 잠정적인 문제점을 해결하여 런칭 후 2년안에 $170 million이라는 매출달성에 성공하였습니다. 원문: link

이렇듯 외국 기업에서는 RUM데이터를 적극적으로 또 구체적으로 사용하고 있는데요, 현재 실제로 서비스를 통해서 아니면 자체적으로 RUM 데이터를 수집하고 사용하고 계신가요?? 사용하고 계시다면 혹시 어떤 서비스를 이용하시나요?

새 창 링크나 입력 서식의 레이블을 제공하기 위한 title 속성

펄님 | 2013-02-18 15:43:07

새 창 링크를 나타내기 위해 target="_blank" 속성을 선언하고, target 속성을 사용하지 않은 경우에는 title="새창"이라고 선언해주어도 무방하다고 알고 있습니다. 또한 입력 서식의 레이블도 label 요소를 연결시켜주는 것 외에 입력 서식 자체에 title 속성으로 입력 서식의 역할을 나타내주기도 하죠.

그런데 이 방법이 정말 시각 장애인에게 도움이 되는 방법인지 의문이 들 때가 많습니다.

title 속성은 스크린리더(센스리더)에서 툴팁 읽기를 선택해야 들을 수 있고 일반적으로는 거의 인식하지 못한다는 얘기를 들었습니다. 입력 서식에서도 순차적인 접근 시에는 인식할 수 없고, 단축키 등을 통해서 입력 서식에 바로 접근할 때만 인식 가능하며, labeltitle이 함께 선언되어 있을 때는 label의 우선순위가 높습니다.

비장애인에게 툴팁으로도 정보를 제공하려는 목적이 아니라면, title 속성은 지양하는 것이 맞다고 생각하는데요, 여러분들 생각은 어떠신가요?