키보드와 스크린 리더

키보드로 할 수 있는 일과 스크린 리더가 읽어 주는 내용, 아직 포인터가 필요한 동작을 정리해요.

마우스를 쓸 수 없어도 키보드만으로 작업을 옮기고, 크기를 조절하고, 순서를 바꾸고, 삭제할 수 있어요. 편집할 때마다 라이브 영역이 내용을 읽어 주고, 아직 포인터가 필요한 동작도 몇 가지 있어요.

키보드로 이동하기

작업 목록과 타임라인은 하나의 role="treegrid"이고, 각 행은 aria-owns로 타임라인에 있는 자기 막대를 가리켜요. 이 요소에 붙은 핸들러 하나가 모든 키 입력을 읽어서 아래 14개 키를 처리하고 나머지는 앱으로 그대로 올려보내요. 처리하는 키는 먼저 preventDefault되니 Alt+ArrowLeft를 눌러도 브라우저가 뒤로 가지 않아요.

키동작
ArrowUp / ArrowDown열은 그대로 두고 한 행 위아래로 옮겨요. 양 끝에서 멈추고 순환하지 않아요.
ArrowRight / ArrowLeft펼칠 수 있는 행의 첫 셀에서는 펼치거나 접어요. 그 밖에는 한 셀 옮기고 끝에서 멈춰요.
Home / End행의 첫 셀이나 마지막 셀로 가요. Ctrl이나 Cmd를 더하면 첫 행이나 마지막 행으로 가요.
Enter / Space펼칠 수 있는 행은 펼치거나 접어요. 그 밖의 행은 행을 읽어 주고 상세 패널이 켜져 있으면 열어요.

키보드로 편집하기

가로 방향키는 이동으로 읽히기 전에 조합키가 먼저 가져가요. 그래서 Shift+ArrowRight는 크기 조절이고 Ctrl+ArrowRight는 들여쓰기예요. 범위 선택은 없어요.

키동작권한
Alt + 가로 방향키작업을 옮겨요. 두 날짜가 함께 움직여요.canMove
Shift + 가로 방향키종료일을 옮겨요.canResize
Alt+Shift + 가로 방향키시작일을 옮겨요.canResize
+ / =, - / _진행률을 5포인트 올리거나 내려요.canChangeProgress
Delete / Backspace작업과 하위 트리를 삭제해요.canMove
Alt + 세로 방향키형제 사이에서 행을 옮겨요.canReorder
Ctrl/Cmd + 가로 방향키앞 형제 아래로 들여쓰거나 한 단계 내어써요.canReorder
Ctrl/Cmd + 세로 방향키배율을 한 단계 촘촘하거나 성기게 바꿔요.없음

한 단계는 키를 누르는 순간 읽는 현재 배율의 드래그 단계예요. 달력 단위로 더하니 서머타임 경계를 넘어도 작업의 시각은 그대로예요. 줌을 바꾸면 편집 키가 움직이는 크기도 달라져요. 새 배율은 알려 주지만 새 단계는 알리지 않고, 배율별 단계는 타임라인에 있어요.

삭제 권한은 따로 없어서 옮길 수 있는 작업은 삭제할 수도 있어요. 요약 행은 플래그와 상관없이 크기 조절과 진행률 변경을 무시하고, progress 필드가 없는 작업도 마찬가지예요. 플래그는 드래그와 똑같이 정해져요. 우선순위는 상호작용 설정에서 확인해요.

이동, 접기, 배율 단계는 권한을 확인하지 않아서 읽기 전용 차트에서도 동작해요. 순서 바꾸기 키는 읽기 전용 여부와 상관없이 allowReorder가 필요하고, 들여쓰기와 내어쓰기는 hierarchy도 필요해요. Ctrl/Cmd+ArrowLeft는 한 번에 한 단계씩 올라가면서 방금 떠난 부모 뒤에 행을 놓아요. 3단계 깊이의 행은 세 번 눌러야 하지만 드래그는 한 번에 최상위로 가요.

차트는 배율 선택기를 그리지 않아서 포인터 없이 배율을 바꾸는 내장 수단은 이 두 키뿐이에요. setScale에 연결한 네이티브 <select>는 플랫폼의 키보드 동작을 그대로 가져와요. 완성된 컴포넌트는 명령형 API에 있어요.

스크린 리더가 읽는 내용

막대의 라벨은 이름, 요약 행이면 summary, 날짜, 진행률이 있으면 진행률 순이에요. 날짜는 현재 배율의 툴팁 포매터로 만드니 locale이나 formats[scale].tooltip을 바꾸면 읽히는 내용도 바뀌어요. 자세한 내용은 로케일과 날짜 형식에서 확인해요.

키보드 편집은 차트의 첫 자식인 role="status" aria-live="polite" 영역 하나로 알려요. 문구는 영어로 고정이고 그 안의 날짜만 locale과 formats를 따라요.

