Repository navigation
[전수조사/I] UI kit(@devup-ui/components)·theme 런타임·reset-css #692
Copy link
Copy link
Open
Labels
Description
사용자에게 배포되는 런타임 코드의 문제들입니다. 접근성 P0(Select, Toggle, Radio)를 이번에 함께 진행합니다.
전체 계획과 결정 사항은 추적 이슈(#682)에 있습니다. 조사 기준 커밋: 77daad74 (현재 main d0b84255).
발견 항목
심각도: P0 = 흔한 사용에서 조용히 틀리거나 크래시, P1 = 흔한 패턴이 명시적 에러로 막히거나 드물게 조용히 틀림, P2 = 드문 경우·도구/문서 불일치, P3 = 있으면 좋음.
필수 (4)
- RT-01 (P0, WRONG) Select is not an accessible select/combobox
- 근거:
SelectTriggeronly hasaria-expanded/aria-label; the popup and options are generic divs with labels (Select/index.tsx:199-215,235-320,369-460). There are norole=combobox/listbox/option,aria-controls, active descendant, roving focus, key handlers, Escape handling, or focus restoration. - 수정 방향: Implement the listbox/combobox pattern with stable IDs, proper roles/states, active option management, arrows/Home/End/typeahead/Escape, and focus restoration.
- 근거:
- RT-02 (P0, WRONG) Toggle is a mouse-only div, not a switch
- 근거:
Toggle/index.tsx:55-127renders a clickable Box with onlyaria-disabled; it has no switch/checkbox role,aria-checked, tab stop, keyboard handler, visible focus state, form name, or disabled hidden input. The hidden input contains only a stringvalue. - 수정 방향: Prefer a native checkbox input styled as a switch, forwarding input/form props; otherwise implement the complete switch pattern.
- 근거:
- RT-03 (P0, WRONG) Radio button mode removes radios from keyboard/focus navigation
- 근거: button-variant Radio uses
display="none"on the native radio (Radio/index.tsx:70-81); RadioGroup supplies no sharednameand noradiogrouprole (RadioGroup/index.tsx:67-102). Default Radio's only focus rule compiles as invalidoutline:1px sold ...(Radio/index.tsx:83-86;probe-outputs/Radio.txtcontainssold). - 수정 방향: Visually hide rather than
display:none, give each group a stable shared name/legend or ARIA group label, and fixsolid/:focus-visiblestyling.
- 근거: button-variant Radio uses
- RT-04 (P0, WRONG) RadioGroup loses false/0 values and lies about callback types
- 근거: controlled/default initialization uses truthiness (
value ? String(value) : defaultValue ? ..., lines 57-60), sofalse,0, and''become unselected.handleChangealways callsonChange(_value)with a string although its public type promisesstring | number | boolean(lines 7-18,62-65); tests explicitly expect'2'and'false'. - 수정 방향: Track the original typed value, use
value !== undefined, and make RadioGroup generic sooptions,value, andonChangeshare one type.
- 근거: controlled/default initialization uses truthiness (
권장 (15)
- RT-07 (P1, WRONG) Select controlled-open close paths silently bypass
onOpenChange- 근거: outside click invokes raw
setOpen(false)(Select/index.tsx:83-90) rather thanhandleOpenChange; whenopenPropis supplied, internal state changes are ignored, so the controlled popup cannot close and the owner is not notified. - 수정 방향: Route all open transitions through one setter and only mutate internal state when uncontrolled.
- 근거: outside click invokes raw
- RT-08 (P1, WRONG) Select popup positioning can remain off-screen and leaks listeners on React 18
- 근거: overflow is compared against document dimensions despite
position:fixed(lines 251-257), and switching branches setstoporbottomwithout clearing the other (259-265). Cleanup is returned from a ref callback (236-280), which React only supports in React 19. - 수정 방향: Use an effect/layout-effect or positioning primitive, compare against
innerWidth/innerHeight, clear opposite inset properties, and add collision tests.
- 근거: overflow is compared against document dimensions despite
- RT-09 (P1, WRONG)
SelectTrigger asChildsuppresses opening when the child hasonClickand drops ref composition- 근거: line 193 selects
children.props.onClick ?? handleClick; a child handler therefore replaces, rather than composes with, select opening. The cloned element receives no merged trigger/child ref and ignores wrapper props. - 수정 방향: Clone the element with composed handlers/ref and deterministic prop precedence; honor prevented events.
- 근거: line 193 selects
- RT-10 (P1, WRONG) Stepper freezes in callback-only uncontrolled usage
- 근거:
handleChangereturns immediately wheneveronValueChangeexists (Stepper/index.tsx:51-58), even if novalueprop is supplied. The displayed internal value never changes. - 수정 방향: Always notify; update internal state when
valueProp === undefined.
- 근거:
- RT-11 (P1, WRONG) Stepper safeguards can be overwritten and text mode emits input-only DOM props
- 근거: consumer props spread after internal
disabled,onClick,value, andonChange(lines 83-93,109-121,149-178), allowing bounds/state logic to be replaced. Text mode renders a div withonChange,readOnly,type="number", andvalueattributes and no value semantics. - 수정 방향: Compose handlers, put invariant props after spreads, and render a separate semantic text output path.
- 근거: consumer props spread after internal
- RT-12 (P1, WRONG) Forced generic aria-labels override visible/external labels
- 근거: Button always defaults to
aria-label="button", Input to"input", and Textarea to"textarea"(Button:161-163,Input:123,Textarea:70-72). Accessible names therefore ignore visible button text or associated<label>unless users manually override. - 수정 방향: Remove generic labels; expose optional user labels and keep icon-only defaults explicit at call sites.
- 근거: Button always defaults to
- RT-13 (P1, WRONG) Input clear emits a fabricated, incomplete ChangeEvent
- 근거:
Input/index.tsx:64-69casts{target:{value:''}}toReact.ChangeEvent;currentTarget, name/type, nativeEvent, preventDefault, and other standard event fields are absent. Controlled inputs also only clear if the parent synchronously accepts this fake event. - 수정 방향: Do not fabricate React events; use
onClear/onValueChange, or dispatch a real input event after updating the DOM through a ref.
- 근거:
- RT-14 (P1, WRONG) Input/Textarea errors are not associated with their controls
- 근거: Input never sets
aria-invalid; neither Input nor Textarea gives the error message an ID or appliesaria-describedby/aria-errormessageto the control (Input:157-171,Textarea:70-124). - 수정 방향: Generate/accept message IDs, set
aria-invalid, and merge user descriptions witharia-describedbyoraria-errormessage.
- 근거: Input never sets
- RT-15 (P1, WRONG) Runtime color customization is scoped to the wrong DOM node
- 근거: Input places all variables on the
<input>(lines 140-150), but icon, clear button, and error text are siblings and cannot inherit them; CSS asks for unmatched names such as--inputBg,--inputDisabledText,--primaryBg, and--inputPlaceholder. Textarea's error sibling cannot see--error; Select declares--inputBackgroundbut popup CSS reads--inputBg. Probes show… - 수정 방향: Put shared variables on the root container and align every public color key with the CSS custom-property name; add computed-style tests.
- 근거: Input places all variables on the
- RT-16 (P1, WRONG) Loading Button remains actionable and does not announce busy state
- 근거:
loadingonly swaps in IconSpinner (Button/index.tsx:231-246); it does not disable the button, block duplicate clicks, setaria-busy, or provide loading status text. - 수정 방향: Add configurable disable-while-loading behavior,
aria-busy, and an accessible loading label/status.
- 근거:
- RT-17 (P1, SEC) ThemeScript cannot run under nonce-based CSP and can abort before setting a theme
- 근거: ThemeScript props expose only
autoandtheme, and render a nonce-less inline script (ThemeScript.tsx:4-18). Its generated code callslocalStorage.getItemandmatchMediawithout a guard; storage security errors abort initialization and produce FOUC. - 수정 방향: Accept/pass standard script props including
nonce; wrap storage/media access with safe fallbacks while setting a deterministic default.
- 근거: ThemeScript props expose only
- RT-18 (P1, CRASH) Theme helpers are browser-global APIs despite the RSC/SSR surface
- 근거:
getTheme,setTheme, andinitThemeunconditionally accessdocument,localStorage, and/orwindow; direct invocation during SSR throws.'use client'is a bundler boundary, not a runtime guard. - 수정 방향: Guard browser globals and document server behavior, or separate browser-only exports so SSR imports cannot call them.
- 근거:
- RT-27 (P1, WRONG) reset-css omits the clipping half of its accessible-hidden rule
- 근거: source and probe output contain only
position:absolutefor:where([aria-hidden=false i][hidden]:not(:focus))(reset-css/src/index.ts:140-142). Canonical sanitize.css also requiresclip:rect(0,0,0,0), so revealed-for-AT hidden content can be visibly painted. - 수정 방향: Add the missing
clipdeclaration and a full emitted-CSS parity test against the intended sanitize.css version.
- 근거: source and probe output contain only
- RT-28 (P1, TEST) Tests achieve line coverage without covering user-facing contracts
- 근거:
bun test packages/components packages/reactpasses 194 tests/88 snapshots and reports component/runtime lines covered, but there are no React-18 runs, ref tests, keyboard/focus navigation tests, role/name/description assertions for Select/Toggle/RadioGroup, CSP/storage-failure tests, multi-tab/system-theme tests, ThemeProvider table/nesting/function-theme tests, Proxy enumera… - 수정 방향: Add a concern matrix and browser behavioral tests, plus React 18/19 compatibility jobs if the peer remains
*.
- 근거:
- RT-19 (P2, MISMATCH) Theme does not follow system changes or synchronize across tabs
- 근거: initialization reads
matchMediaand storage once; the store only observesdata-thememutations in the current document. There are nostorageor media-query change listeners (theme-store.ts:8-42). - 수정 방향: Subscribe to
matchMedia().changewhen in auto mode andwindow.storage; define explicit-vs-auto persistence semantics.
- 근거: initialization reads
Reactions are currently unavailable
Activity
fix(components): label Checkbox with the id it is given and pin test ids
test(components): pin Checkbox and nested Select ids so snapshots ign…
fix(components): call the Stepper setter as a member so a minified na…
fix(components): name Select options, container, trigger and confirm …
chore: merge main into ui kit behavior
fix(components): merge main into radio group accessibility
fix(components): merge main into select accessibility
fix(components): merge updated radio accessibility parent
fix(components): merge reviewed toggle switch parent
fix(components): merge updated select accessibility parent