Skip to content

fix(components): make Select an accessible listbox with keyboard support - #725

Open
owjs3901 wants to merge 2 commits into
mainfrom
fix/select-a11y
Open

owjs3901 wants to merge 2 commits into
mainfrom
fix/select-a11y

Conversation

@owjs3901

@owjs3901 owjs3901 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Refs #692 (I 세 번째 PR). main에서 분기했습니다.

문제

  • RT-01: Select는 접근 가능한 select/combobox가 아니었습니다. 트리거에는 aria-expanded/aria-label만 있었고, 팝업과 옵션은 일반 div였습니다. role, aria-controls, 키보드 이동, Escape, 포커스 복귀가 없었습니다.
  • RT-07: 바깥 클릭 시 onOpenChange를 거치지 않고 내부 상태만 바꿔, open을 제어하는 쪽은 닫힘을 알 수 없었습니다.

변경

  • 트리거: aria-haspopup="listbox", aria-controls를 붙이고, 닫힌 상태에서 ArrowDown/ArrowUp으로 엽니다. asChild 트리거도 같습니다.
  • 팝업: role="listbox", id를 붙이고 checkbox 타입이면 aria-multiselectable을 붙입니다.
  • 옵션: role="option", aria-selected, aria-disabled, tabIndex=-1을 붙입니다.
  • 열면 선택된 옵션(없으면 첫 옵션)에 포커스합니다. Arrow·Home·End로 사용 가능한 옵션 사이를 이동하고, Enter·Space로 선택합니다.
  • Escape나 선택으로 닫으면 트리거로 포커스를 돌려줍니다.
  • 바깥 클릭은 onOpenChange(false)를 호출하고, 제어되지 않을 때만 내부 상태를 바꿉니다.

동작 변화

  • 키보드와 스크린리더로 사용할 수 있습니다. 마우스 동작은 같습니다.
  • 제어 모드(open prop)에서는 바깥 클릭이 더 이상 숨은 상태를 바꾸지 않고 소유자에게 알립니다.

새로 생기는 오류

  • 없습니다.

검증

  • bun test 5478개 통과, 커버리지 100%. 새 테스트는 키보드 조작, asChild/checkbox, 제어·비제어 바깥 클릭입니다.
  • bun lint 오류 0개, components 빌드 통과.
  • 스냅샷의 useId 의존을 없애려고 테스트의 Select에 id를 고정했습니다(중첩 Select는 별도 id).
  • Select가 useId를 쓰면서 CI의 테스트 순서에서 Checkbox 스냅샷 id가 밀려, #723과 같은 Checkbox 수정(주어진 id 사용, 테스트 id 고정)을 그대로 넣었습니다. 두 PR의 해당 변경은 같은 내용이라 충돌 없이 병합됩니다.
  • Rust 1.99 clippy assert_is_empty 수정은 fix(extractor): let a later css() part replace an earlier one's declarations #706 등과 같은 내용입니다.

owjs3901 and others added 2 commits October 2, 2026 17:35
Refs #692

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…ore test order

Refs #692

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Changepacks

@devup-ui/wasm@1.0.82 → 1.0.83 - bindings/devup-ui-wasm/package.json

Patch

  • globalCss: theme tokens ($text) resolve to var(--text) in global rules instead of being written as invalid $text; imports given as { url, query } objects with unquoted keys are emitted instead of dropped; an '@layer name' key puts the rules it holds in that cascade layer (layers nest as base.reset) instead of flattening them into invalid declarations, and a layer name that is not one identifier is a build error
  • Theme CSS: a color only one variant defines is defined in that variant; a variant renders dark only when it is named dark or listed as dark in the new theme.colorScheme, others light, and light-dark() only encodes a light default with one dark variant; theme names that are not CSS identifiers are quoted in [data-theme]; typography emits fontStyle and textTransform; color variables come out in a fixed order. $tokens with dashes (text-primary) resolve instead of breaking into var(--text)-primary, and dotted length and shadow names are declared with dashes like their references. registerTheme reports, with the theme path, a token name the $token syntax cannot reach and two tokens of a variant that become the same CSS variable (a-b and a.b)

