Files
Ben GotowandClaude 21bb56a4a9 Implement comprehensive accessibility improvements across Mailspring (#2638)
* Add 8 screen reader accessibility implementation plans

Research and detailed implementation plans for each WCAG/a11y best practice
area identified in the initial accessibility audit of the Mailspring codebase.
Each plan includes file-by-file change lists, code snippets, CSS impact
analysis, and implementation sequencing.

Plans cover:
01. Semantic HTML landmarks (<nav>, <main>, <aside>, <article>, etc.)
02. ARIA attributes for interactive widgets (toolbar buttons, toggles, dropdowns)
03. Icon button accessible names and image alt text (RetinaImg fix)
04. Keyboard tab order and tabIndex fixes (roving tabindex toolbar)
05. List and tree semantics (thread list listbox, sidebar tree widget)
06. Focus trap for modals and popovers (useFocusTrap hook)
07. aria-live regions for dynamic content (Announcer singleton service)
08. Form label associations and form accessibility (tokenizing fields, Switch)

https://claude.ai/code/session_01MFM4iawNs5uYSxfBbYp3Vk

* Implement accessibility plans 01-03: semantic HTML, ARIA, and icon labels

Plan 01 — Semantic HTML landmarks:
- sheet-container.tsx: toolbar div→<header role="banner">, sheet area div→<main>, footer div→<footer>
- sheet.tsx: add role/aria-label to column containers (complementary, region)
- sheet-toolbar.tsx: add role="toolbar" + aria-label to per-column toolbar divs
- account-sidebar.tsx: account-sidebar-sections div→<nav aria-label="Mailboxes">
- message-list.tsx: outer div→<section aria-label={subject}>, subject wrap→<header>
- normalize.less: add display:block reset for semantic HTML5 elements

Plan 02 — ARIA attributes for interactive widgets:
- switch.tsx: add role="switch", aria-checked, tabIndex, keyboard handler
- button-dropdown.tsx: add role="button", aria-haspopup, aria-expanded to pickers
- disclosure-triangle.tsx: add role="button", aria-expanded, label prop
- multiselect-list.tsx: add role="listbox", aria-multiselectable, aria-label to container
- outline-view.tsx: add aria-label to section

Plan 03 — Icon accessible names:
- retina-img.tsx: change default alt from filename to "" (fixes all ~420 usages)
- thread-toolbar-buttons.tsx: add aria-label to Archive, Trash, Spam, Star, Unread buttons

https://claude.ai/code/session_01MFM4iawNs5uYSxfBbYp3Vk

* a11y: Add semantic HTML landmark elements for screen reader navigation

Implements Plan 01 from docs/a11y-plan-01-semantic-landmarks.md. Converts
generic div containers to semantic HTML5 landmark elements so screen reader
users can navigate by landmarks (JAWS R key, NVDA D key, VoiceOver rotor).

Changes by file:
- app/static/style/normalize.less: Add defensive CSS reset for semantic
  elements (display:block, margin:0, padding:0)
- app/src/sheet-container.tsx: toolbar div→<header role="banner">,
  sheet area div→<main aria-label="Email workspace">,
  footer div→<footer> (already applied in worktree)
- app/src/sheet-toolbar.tsx: Add COLUMN_ARIA_LABELS map and role="toolbar"
  aria-label to per-column toolbar divs (already applied in worktree)
- app/src/sheet.tsx: Add COLUMN_META map; pass role and aria-label to
  ResizableRegion and InjectedComponentSet column containers
- app/internal_packages/account-sidebar/lib/components/account-sidebar.tsx:
  div.account-sidebar-sections → <nav aria-label="Mailboxes">
- app/internal_packages/message-list/lib/message-list.tsx:
  div#message-list → <section aria-label={thread.subject}>;
  div.message-subject-wrap → <header>
- app/internal_packages/message-list/lib/message-item.tsx:
  outer div in _renderFull() → <article aria-label="Message from {sender}">
- app/internal_packages/message-list/lib/sidebar-plugin-container.tsx:
  div.sidebar-section → <aside aria-label="Contact information">
- app/internal_packages/composer/lib/composer-view.tsx:
  div.composer-action-bar-wrap → <footer>
- app/internal_packages/composer/lib/composer-header.tsx:
  div.composer-header → <header aria-label="Message addressing">

https://claude.ai/code/session_01MFM4iawNs5uYSxfBbYp3Vk

* a11y: Add ARIA roles and states to shared UI components (Plan 02)

- account-switcher.tsx: add role="button", aria-label, aria-haspopup, tabIndex
- preferences-tabs-bar.tsx: add role="tablist", role="tab", aria-selected, roving tabIndex
- thread-search-bar.tsx: add role="combobox", aria-expanded, aria-haspopup; suggestions get role="listbox"/role="option"/aria-selected
- tokenizing-contenteditable.tsx: accessibility improvements for token input
- multiselect-list.tsx: role="option" and aria-selected on list items
- outline-view-item.tsx: role="treeitem", aria-selected, aria-label on items; role="group" on children
- outline-view.tsx: aria-label on section, role="button"/aria-expanded on collapse/add spans

https://claude.ai/code/session_01MFM4iawNs5uYSxfBbYp3Vk

* chore: ignore .claude/worktrees/ agent isolation directory

https://claude.ai/code/session_01MFM4iawNs5uYSxfBbYp3Vk

* Add accessible names to icon-only buttons and fix RetinaImg alt text (Plan 03)

- Change RetinaImg default alt from filename to "" to stop screen readers
  from announcing filenames like "toolbar-archive.png, image"
- Add aria-label to all icon-only toolbar buttons across the app
- Add aria-hidden="true" to decorative RetinaImg inside labeled buttons
- Fix div-as-button elements with role="button", tabIndex, and keyboard handlers
- Add aria-pressed to toggle buttons (star icon in thread list)
- Move title from RetinaImg to wrapper divs in subject-line-icons.tsx
- Add role="toolbar" and aria-label to column toolbar divs in sheet-toolbar.tsx

Files changed:
- app/src/components/retina-img.tsx
- app/internal_packages/thread-list/lib/thread-toolbar-buttons.tsx
- app/internal_packages/thread-list/lib/thread-list-icon.tsx
- app/internal_packages/thread-list/lib/thread-list-quick-actions.tsx
- app/internal_packages/message-list/lib/find-in-thread.tsx
- app/internal_packages/message-list/lib/message-controls.tsx
- app/internal_packages/message-list/lib/subject-line-icons.tsx
- app/internal_packages/composer/lib/compose-button.tsx
- app/internal_packages/composer/lib/send-action-button.tsx
- app/internal_packages/composer/lib/composer-header-actions.tsx
- app/internal_packages/draft-list/lib/draft-toolbar-buttons.tsx
- app/internal_packages/category-picker/lib/toolbar-category-picker.tsx
- app/internal_packages/contacts/lib/ContactDetailToolbar.tsx
- app/internal_packages/mode-switch/lib/mode-toggle.tsx
- app/src/sheet-toolbar.tsx
- app/internal_packages/main-calendar/lib/core/header-controls.tsx
- app/internal_packages/main-calendar/lib/quick-event-button.tsx
- app/internal_packages/activity/lib/list/activity-list-button.tsx
- app/internal_packages/open-tracking/lib/open-tracking-icon.tsx
- app/src/components/metadata-composer-toggle-button.tsx
- app/internal_packages/composer-grammar-check/lib/grammar-check-toggle.tsx
- app/internal_packages/composer-templates/lib/template-picker.tsx

https://claude.ai/code/session_01MFM4iawNs5uYSxfBbYp3Vk

* a11y: Add listbox/option roles to thread list and tree/treeitem roles to sidebar (Plan 05)

https://claude.ai/code/session_01MFM4iawNs5uYSxfBbYp3Vk

* a11y: Associate form labels and add aria-labels to all form inputs (Plan 08)

- Phase 1: Wrap bare title strings with localized() in onboarding IMAP form;
  fix duplicate id on custom port input with unique id and sr-only label
- Phase 2: Add sr-only label for composer subject input (id="composer-subject")
- Phase 3: TokenizingTextField - convert div label to <label htmlFor>, generate
  stable _inputId, pass id to SizeToFitInput for To/Cc/Bcc fields
- Phase 4: Add aria-label/role/aria-autocomplete to thread search bar;
  add aria-label to find-in-thread input; add aria-* prop pass-through to
  TokenizingContenteditable
- Phase 5: Add id={keyPath} to config-schema-item select to match label htmlFor
- Phase 6: Replace <h6> visual labels with <label htmlFor> for account label,
  sender name, account color inputs; add aria-label to AutoaddressControl inputs
- Phase 7: Add sr-only label and aria-label to AppearanceScaleSlider range input
- Phase 8: Add sr-only label+id to language select; add sr-only label+id to
  mail rules account picker select
- Phase 9: Fix signature title (id="signature-title"), body textarea
  (id="signature-body") with sr-only labels; add htmlFor to DataShape labels;
  add sr-only label to photo URL input; add sr-only label to template name input
- Phase 10: Add aria-label to calendar event title input, notes textarea,
  and location/video call input
- Phase 11: Convert ContactDetailEdit sibling labels to implicit label wrapping
  for all fields; replace empty <label> spacers with aria-hidden spans;
  convert YYMMDDInput to fieldset/legend pattern with aria-labels per subfield
- Phase 12: Extend Switch component with role="switch", aria-checked, tabIndex,
  aria-label, aria-labelledby props and keyboard (Space/Enter) support;
  pass label to AllDayToggle Switch
- Phase 13: Replace deprecated jsx-a11y/label-has-for rule with
  jsx-a11y/label-has-associated-control in .eslintrc
- Add .sr-only utility CSS class to app/static/style/utilities.less

https://claude.ai/code/session_01MFM4iawNs5uYSxfBbYp3Vk

* a11y: Fix tabIndex usage, implement roving toolbar tabindex, add focus-visible CSS (Plan 04)

https://claude.ai/code/session_01MFM4iawNs5uYSxfBbYp3Vk

* Fix localized import

* Tons of nits

* Indent feedback

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-05 23:15:09 -06:00

334 lines
7.7 KiB
Plaintext

//
// Scaffolding
// --------------------------------------------------
// Reset the box-sizing
//
// Heads up! This reset may cause conflicts with some third-party widgets.
// For recommendations on resolving such conflicts, see
// http://getbootstrap.com/getting-started/#third-box-sizing
* {
box-sizing: border-box;
}
*:before,
*:after {
box-sizing: border-box;
}
// Reset fonts for relevant elements
input,
button,
select,
textarea {
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
// Figures
//
// We reset this here because previously Normalize had no `figure` margins. This
// ensures we don't break anyone's use of the element.
figure {
margin: 0;
}
// Images
img {
vertical-align: middle;
border: 0;
}
/*! normalize.css v3.0.1 | MIT License | git.io/normalize */
// * 1. Set default font family to sans-serif.
// * 2. Prevent iOS text size adjust after orientation change, without disabling user zoom.
html {
font-family: sans-serif;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
// Remove default margin.
body {
margin: 0;
}
// HTML5 display definitions ==========================================================================
// * Correct `block` display not defined for any HTML5 element in IE 8/9.
// * Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox.
// * Correct `block` display not defined for `main` in IE 11.
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
nav,
section,
summary {
display: block;
}
// * 1. Correct `inline-block` display not defined in IE 8/9.
// * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
audio,
canvas,
progress,
video {
display: inline-block;
vertical-align: baseline;
}
// * Prevent modern browsers from displaying `audio` without controls.
// * Remove excess height in iOS 5 devices.
audio:not([controls]) {
display: none;
height: 0;
}
// Links ==========================================================================
// * Remove the gray background color from active links in IE 10.
a {
background: transparent;
// * Improve readability when focused and also mouse hovered in all browsers.
&:active,
&:hover {
outline: 0;
}
}
// Text-level semantics ==========================================================================
// * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
abbr[title] {
border-bottom: 1px dotted;
}
// * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
b,
strong {
font-weight: bold;
}
// * Address styling not present in Safari and Chrome.
dfn {
font-style: italic;
}
// * Address variable `h1` font-size and margin within `section` and `article` contexts in Firefox 4+, Safari, and Chrome.
h1 {
font-size: 2em;
margin: 0.67em 0;
}
// * Address styling not present in IE 8/9.
mark {
background: #ff0;
color: #000;
}
// * Address inconsistent and variable font size in all browsers.
small {
font-size: 80%;
}
// * Prevent `sub` and `sup` affecting `line-height` in all browsers.
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
// * Correct overflow not hidden in IE 9/10/11.
svg:not(:root) {
overflow: hidden;
}
// * Address differences between Firefox and other browsers.
hr {
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}
// * Contain overflow in all browsers.
pre {
overflow: auto;
}
// * Address odd `em`-unit font size rendering in all browsers.
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
// Forms ==========================================================================
// * Known limitation: by default, Chrome and Safari on OS X allow very limited
// * styling of `select`, unless a `border` property is set.
// * 1. Correct color not being inherited.
// * Known issue: affects color of disabled elements.
// * 2. Correct font properties not being inherited.
// * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
button,
input,
optgroup,
select,
textarea {
color: inherit;
font: inherit;
margin: 0;
}
// * Address `overflow` set to `hidden` in IE 8/9/10/11.
button {
overflow: visible;
}
// * Address inconsistent `text-transform` inheritance for `button` and `select`.
// * All other form control elements do not inherit `text-transform` values.
// * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
// * Correct `select` style inheritance in Firefox.
button,
select {
text-transform: none;
}
// * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls.
// * 2. Correct inability to style clickable `input` types in iOS.
// * 3. Improve usability and consistency of cursor style between image-type `input` and others.
button,
html input[type='button'] {
-webkit-appearance: button;
cursor: pointer;
}
// * Re-set default cursor for disabled elements.
button[disabled],
html input[disabled] {
cursor: default;
}
// * Remove inner padding and border in Firefox 4+.
button input {
&::-moz-focus-inner {
border: 0;
padding: 0;
}
}
// * Address Firefox 4+ setting `line-height` on `input` using `!important` in the UA stylesheet.
input {
line-height: normal;
&[type='reset'],
&[type='submit'] {
-webkit-appearance: button;
cursor: pointer;
}
// * It's recommended that you don't attempt to style these elements.
// * Firefox's implementation doesn't respect box-sizing, padding, or width.
// * 1. Address box sizing set to `content-box` in IE 8/9/10.
// * 2. Remove excess padding in IE 8/9/10.
&[type='checkbox'],
&[type='radio'] {
box-sizing: border-box;
padding: 0;
}
// * Fix the cursor style for Chrome's increment/decrement buttons. For certain
// * `font-size` values of the `input`, it causes the cursor style of the
// * decrement button to change from `default` to `text`.
&[type='number'] {
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
height: auto;
}
}
// * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
// * 2. Address `box-sizing` set to `border-box` in Safari and Chrome (include `-moz` to future-proof).
&[type='search'] {
-webkit-appearance: textfield;
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box;
box-sizing: content-box;
// * Remove inner padding and search cancel button in Safari and Chrome on OS X.
// * Safari (but not Chrome) clips the cancel button when the search input has
// * padding (and `textfield` appearance).
&::-webkit-search-cancel-button,
&::-webkit-search-decoration {
-webkit-appearance: none;
}
}
}
// * Define consistent border, margin, and padding.
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
// * 1. Correct `color` not being inherited in IE 8/9/10/11.
// * 2. Remove padding so people aren't caught out if they zero out fieldsets.
legend {
border: 0;
padding: 0;
}
// * Remove default vertical scrollbar in IE 8/9/10/11.
textarea {
overflow: auto;
}
// * Don't inherit the `font-weight` (applied by a rule above).
// * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
optgroup {
font-weight: bold;
}
// Tables ==========================================================================
// * Remove most spacing between table cells.
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}
// Reset semantic elements to block display (defensive reset)
main,
section,
article,
aside,
nav,
header,
footer {
display: block;
margin: 0;
padding: 0;
}