상황알림
날짜 편집Design phase, Mar 3, 2025 to Mar 15, 2025
진행률 변경Design phase, 45% complete
삭제Design phase deleted
행 이동Design phase moved to 2 of 5 under Phase 1. 부모가 없으면 at the top level
막힌 편집Design phase cannot be changed, progress cannot be changed, cannot be deleted
배율 변경week scale

적용되지 않은 행 이동은 아무것도 알리지 않고, 접기와 펼치기도 알리지 않아요. 형제 목록의 양 끝, 앞 형제가 없는 들여쓰기, 최상위 행의 내어쓰기, allowReorder가 없는 차트, false를 반환한 onTaskMove가 모두 조용히 끝나요.

포인터 드래그는 막대에 그려지는 두 번째 role="status" 줄로 알려요. 이동과 크기 조절에서는 이 줄이 숨겨져 있고 진행률 드래그에서는 퍼센트 표시로 보여요. 드래그 중 눈금 행에 쓰이는 날짜는 aria-hidden이에요. showTooltip={false}는 이 줄을 호버 카드와 함께 없애서 툴팁을 끈 차트는 포인터 편집 중에 아무것도 알리지 않아요. 키보드 알림은 그대로예요.

호버 카드는 450ms 머문 뒤에 열리고, Escape로 닫을 수 있고, 카드 위로 포인터를 옮길 수 없고, 터치에서는 열리지 않아요. 카드의 날짜와 진행률은 이미 막대 라벨에 있어요.

탭 순서와 포커스

마운트 직후에는 포커스된 요소가 없고 첫 Tab은 treegrid의 첫 셀로 들어가요. tasks가 비어 있으면 들어갈 셀이 없어요.

treegrid의 탭 스톱은 로빙 방식 하나예요. 여기에 스플리터, Add task 버튼, 열린 상세 패널의 이름과 Start, End, Progress 입력란, 닫기 버튼까지 더하면 페이지에 최대 8개가 생겨요. 작업의 플래그가 편집을 막은 칸은 텍스트로 그려지고 탭 스톱이 아니며, 직접 만든 renderDetail 본문이 패널의 5개를 대신해요.

셀을 클릭하면 포커스 상태가 그 셀로 맞춰지고, 키보드로 옮기면 행을 화면 안으로 스크롤한 뒤 포커스를 줘요. 방향키로 막대에 올라가도 선택되지는 않고 aria-selected도 쓰지 않아요. 이벤트를 참고해요.

상세 패널은 treegrid 밖의 role="complementary"이고 포커스 트랩이 없어요. 패널을 닫으면 열 때 포커스를 갖고 있던 요소로 돌아가지만, 포커스가 패널 안이나 <body>에 있거나 아무 데도 없을 때만 그래요.

treegrid 밖에서는 스플리터가 가로 방향키와 Enter를 받아요. 의존성 화살표가 선택된 동안에는 Delete, Backspace, Escape가 document까지 닿고 입력란 안에서는 건너뛰어요. Escape는 열린 패널도 어디서든 닫지만, 패널의 입력란 안에서는 그 칸을 되돌려요.

알려진 빈틈

  • 의존성 화살표를 그리는 키는 없어요. 링크 핸들이 tabIndex={-1}이라 화살표를 선택하는 것도, 그래서 삭제하는 것도 클릭이 필요해요.
  • 새 작업을 그리려면 포인터가 필요하지만, 작업 목록 아래 Add task 버튼이 평범한 탭 스톱에서 같은 초안을 만들어요. 목록 패널을 숨겼다면 직접 만든 단축키로 addTask()를 호출하세요.
  • Delete는 행의 어느 셀에서든 포커스된 행과 그 하위 트리를 지워요. 확인 절차도, 되돌릴 스냅샷도 없어요. 되돌리려면 이전 배열을 onTasksChange에서 갖고 있으세요.
  • 가로로 화면 밖에 나간 막대는 그리지 않아서 로빙 탭 스톱이 가리킬 요소가 없어질 수 있어요. 그러면 Tab이 treegrid 안에서 아무것도 찾지 못하고, 포커스가 아직 안에 있다면 Home이나 End가 탭 스톱을 되살려요. 작업 목록을 숨기면 모든 셀이 막대라서 treegrid 전체가 비기도 해요.
  • PageUp, PageDown, 타이핑 검색, F2 편집 모드, 다중 선택, treegrid 안의 Escape는 처리하지 않아요. 키를 다시 배치하거나 키보드 편집만 끄는 prop은 없어요.
  • 고대비용 스타일은 없어서 스타일시트에 forced-colors와 prefers-contrast 블록이 없어요. prefers-reduced-motion은 트랜지션과 애니메이션을 0.01ms로, 스크롤 동작을 auto로 바꿔요.

실제 스크린 리더로 검증한 내용은 없어요. 사용자가 쓰는 리더로 직접 확인하세요. prop과 기본값은 GanttProps에 있어요.