fix(a11y): color contrast, skip link, focus rings, and aria attributes#421
fix(a11y): color contrast, skip link, focus rings, and aria attributes#421gabitoesmiapodo wants to merge 3 commits intotest/demo-smokefrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR improves accessibility across the app by addressing color-contrast issues, adding keyboard navigation support (skip link + focus rings), and improving screen-reader semantics with ARIA attributes.
Changes:
- Update semantic color tokens (warning/ok/danger) to improve WCAG contrast in light/dark modes.
- Add a skip-to-main-content link and restore visible focus styling for icon-only controls.
- Add/extend ARIA attributes for validation and icon-only buttons/links.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/routes/__root.tsx | Adds a skip-to-main-content link and a main-content anchor target. |
| src/components/ui/provider.tsx | Updates semantic token colors for contrast-compliant warning/ok/danger states. |
| src/components/sharedComponents/ui/Modal/index.tsx | Adds aria-label to icon-only close button. |
| src/components/sharedComponents/ui/ExternalLink/index.tsx | Replaces outline="none" with _focusVisible focus ring styling. |
| src/components/sharedComponents/ui/CopyButton/index.tsx | Replaces outline="none" with _focusVisible focus ring styling. |
| src/components/sharedComponents/TokenInput/index.tsx | Adds aria-label to icon-only close button in the token dialog. |
| src/components/sharedComponents/Hash.tsx | Adds aria-label to icon-only explorer external link. |
| src/components/sharedComponents/BigNumberInput.tsx | Introduces aria-invalid wiring based on range validation failures. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| outlineOffset: '2px', | ||
| borderRadius: '2px', | ||
| }} | ||
| transition="background-color {durations.moderate}, border-color {durations.moderate}, color {durations.moderate" |
There was a problem hiding this comment.
transition value looks malformed: the last token is missing the closing } (color {durations.moderate). This will likely result in an invalid CSS transition string and the transition being ignored. Close the token (and consider reusing the same corrected string wherever this pattern appears).
| transition="background-color {durations.moderate}, border-color {durations.moderate}, color {durations.moderate" | |
| transition="background-color {durations.moderate}, border-color {durations.moderate}, color {durations.moderate}" |
There was a problem hiding this comment.
Fixed: added the closing } to the last token.
| borderRadius: '2px', | ||
| }} | ||
| textDecoration="none" | ||
| transition="background-color {durations.moderate}, border-color {durations.moderate}, color {durations.moderate" |
There was a problem hiding this comment.
transition value looks malformed: the last token is missing the closing } (color {durations.moderate). This can invalidate the whole transition declaration. Close the token so Chakra can resolve the duration correctly.
| transition="background-color {durations.moderate}, border-color {durations.moderate}, color {durations.moderate" | |
| transition="background-color {durations.moderate}, border-color {durations.moderate}, color {durations.moderate}" |
There was a problem hiding this comment.
Fixed: same as above.
| const inputProps = { | ||
| disabled, | ||
| onChange: updateValue, | ||
| placeholder, | ||
| type: 'text', | ||
| } | ||
|
|
||
| return renderInput ? ( | ||
| renderInput({ ...inputProps, inputRef }) | ||
| ) : ( | ||
| <chakra.input | ||
| aria-invalid={hasError || undefined} | ||
| {...inputProps} | ||
| ref={inputRef} | ||
| /> |
There was a problem hiding this comment.
aria-invalid is only applied in the default <chakra.input> branch. When renderInput is provided (which appears to be the common path), the rendered input won’t receive aria-invalid on range validation failures. Consider adding aria-invalid to inputProps before calling renderInput, so both branches get it consistently.
There was a problem hiding this comment.
Fixed: aria-invalid is now part of inputProps before the renderInput branch, so both paths receive it.
| as="main" | ||
| direction="column" | ||
| flexGrow="1" | ||
| id="main-content" |
There was a problem hiding this comment.
Skip link target is a <main> element, which is not focusable by default. Following href="#main-content" may scroll but leave keyboard focus on the skip link, reducing its usefulness. Add tabIndex={-1} (or otherwise ensure the target can receive focus) so activation moves focus into main content.
| id="main-content" | |
| id="main-content" | |
| tabIndex={-1} |
There was a problem hiding this comment.
Fixed: added tabIndex={-1} to the <Flex as="main"> element.
| }] and value is: ${value}` | ||
| console.warn(message) | ||
| onError?.({ value, message }) | ||
| setHasError(true) | ||
| } else { | ||
| setHasError(false) | ||
| } |
There was a problem hiding this comment.
hasError is only updated inside the min/max range check. Other early-return paths in updateValue (e.g. empty string, parseUnits failure, decimals-length guard) don’t clear hasError, so aria-invalid can remain true after the user fixes/clears the input. Consider resetting hasError (and optionally clearing onError) on those early returns to keep validity state accurate.
There was a problem hiding this comment.
Fixed: setHasError(false) is now called on the empty-string early return path.
88ed222 to
27b5511
Compare
b84e168 to
184f8ea
Compare
27b5511 to
5d4e886
Compare
184f8ea to
13e024b
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/components/sharedComponents/BigNumberInput.tsx:74
hasErroris only updated insideupdateValue, so if the parent updatesvalueprogrammatically (e.g. reset to 0 / set max) after an out-of-range entry,aria-invalidcan remain stucktrueeven though the newvalueis valid. Consider deriving/clearinghasErrorin an effect whenvalue/min/maxchange, or resetting it whenevervalueis externally synchronized.
const [hasError, setHasError] = useState(false)
// update inputValue when value changes
useEffect(() => {
const current = inputRef.current
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // update inputValue when value changes | ||
| useEffect(() => { |
There was a problem hiding this comment.
In the useEffect that syncs the DOM input value, the parseUnits(...) call inside the effect can throw if the input currently contains an unparseable intermediate string (possible because updateValue returns early on parse errors). Since this effect runs on external value updates, an external update while the user has invalid text could crash rendering; wrap the parseUnits call in a try/catch or guard the string before parsing.
There was a problem hiding this comment.
Fixed: wrapped parseUnits in a try/catch; on failure a sentinel value of BigInt(-1) forces the DOM value to be overwritten, ensuring the external value prop always wins when the current input is unparseable.
5d4e886 to
5c9aa29
Compare
13e024b to
3b7b376
Compare
…butes - Fix warning color contrast: #cc0 fails WCAG AA; use #996600 (light) / #e6b800 (dark) - Fix danger/ok colors: add proper light/dark variants (#ff6666, #66ee66) for dark mode contrast - Add skip-to-main-content link in root layout for keyboard navigation - Add aria-invalid to BigNumberInput default input on range validation failure - Replace outline:none with _focusVisible focus ring on CopyButton and ExternalLink - Add aria-label="Close" to icon-only close buttons in Modal and TokenInput - Add aria-label="View on explorer" to icon-only ExternalLink in Hash component
- Fix malformed transition string in CopyButton and ExternalLink (missing
closing brace on last token)
- Move aria-invalid into inputProps so it applies to both the default input
and any custom renderInput renderer
- Clear hasError state on empty-string input path (was only cleared on
valid range path, leaving aria-invalid=true after user clears the field)
- Add tabIndex={-1} to #main-content so skip link activation moves
keyboard focus into the main region
An intermediate/unparseable input string while the user is typing could cause the useEffect that syncs the DOM value on external prop changes to throw. Wrap parseUnits in a try/catch and use a sentinel value to force the DOM overwrite when the current string is not parseable.
657ada0 to
b0fe779
Compare
22c9e54 to
6a2f37e
Compare
Summary
#cc0fails WCAG AA;#996600(light) /#e6b800(dark)#ff6666,#66ee66)aria-invalidtoBigNumberInputon range validation failureoutline:nonewith_focusVisiblefocus ring onCopyButtonandExternalLinkaria-label="Close"to icon-only close buttons inModalandTokenInputaria-label="View on explorer"to icon-onlyExternalLinkinHashTest plan
pnpm testpasses