From cf8852da9549d01b103c776aa48658428c873bb7 Mon Sep 17 00:00:00 2001 From: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:37:40 -0300 Subject: [PATCH 1/4] fix(a11y): fix color contrast, skip link, focus rings, and aria attributes - 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 --- .../sharedComponents/BigNumberInput.tsx | 6 ++++++ src/components/sharedComponents/Hash.tsx | 7 ++++++- .../sharedComponents/TokenInput/index.tsx | 5 ++++- .../sharedComponents/ui/CopyButton/index.tsx | 7 ++++++- .../sharedComponents/ui/ExternalLink/index.tsx | 7 ++++++- .../sharedComponents/ui/Modal/index.tsx | 7 ++++++- src/components/ui/provider.tsx | 10 +++++----- src/routes/__root.tsx | 16 +++++++++++++++- 8 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/components/sharedComponents/BigNumberInput.tsx b/src/components/sharedComponents/BigNumberInput.tsx index 60702ede..b3715cd5 100644 --- a/src/components/sharedComponents/BigNumberInput.tsx +++ b/src/components/sharedComponents/BigNumberInput.tsx @@ -6,6 +6,7 @@ import { type RefObject, useEffect, useRef, + useState, } from 'react' import { formatUnits, maxUint256, parseUnits } from 'viem' export type RenderInputProps = Omit & { @@ -66,6 +67,7 @@ export const BigNumberInput: FC = ({ value, }: BigNumberInputProps) => { const inputRef = useRef(null) + const [hasError, setHasError] = useState(false) // update inputValue when value changes useEffect(() => { @@ -125,6 +127,9 @@ export const BigNumberInput: FC = ({ }] and value is: ${value}` console.warn(message) onError?.({ value, message }) + setHasError(true) + } else { + setHasError(false) } onChange(newValue) @@ -141,6 +146,7 @@ export const BigNumberInput: FC = ({ renderInput({ ...inputProps, inputRef }) ) : ( diff --git a/src/components/sharedComponents/Hash.tsx b/src/components/sharedComponents/Hash.tsx index 5022ee1f..f44594df 100644 --- a/src/components/sharedComponents/Hash.tsx +++ b/src/components/sharedComponents/Hash.tsx @@ -61,7 +61,12 @@ const Hash: FC = ({ aria-label="Copy" /> )} - {explorerURL && } + {explorerURL && ( + + )} ) } diff --git a/src/components/sharedComponents/TokenInput/index.tsx b/src/components/sharedComponents/TokenInput/index.tsx index 60d60626..d558d39b 100644 --- a/src/components/sharedComponents/TokenInput/index.tsx +++ b/src/components/sharedComponents/TokenInput/index.tsx @@ -202,7 +202,10 @@ const TokenInput: FC = ({ showBalance={showBalance} showTopTokens={showTopTokens} > - setIsOpen(false)} /> + setIsOpen(false)} + /> diff --git a/src/components/sharedComponents/ui/CopyButton/index.tsx b/src/components/sharedComponents/ui/CopyButton/index.tsx index 93a2b523..55863799 100644 --- a/src/components/sharedComponents/ui/CopyButton/index.tsx +++ b/src/components/sharedComponents/ui/CopyButton/index.tsx @@ -77,8 +77,13 @@ export const CopyButton: FC = ({ height="fit-content" justifyContent="center" lineHeight="1" - outline="none" padding="0" + _focusVisible={{ + outline: '2px solid', + outlineColor: 'primary.default', + outlineOffset: '2px', + borderRadius: '2px', + }} textDecoration="none" transition="background-color {durations.moderate}, border-color {durations.moderate}, color {durations.moderate" userSelect="none" diff --git a/src/components/sharedComponents/ui/ExternalLink/index.tsx b/src/components/sharedComponents/ui/ExternalLink/index.tsx index feb27ac5..eedbc24d 100644 --- a/src/components/sharedComponents/ui/ExternalLink/index.tsx +++ b/src/components/sharedComponents/ui/ExternalLink/index.tsx @@ -57,8 +57,13 @@ export const ExternalLinkButton: FC = ({ fontWeight="400" height="fit-content" justifyContent="center" - outline="none" padding="0" + _focusVisible={{ + outline: '2px solid', + outlineColor: 'primary.default', + outlineOffset: '2px', + borderRadius: '2px', + }} transition="background-color {durations.moderate}, border-color {durations.moderate}, color {durations.moderate" whiteSpace="nowrap" width="fit-content" diff --git a/src/components/sharedComponents/ui/Modal/index.tsx b/src/components/sharedComponents/ui/Modal/index.tsx index 57b263d7..d72455e6 100644 --- a/src/components/sharedComponents/ui/Modal/index.tsx +++ b/src/components/sharedComponents/ui/Modal/index.tsx @@ -84,7 +84,12 @@ export const Modal: FC = ({ css, children, title, onClose, text, ...restP > {title} - {onClose && onClose()} />} + {onClose && ( + onClose()} + /> + )} {children ? children : 'No contents'} + + Skip to main content +
From 1ae1991fcf77de0b09cc08a5e82babd415bce9fb Mon Sep 17 00:00:00 2001 From: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:01:34 -0300 Subject: [PATCH 2/4] fix(a11y): address review feedback on accessibility fixes - 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 --- src/components/sharedComponents/BigNumberInput.tsx | 3 ++- src/components/sharedComponents/ui/CopyButton/index.tsx | 2 +- src/components/sharedComponents/ui/ExternalLink/index.tsx | 2 +- src/routes/__root.tsx | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/sharedComponents/BigNumberInput.tsx b/src/components/sharedComponents/BigNumberInput.tsx index b3715cd5..20d07889 100644 --- a/src/components/sharedComponents/BigNumberInput.tsx +++ b/src/components/sharedComponents/BigNumberInput.tsx @@ -93,6 +93,7 @@ export const BigNumberInput: FC = ({ const { value } = typeof event === 'string' ? { value: event } : event.currentTarget if (value === '') { + setHasError(false) onChange(BigInt(0)) return } @@ -136,6 +137,7 @@ export const BigNumberInput: FC = ({ } const inputProps = { + 'aria-invalid': (hasError || undefined) as true | undefined, disabled, onChange: updateValue, placeholder, @@ -146,7 +148,6 @@ export const BigNumberInput: FC = ({ renderInput({ ...inputProps, inputRef }) ) : ( diff --git a/src/components/sharedComponents/ui/CopyButton/index.tsx b/src/components/sharedComponents/ui/CopyButton/index.tsx index 55863799..bff35858 100644 --- a/src/components/sharedComponents/ui/CopyButton/index.tsx +++ b/src/components/sharedComponents/ui/CopyButton/index.tsx @@ -85,7 +85,7 @@ export const CopyButton: FC = ({ borderRadius: '2px', }} textDecoration="none" - transition="background-color {durations.moderate}, border-color {durations.moderate}, color {durations.moderate" + transition="background-color {durations.moderate}, border-color {durations.moderate}, color {durations.moderate}" userSelect="none" whiteSpace="nowrap" width="fit-content" diff --git a/src/components/sharedComponents/ui/ExternalLink/index.tsx b/src/components/sharedComponents/ui/ExternalLink/index.tsx index eedbc24d..6fda6159 100644 --- a/src/components/sharedComponents/ui/ExternalLink/index.tsx +++ b/src/components/sharedComponents/ui/ExternalLink/index.tsx @@ -64,7 +64,7 @@ export const ExternalLinkButton: FC = ({ outlineOffset: '2px', borderRadius: '2px', }} - transition="background-color {durations.moderate}, border-color {durations.moderate}, color {durations.moderate" + transition="background-color {durations.moderate}, border-color {durations.moderate}, color {durations.moderate}" whiteSpace="nowrap" width="fit-content" _hover={{ diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index fb6b48ab..7037e400 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -49,6 +49,7 @@ function Root() { direction="column" flexGrow="1" id="main-content" + tabIndex={-1} > From 6a2f37e9494af85b9872780bdb9333bf1b8df9fa Mon Sep 17 00:00:00 2001 From: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com> Date: Mon, 23 Mar 2026 21:00:22 -0300 Subject: [PATCH 3/4] fix(a11y): guard parseUnits in BigNumberInput sync effect against throws 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. --- src/components/sharedComponents/BigNumberInput.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/sharedComponents/BigNumberInput.tsx b/src/components/sharedComponents/BigNumberInput.tsx index 20d07889..d299629e 100644 --- a/src/components/sharedComponents/BigNumberInput.tsx +++ b/src/components/sharedComponents/BigNumberInput.tsx @@ -75,7 +75,15 @@ export const BigNumberInput: FC = ({ if (!current) { return } - const currentInputValue = parseUnits(current.value.replace(/,/g, '') || '0', decimals) + // The input may contain an intermediate/unparseable string while the user is + // typing; guard against a parseUnits throw so an external value update never + // crashes the effect. + let currentInputValue: bigint + try { + currentInputValue = parseUnits(current.value.replace(/,/g, '') || '0', decimals) + } catch { + currentInputValue = BigInt(-1) // sentinel: force the DOM value to be overwritten + } if (currentInputValue !== value) { current.value = formatUnits(value, decimals) From 738bfda1eddc26d57209b7960bf1b91070b610dd Mon Sep 17 00:00:00 2001 From: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:50:14 -0300 Subject: [PATCH 4/4] fix: add default rel="noopener noreferrer" to ExternalLinkButton Also exclude .worktrees/ from vitest to prevent cross-worktree test pollution. --- src/components/sharedComponents/ui/ExternalLink/index.tsx | 2 ++ vite.config.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/components/sharedComponents/ui/ExternalLink/index.tsx b/src/components/sharedComponents/ui/ExternalLink/index.tsx index 6fda6159..4af70c36 100644 --- a/src/components/sharedComponents/ui/ExternalLink/index.tsx +++ b/src/components/sharedComponents/ui/ExternalLink/index.tsx @@ -41,6 +41,7 @@ const LinkSVG: FC> = ({ ...restProps }) => ( export const ExternalLinkButton: FC = ({ children = , css, + rel = 'noopener noreferrer', target = '_blank', ...restProps }: LinkProps) => { @@ -78,6 +79,7 @@ export const ExternalLinkButton: FC = ({ cursor: 'not-allowed', opacity: 0.6, }} + rel={rel} target={target} {...restProps} > diff --git a/vite.config.ts b/vite.config.ts index e050c981..3244648d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -26,6 +26,7 @@ export default defineConfig({ }, test: { environment: 'jsdom', + exclude: ['**/node_modules/**', '.worktrees/**'], globals: true, setupFiles: ['./setupTests.ts'], },