The list was a stack of clickable divs. Screen readers heard nothing when
j or k moved the selection, every row added several Tab stops (Compose
sat 253 Tabs from the top), and a deleted message left the focus on the
page.
The list is now a treegrid named "Messages": one Tab stop, rows named in
one sentence (unread, sender, subject, date, flags, and the preview when
that setting is on), conversations with aria-expanded and their
messages a level down, position and list size on every row. j, k and the
arrows move the focus with the selection, Space ticks a row, Enter opens
it, Shift+F10 or the menu key opens its actions, and after a delete the
focus lands on the next row. With the actions button setting on, each
row shows a button for the same menu, for pointers that can't hover or
right-click.
Pasted and dropped images used to get the file name as alt text, and
there was no way to change it. Now a small dialog asks what the image
shows right after it is inserted, with a "Decorative image" option that
writes alt="". An image already in the text can be described from the
toolbar or from the ALT button on the selected image. The plain-text
part of an HTML mail writes described images as "[Image: ...]" and
leaves decorative ones out.
The formatting toolbar now also re-renders when only the selection
changes, so its pressed states and the image button follow the caret.
A plugin's results to a search came all at once after its handler answered,
with nothing before: the block then appeared above the message list and
pushed it down, under a pointer about to click a row. onProvideSearchResults
handlers that search elsewhere (a CRM's search by meaning) take seconds.
Each plugin's answer now shows as it comes, and the store keeps the plugins
still asked. One that has not answered after 300 ms gets the block at a fixed
height with placeholder rows under "Searching for more results…"; its
entries then fill that height, so the list moves once, with its own results.
A plugin that answers at once (one not searching for this query) shows
nothing.
An entry from onProvideSearchResults may wrap what matched in <mark>, as
SearchSnippet/get does, and the block above the message list highlights
it with the list's own SearchSnippetText. The host keeps the marks,
escapes everything else and cuts the snippet by its text, closing a mark
it cuts, so the snippet is still never rendered as HTML.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A draft does not have to come from the composer: the JMAP API, another
client, an automation or a template can leave one in Drafts, and its
paragraphs carry no style, so every mail client lays them out with the
user agent's 1em margins. Since #1340 the composer stamps its own 4px
margin on every paragraph it serializes, so opening such a draft to read
it over and pressing Send re-flowed the message, and the composer showed
it at a spacing the recipient would never see.
What is loaded now keeps the spacing it came with: a paragraph with text
and no margin of its own is stamped with the mail default on the way in,
so the editor shows the message's own layout and what goes out is what
came in. The empty paragraph a new message is scaffolded with is left
alone, as are table cells, the quoted original and an embedded
signature. addParagraphSpacing skips a paragraph that already states a
margin, so nothing is declared twice.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>