Screen reader Accessibility Improvements#8497
Screen reader Accessibility Improvements#8497matalvernaz wants to merge 21 commits intoSonarr:v5-developfrom
Conversation
- IconButton: remove hardcoded 'Table Options Button' aria-label that was announced on every icon button regardless of purpose; callers now supply their own aria-label via props; icon marked aria-hidden - Modal/ModalHeader: add role="dialog", aria-modal, and aria-labelledby via a new ModalContext so screen readers announce the modal title on open - MonitorToggleButton: add aria-label alongside title for NVDA compatibility - TablePager: add descriptive aria-label to first/previous/next/last page links; pagination icons marked aria-hidden - PageSidebar: change wrapper div to <nav> with aria-label so it appears as a navigation landmark - PageSidebarItem: add aria-current="page" on the active link; sidebar icons marked aria-hidden - TableHeaderCell: add aria-sort (ascending/descending/none) and scope="col" to sortable headers; sort icon marked aria-hidden - en.json: add MainNavigation and PagerGoTo* translation keys Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Every IconButton that previously relied on the hardcoded fallback 'Table Options Button' label now has a meaningful, context-specific aria-label. Labels use existing translate() keys where available and plain strings where translate is not imported in the file. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
stevietv
left a comment
There was a problem hiding this comment.
Some strings are missing the translations, added the suggestions but please check they are the correct keys. also ensure translate is imported where it isn't
| return ( | ||
| <TableRowCell key={name} className={styles.details}> | ||
| <IconButton name={icons.INFO} onPress={handleDetailsPress} /> | ||
| <IconButton name={icons.INFO} aria-label="Details" onPress={handleDetailsPress} /> |
There was a problem hiding this comment.
| <IconButton name={icons.INFO} aria-label="Details" onPress={handleDetailsPress} /> | |
| <IconButton name={icons.INFO} aria-label={translate('Details')} onPress={handleDetailsPress} /> |
| /> | ||
|
|
||
| <IconButton name={icons.ADD} onPress={handleAddPress} /> | ||
| <IconButton name={icons.ADD} aria-label="Add" onPress={handleAddPress} /> |
There was a problem hiding this comment.
| <IconButton name={icons.ADD} aria-label="Add" onPress={handleAddPress} /> | |
| <IconButton name={icons.ADD} aria-label={translate('Add')} onPress={handleAddPress} /> |
| <IconButton | ||
| className={styles.editButton} | ||
| name={icons.EDIT} | ||
| aria-label="Edit" |
There was a problem hiding this comment.
| aria-label="Edit" | |
| aria-label={translate('Edit')} |
| {isNew ? null : ( | ||
| <IconButton | ||
| name={icons.REMOVE} | ||
| aria-label="Remove" |
There was a problem hiding this comment.
| aria-label="Remove" | |
| aria-label={translate('Remove')} |
| onTableOptionChange={onTableOptionChange} | ||
| > | ||
| <IconButton name={icons.ADVANCED_SETTINGS} /> | ||
| <IconButton name={icons.ADVANCED_SETTINGS} aria-label="Advanced Settings" /> |
There was a problem hiding this comment.
| <IconButton name={icons.ADVANCED_SETTINGS} aria-label="Advanced Settings" /> | |
| <IconButton name={icons.ADVANCED_SETTINGS} aria-label={translate('AdvancedSettings')} /> |
assuming AdvancedSettings is a translation key
| <SpinnerIconButton | ||
| name={icons.REFRESH} | ||
| spinningName={icons.REFRESH} | ||
| aria-label="Run" |
There was a problem hiding this comment.
| aria-label="Run" | |
| aria-label={translate('Run')} |
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
|
|
||
| export const urlBase = window.Sonarr.urlBase; | ||
| export const apiRoot = '/api/v5'; // window.Sonarr.apiRoot; | ||
| export const apiRoot = window.Sonarr.apiRoot; |
There was a problem hiding this comment.
why are you making this change in a screen reader accessibilty PR? we are not ready for this change until the full api is migrated to v5, this change essentially puts us back to v3.
There was a problem hiding this comment.
please remove this added docker file, I assume this is for your testing only.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Revert fetchJson.ts apiRoot back to hardcoded /api/v5 per reviewer request; dynamic apiRoot change is premature until full v5 API migration - Remove Dockerfile (was testing artifact, not intended for the PR) Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Underlay link now announces series title (existing) or "Add {title}" (new)
- TVDB external link gets aria-label "View {title} on TVDB"; icon marked aria-hidden
- Added ViewSeriesOnTvdb translation key to en.json
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
|
Hey, I just wanted to apologize for my last very messy pr, and for the AI code submissions in general. I have a bit of coding knowledge and access to an AI, but have never really used github which is a dangerous combo in the best case. I was adding a new show and getting annoyed, not really doing any research whatsoever before I made the PR in the first place. I really appreciate that people are willing to possibly look into adding this, and want to make it as easy as possible for everybody involved. If there's anything else I can do to help with that including screwing off and staying out of the way, please let me know. |
In my opinion the aria-improvements are very welcome, indeed sonarr doesn't have much A11y consideration in place. |
|
I'm testing locally on my instance with firefix, chrome and edge using both jaws and NVDA. I'm still adding missing labels when I find them but it's definitely better. |
markus101
left a comment
There was a problem hiding this comment.
I agree, the changes are very welcomed, but Claude needs to have it's hand-held to actually do things correctly, there are several issues that would be caught by the linter if that was run prior to submission and some random things that were changed for seemingly no reason like the API root change Stevie caught and another because it decided UI settings was using the wrong path.
| ? icons.SORT_ASCENDING | ||
| : icons.SORT_DESCENDING; | ||
|
|
||
| const ariaSortValue = isSorting |
There was a problem hiding this comment.
This fails linting. Do not nest ternary expressions
There was a problem hiding this comment.
Something like this would be much more readable.
const ariaSortValue = useMemo(() => {
if (!isSortable) {
return undefined;
}
if (!isSorting) {
return 'none';
}
return sortDirection === sortDirections.ASCENDING
? 'ascending'
: 'descending';
}, [isSorting, sortDirection, isSortable]);
|
|
||
| const ariaSortValue = isSorting | ||
| ? sortDirection === sortDirections.ASCENDING | ||
| ? ('ascending' as const) |
There was a problem hiding this comment.
Why is everything using as const?
| } | ||
|
|
||
| const PATH = '/settings/ui'; | ||
| const PATH = '/config/ui'; |
| onTableOptionChange={onTableOptionChange} | ||
| > | ||
| <IconButton name={icons.ADVANCED_SETTINGS} /> | ||
| <IconButton name={icons.ADVANCED_SETTINGS} aria-label="Advanced Settings" /> |
Addresses PR review from markus101: nested ternary failed linting, replaced with useMemo block per suggested pattern. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Addresses PR review from markus101: hardcoded string replaced with
translate('AdvancedSettings'); import added.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
The /config/ui path was incorrect; /settings/ui is consistent with all other settings hooks (general, mediamanagement, naming, etc.). Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
|
I've addressed these fixes, and everything seems to be working in my test instance. I've tested with chrome and firefox, jaws and NVDA. Everything seems to be working and the logs aren't screaming at me, so there's hope. |
just want to make sure you also ran |
Remove event.preventDefault() from label onClick so screen readers receive a real DOM state change when a checkbox is toggled. ShiftKey is captured via a ref on the label click (which fires before onChange) so shift-range-select on table rows continues to work. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Auto-fix prettier formatting and import sort order across all files flagged by yarn lint. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
|
Yes, just ran that and fixed a bunch of formatting issues. |
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
|
Please remove all the recent changes around build scripts and CI. GitHub actions already runs linting as part of the build process. |
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
|
Done. Sorry about that, claude ran along with an idea I wasn't wanting. |
markus101
left a comment
There was a problem hiding this comment.
Shift clicking ranges of check inputs is broken and I'm not really sure what the purpose of the modal header ID is.
| import styles from './CheckInput.css'; | ||
|
|
||
| interface ChangeEvent<T = Element> extends SyntheticEvent<T, MouseEvent> { | ||
| interface ChangeEvent<T = Element> extends React.SyntheticEvent<T, MouseEvent> { |
There was a problem hiding this comment.
This should use SyntheticEvent directly, like it did before.
| }, | ||
| [isDisabled, toggleChecked] | ||
| ); | ||
| const handleLabelClick = useCallback((event: React.MouseEvent) => { |
| [name, value, checkedValue, uncheckedValue, onChange] | ||
| ); | ||
|
|
||
| const handleClick = useCallback( |
There was a problem hiding this comment.
This broke using shift-clicking to change a whole range of check inputs, why was it changed?
| return null; | ||
| } | ||
|
|
||
| const headerId = `${modalId}-header`; |
There was a problem hiding this comment.
What is the purpose of the headerId? Seems like it's just passed to the header's div, but not clear what that actually provides.
If the modal needs to pass an ID down then it should just pass the ID, such as modalId, then consumers can use it as needed, right now if the body also needed an ID then you'd have to pass bodyId even though it's the same ID and a different suffix.
If this isn't needed then it should be removed though.
There was a problem hiding this comment.
the headerId is what aria-labelledby on the dialog points to (line 184), and ModalHeader sets the same id on the visible h2 through ModalContext. without that link the dialog has no accessible name, so screen readers just say "dialog" with nothing else.
I can rename it to something less generic like modalHeaderId if that reads better, or pass the raw modalId through context and have ModalHeader build its own suffix. let me know which you'd prefer and I'll change it.
There was a problem hiding this comment.
Gotcha, I think this actually makes sense as-is after looking at it again so we don't potentially create different header IDs in different places.
| if (!isSortable) { | ||
| return undefined; | ||
| } | ||
| if (!isSorting) { |
There was a problem hiding this comment.
New line needed before if block
| if (!isSorting) { | ||
| return 'none'; | ||
| } | ||
| return sortDirection === sortDirections.ASCENDING |
| {children} | ||
|
|
||
| {isSorting && <Icon name={sortIcon} className={styles.sortIcon} />} | ||
| {isSorting && ( |
CheckInput: revert label onClick to handleClick with preventDefault so event.nativeEvent.shiftKey is read directly in onChange, restoring shift-range selection on table check inputs. Drops the shiftKeyRef hack and the React.SyntheticEvent/React.MouseEvent qualifications. TableHeaderCell: blank lines before if/return inside the useMemo, ternary with null instead of && for the sort icon. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Sonarr is a great tool for me even though I'm completely blind. Right now the accessibility kinda sucks though so I wanted to try and make it better.
What's fixed
Buttons weren't labeled for my screen reader, aria labels have been added to fix that. Tables were unclear, and have been clarified a bit; at least from what I can tell from limited local testing. More images have been given alt text, so it doesn't just say iconx360.png or something equally unhelpful. Translations updated for aria labels.
Changes
No visual changes.