@devup-ui/bun-plugin@1.0.21 → 1.0.22 - packages/bun-plugin/package.json

Patch

  • Auto-update: depends on '@devup-ui/plugin-utils' via a local workspace dependency

@devup-ui/components@0.1.59 → 0.1.60 - packages/components/package.json

Patch

  • Select follows the listbox pattern: the trigger has aria-haspopup, aria-controls and opens with ArrowDown/ArrowUp; the popup is a listbox (aria-multiselectable for checkbox selects) whose options carry role=option, aria-selected and aria-disabled; opening focuses the selected or first option, arrows/Home/End move between enabled options, Enter/Space choose one, and Escape or choosing closes the popup and returns focus to the trigger. An outside click on a controlled select now calls onOpenChange(false) instead of changing hidden internal state

@devup-ui/eslint-plugin@1.0.21 → 1.0.22 - packages/eslint-plugin/package.json

Patch

  • no-duplicate-value, no-useless-responsive, no-useless-tailing-nulls, no-typography-token-prefix and prefer-media-shorthand only report and fix values the build reads as styles: style props of Box, Flex and the other style components and the arguments of css, globalCss and keyframes, through style objects, responsive arrays, conditions and spreads. Arrays and keys in props the component passes through (data-, aria-, event handlers, HTML attributes, props, styleVars), in arguments of other functions and under imports/fontFaces/params are left alone, where autofix used to rewrite them; styles of a component nested in another's prop are checked too. css-utils-literal-only reads a css() or keyframes() result held in a const of any scope as static, as the build does

@devup-ui/next-plugin@1.0.89 → 1.0.90 - packages/next-plugin/package.json

Patch

  • Auto-update: depends on '@devup-ui/plugin-utils' via a local workspace dependency

@devup-ui/plugin-utils@1.0.16 → 1.0.17 - packages/plugin-utils/package.json

Patch

  • Theme CSS: a color only one variant defines is defined in that variant; a variant renders dark only when it is named dark or listed as dark in the new theme.colorScheme, others light, and light-dark() only encodes a light default with one dark variant; theme names that are not CSS identifiers are quoted in [data-theme]; typography emits fontStyle and textTransform; color variables come out in a fixed order. $tokens with dashes (text-primary) resolve instead of breaking into var(--text)-primary, and dotted length and shadow names are declared with dashes like their references. registerTheme reports, with the theme path, a token name the $token syntax cannot reach and two tokens of a variant that become the same CSS variable (a-b and a.b)

@devup-ui/rsbuild-plugin@1.0.66 → 1.0.67 - packages/rsbuild-plugin/package.json

Patch

  • Auto-update: depends on '@devup-ui/plugin-utils' via a local workspace dependency

@devup-ui/vite-plugin@1.0.72 → 1.0.73 - packages/vite-plugin/package.json

Patch

  • Auto-update: depends on '@devup-ui/plugin-utils' via a local workspace dependency

@devup-ui/webpack-plugin@1.0.70 → 1.0.71 - packages/webpack-plugin/package.json

Patch

  • Auto-update: depends on '@devup-ui/plugin-utils' via a local workspace dependency

@codecov

codecov Bot commented Oct 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
bindings/devup-ui-wasm/src/lib.rs 100.00% <ø> (ø)
libs/css/src/theme_tokens.rs 100.00% <ø> (ø)
libs/extractor/src/lib.rs 100.00% <ø> (ø)
libs/extractor/src/tailwind.rs 100.00% <100.00%> (ø)
...kages/components/src/components/Checkbox/index.tsx 100.00% <100.00%> (ø)
...ackages/components/src/components/Select/index.tsx 100.00% <100.00%> (ø)
packages/components/src/contexts/useSelect.ts 100.00% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant