Repository navigation
v0.14.0
Added
Accessibility
-
teksilo_platform::key_report: each key reported to the AT-SPI registry.
KeyReportGatereports every key press and release to
org.a11y.atspi.DeviceEventController.NotifyListenersSyncwhile an
assistive technology is attached, and says which keys the screen reader
took, so they are not dispatched.teksilo-appruns everyKeyboardInput
through it; Linux only, it does nothing on Windows and macOS. -
tools/reader/: what a screen reader gets from an example, recorded.
tools/reader/reader.pyruns an example in a private, invisible desktop
session (its own D-Bus, AT-SPI bus, KWin and runtime directory, no
DISPLAY, no sound) and records the AT-SPI events it emits, the tree a
reader walks, and what Orca 46.1 says, speaking to nothing. Acts are real
key presses through the private compositor, AT-SPI actions, or the
automation bridge. Scenarios state what a reader should get;tabwalkand
treeneed no scenario. Linux only. See
docs/a11y/reader-harness.md. -
Widget::accessibility_proxy: a composite can publish itself through the
field that holds focus. Where a composite keeps its focus, text and value
on one inner widget, that widget is the node a screen reader lands on and has
to carry the composite's name, while an application can only reach the
composite's id. Returning the inner widget from the hook makes the tree apply
everything attached to the composite (its overrides, aFormLayout's
labelled_by,access_described_by, the tooltip it owns) to that widget's
node, after the widget's own, and point relations naming the composite at
it. The overrides guide has a section on it. -
accessibility::audit::focusable_nodes_hidden: a control that takes
focus inside a hidden subtree. A hidden node hides everything under it
from every platform adapter, and only the focused node is let back through,
alone, so a reader lands on the control and finds nothing around it. The
audit lists every node that offersAction::Focuswhile hidden, itself or
through an ancestor, and is not disabled. The widget previewer's catalog
census now runs it over every widget and documentation snippet. Beside it,
audit::nodes_in_filtered_treelists the nodesaccesskit_consumer's
filter keeps, which are the ones a platform adapter walks and can
announce. -
RichTextEditor,CodeEditor,PlainTextEditorandLogViewtake a
.label(..). It names the text itself, which is what a screen reader
lands on and announces ("Notes, entry", "Build output, document"), and stays
locale-reactive. Before, nothing could name that text: a name given to the
widget went to its outer node, not to the text a reader reads. -
HandlerSet::access_customize: a widget can finish the accessibility
of a node it reaches by id. The twin of
WidgetWithHandlers::access_customize, for the nodes a widget writes to
throughBuildContext::apply_handlers, and chained after any customization
the node already carries. -
BuildContext::first_focus_capable_descendant: the control a wrapper
stands for, even while it is disabled. Beside
first_focusable_descendant, which finds only what can take focus now,
this finds the first widget in a subtree that is declared focusable, so a
control disabled as it mounts is still recognised as the control.
Internationalization
- A formatted date can name its weekday, or stop at the month.
TeksiloDateTimeFormatter::date_fields(DateFields)picks which fields the
date part names:YearMonthDay, the default and what every date style
rendered before;YearMonthDayWeekday, CLDR's full date atDateStyle::Long
("lundi 31 août 2026", "Monday, August 31, 2026"); orYearMonth, a month
with its year ("août 2026"). ICU picks the pattern for the whole combination,
so the order and the grammar are the locale's: a Russian month is genitive
after a day and nominative beside its year alone.format_in_locale(value, &lang)renders one value once, as a plainString, in a locale the caller
names, for a string computed on demand, such as an accessibility name.
calendar_system(CalendarSystem::Gregorian)keeps the date on the Gregorian
calendar where CLDR gives the locale another one: by defaultfa-IRwrites
24 September 2026 as the 2nd of Mehr 1405, andth-THcounts its year 2569.
Core
EventContext::context_menu_trigger: what asked for a context menu. A
context_menufactory is handed a point whatever opened the menu, and only
a pointer's is one the user chose. Inside the factory,
ctx.context_menu_trigger()now saysContextMenuTrigger::Pointer(a
secondary click, or a hold),Keyboard(the Menu key, Shift+F10, Ctrl+Shift+M
on macOS) orAccessibility(an assistive technology'sShowContextMenu),
so a factory can act on the point, moving a caret to it or picking the item
under it, only when the user pointed there.
Changed
Data views
- A single-selection
TreeViewover an indexSelectionModelkeeps the
selected row selected through a structural change. An insert, a removal or
a collapse above it used to leave the selection at its old position, on
whichever row moved there. The selection now moves with its row, as a keyed
selection does. Multiple selections are unchanged. Behaviour change.
Fixed
Core
-
Tabbing away from a control whose tooltip was showing sent focus back to
it. Once focus had brought up a control's rich or composite tooltip, Tab
moved on to the next control and then, about 120 ms later as the tooltip
faded out, focus returned to the control just left: a screen reader's
reading of the next control was cut, the old control was read again, and
only a second Tab got away. Tabbing out of a tooltip that had turned sticky
did the same. Focus now stays where Tab put it. ASnackbarthat times out
no longer sends focus back to its trigger after the user has moved on
either. -
Orca heard no key typed into a Teksilo window in a Wayland session.
Orca 46 reads no keyboard there: it learns of a key only when the
application reports it to the AT-SPI registry, as GTK and Qt do, and Teksilo
did not. So Orca said nothing when an arrow key moved the caret, echoed no
typing, and none of its own commands ran (Orca+T, which says the time): the
keys went to the application instead. On Linux, while an assistive
technology is attached, each key press and release is now reported to the
registry before the application sees it, and a key the screen reader takes
for one of its commands no longer reaches the application, nor does its
release. A key typed into a secure field (aPasswordField) is reported
without its character: its physical key and modifiers only, which the screen
reader's own commands need, so a program listening to the registry can still
tell which keys were pressed there. GTK and Qt report the character too. A
press waits at most 200 ms for the registry's answer. A registry that stops
answering costs one such pause, however many keys follow; one that answers
every key more than 200 ms late costs up to 200 ms on each key typed after
it has caught up. What a reader hears on an X11 session, where Orca reads
the keyboard itself, on Windows and on macOS is unchanged. -
An empty text measured by text-typeset had no text run.
TextRunSource::from_geometrybuilt no line from the geometry text-typeset
returns for an empty string, so an empty label or field laid out by it
offered no Text interface on AT-SPI and none of the text ranges the other
platforms read. It now emits the one empty runTextRunSource::flatalways
did, so the first text such a node gains, and the edit that empties it, are
reported like any other change. -
A composite that takes focus itself was announced by its structural
node. Where a focusable widget hands its accessibility node to a
descendant (Widget::accessibility_proxy), a screen reader now lands on
that descendant when the widget takes focus, hears its name and content, and
is told of the caret and selection moves AT-SPI reports only for the focused
node. A screen reader's own request to focus that descendant puts the
keyboard on the composite, where its caret, input method and focus ring
live, and a context menu the composite owns is offered on it. -
Focus that a rebuild took away came back in the wrong place. When a
widget rebuilt itself and the control that had focus went with it, focus
went to the first control of the whole rebuilt widget. For a widget that
keeps its parts across a rebuild (a toast stack, a tab widget, a docking
layout) that was the first part, whichever one the user was in. Focus now
comes back in the innermost part that is still there. -
A screen reader could act on the page behind a modal. While an in-tree
modal was up (aMessageBox, aDialog, anyCenteredoverlay), its scrim
kept the pointer off the page and Tab stayed inside it, but a screen
reader's own requests still reached the controls behind it: an AT-SPI click
behind "Save changes?" opened a second box over it, a click behind "Close
window?" toggled the document's checkbox so that the next close quit with no
question, and a focus request put the keyboard on a control the box covers.
An assistive-technology action aimed behind the modal is now refused and
reported unhandled, so the reader's click does nothing and focus stays in
the modal. The modal's own controls, and a menu or drop-down opened over it,
take every request as before. The page stays in the tree, readable, and is
as alive to the reader once the modal closes as before it opened. The
automation bridge answers such a request (invoke_action,focus_node,
set_value,expand,collapse) with its unhandled-action error. -
Every window was an unnamed "frame" to Orca. The accessibility tree's
root, which AT-SPI presents as the window, carried no name, so Orca 46.1
announced each window as it came up with the bare word "frame". The root now
takes the window's title, and follows it when the title changes. Windows and
macOS read the title from the native window and are unchanged. -
Orca heard only the first message the framework's announcer said. Every
announcein every Teksilo application, after the first of a session, was
dropped by Orca 46.1, and even a first one was lost whenever Orca handled the
event after its node had gone. Each message is now spoken from a node the
tree has never had, which stays in the tree for five seconds
(teksilo_core::announcer::LINGER) and then leaves for good. A burst keeps
at most eight in the tree at once. The tree no longer carries two hidden
announcer nodes while nothing is being said, so aTreeUpdateof an idle
tree has two nodes fewer. -
A control a reader had met went silent on Orca once it left and came
back. A tab page shown again, a reopened menu, combo list or date picker
calendar, a snackbar, tooltip or Caps Lock warning shown a second time, a
page of the widget catalog visited twice, a chart or combo box scrolled back
into view, an editor focus came back to: each returned to the platform under
the id it had left with, which the AT-SPI adapter had announced defunct as
it left, and Orca 46.1 dropped every event from it, focus included, so the
reader heard nothing from the second visit on. Anything that leaves the tree
a reader sees and comes back now reaches the platform under an id it has
never had, and is heard as it was the first time. The ids
sync_accessibilityreturns, which the automation bridge and tests read,
are unchanged. An integration that feeds an AccessKit adapter itself hands
itWidgetTree::deliver_accessibilityand routes the adapter's action
requests throughWidgetTree::resolve_adapter_action. Windows and macOS
keep no defunct state, and nothing changes there that a reader can tell. -
An announcement made as focus moved was cut before it was heard. A
keyboard move of a row or a tab ("Moved to 2 of 3", "Doc 1 moved to 5 of 6",
"Moved to level 2") puts focus on the moved item and says where it went, and
Orca 46.1 stopped the message to read the item: no move in aListView,
TreeView,GridViewor tab bar was ever heard whole. A message the
announcer is asked for as focus moves, including to a list's new current
row, now reaches the screen reader after the focus change, one frame later,
so the reader hears the item and then the message. A field focused as the
message is announced leaves the message out of its description while focus
stays, as it already did where the reader keeps the announcement. -
The announcement ring recorded text no platform announces.
WidgetTree::announcements_since, and the automation bridge's
pull_announcementsthat reads it, recorded a live node'svaluebefore
its label, from every live node the walk emitted, whenever that text changed.
Every AccessKit adapter announces a node's name instead, which is its value
only for aRole::Label, and only for a node in the filtered tree. So a
hidden live region, one inside a hidden subtree, and aStatusorAlert
carrying its text as a value were all recorded while every platform stayed
silent, and a probe reading the ring passed over the silence. A tree that
records announcements now replays every updatesync_accessibility
returns, including the re-placed copy a scroll hands out, through
accesskit_consumer, and the ring records what all three adapters
announce: a live node's name as it enters the filtered tree, and again
when it changes while the node stays there, at the
politeness the node has or inherits from a live ancestor. A node inside a
live region is recorded, as the adapters announce it. A change of politeness
alone, which Windows and macOS announce and AT-SPI does not, is not, and
neither is a blank name. Several in one update are kept in reading order,
so a message the framework's announcer says comes after a change of the
application's own live region in the same frame. The replay is a second
pass over every node of every update, making a full sync of 3,000 nodes
1.4 to 2 times as long, so only a tree that has a reader records: one
built withWidgetTree::new(), as every headless tree and every test is,
and the tree of a windowteksilo-appopens only when
install_automation_bridge_in_debug()installed the bridge, which it does
in a debug build with theautomationfeature. The new
WidgetTree::set_records_announcementsswitches it. Behaviour change
for anything that read the ring, and for anything that read it from a
window without the automation bridge, where it is now empty. -
announce_unless_widget_speakskept quiet beside a live region nobody
could hear. It took a widget to be speaking when any node in its subtree
carried a politeness and a value or a label, so a hidden live region, or a
Statusholding its text as a value, silenced the framework's own message,
and the user heard nothing at all. It now reads the last update through
accesskit_consumer, whether or not the tree records announcements, and
counts only a live node the platform adapters walk, with a name to speak,
whose politeness is set inside the widget. A button that is live only
because it sits in a toast or another live region is not the one speaking,
and does not keep the framework quiet about its own menu. The long-press
context-menu announcement goes through it. -
accessibility::announced_textand the label audits took a value for a
name. For any role butRole::Label,announced_textfell back to the
node'svaluewhen it had no label, andaudit::duplicate_label_leaks
andlabels_without_text_rangesread names the same way, while no
platform adapter takes a value as the name of anything but a label. A
Statusholding its text as a value was reported as announcing it, and a
label repeating such a value was reported as repeating a name nobody
hears. Both now read aRole::Label's value and any other node's label,
and nothing else. Behaviour change for a test that found a node by a
value throughannounced_text. -
A merged name took the text from under a hidden descendant.
access_merge_subtreeskipped a hidden descendant's own name but went on
merging its children, so text an application hid withaccess_hidden(true)
on a wrapper was read aloud on the merged node. A hidden descendant and its
whole subtree now contribute nothing, as the adapters treat them. The
rustdoc ofAccessNodeBuilder::set_hidden,clear_hiddenand
access_hiddensaid the flag was local to its node; it now says that it
hides the subtree and that a wrapper which is only chrome is a
Role::GenericContainerinstead. -
A field's validation message was never read on arriving at the field.
access_described_by, and the four stock inputs that wire their
ValidationStripwith it (TextInput,PasswordField,DateTimeEdit,
DateRangeEdit), promised WCAG 3.3.1: the message read as the field's
description when it gains focus. AccessKit 0.25 passes the relation to no
screen reader, on any platform: the consumer never derives a description from
it, and no adapter exports it. So coming back to a refused field said its name
and value and not why. The tree now writes eachdescribed_bytarget's text
into the node'sdescription, after its own, which Orca, NVDA and VoiceOver
read. Around focus it holds new text back, because Orca speaks a change to
the description of the node it holds as focus: a message appearing while the
user is in the field is said once, by its live region, is not begun again as
the user leaves, and is read with the field on the next visit. A shown tooltip
no longer takes away its anchor's description either. Behaviour change: a
node carryingdescribed_bynow carries a description. An application that
announces a message itself as it sends focus to the field showing it is heard
once on each platform, by a different voice: on Windows the arrival leaves
the announced text out, since NVDA says the announcement; on Linux and macOS
the arrival reads it, since Orca cuts an announcement to read the new focus
(VoiceOver is unverified and treated the same way). -
A throttled frame-tick subscriber held back every frame requested while it
was on screen. A widget onsubscribe_frame_tick_throttledset the pace not
only of its own tick but of everyrequest_frame: with a once-a-minute clock
painted, the announcer's follow-up syncs, a caret or a drag auto-scroll each
waited up to a minute. An announcement queue drained one step per wait, so
its later messages reached the screen reader at the user's next key press,
spoken ahead of whatever that key said. The interval now delays the
subscriber's own tick and nothing else; a requested frame is due at 60 Hz
whatever is subscribed.
Widgets
-
A check box, a switch, a radio button or a slider changed without a
screen reader being told. On Space, an arrow key, a click or a screen
reader's own activation,Checkbox,Toggle,RadioButtonandSlider
changed their state and no platform heard of it: the new state reached the
accessibility tree only with the next unrelated update, most often the focus
move that followed, where Orca's "checked" or "52" was cut by the new focus.
Each change now reaches every platform in the frame that makes it, whether
the user, a screen reader or the application made it, and so does a
Checkboxin aListVieworTreeViewrow checked with Space on the row.
ASlideralso gives the platform its figures as they are written: an
f320.3 was published as 0.30000001192092896, which Orca spoke digit by
digit, and is now 0.3, as are its minimum, maximum and steps. A value the
arrow keys reach reads as the step it reached: three steps of 0.01 up from
0.3 are read as 0.33, not as the 0.32999998 thef32sums come to. -
A screen reader heard nothing as the highlight moved through a menu. In
everyMenuList, which is every menu-bar menu, context menu, popover menu
and submenu, the arrow keys, Home, End, PageUp, PageDown and type-ahead
moved a highlight no platform could see: Orca said "menu." as a menu
opened and nothing after it, and Enter ran a command the reader had never
heard. Each move of the highlight is now a focus change to the highlighted
item on Linux, Windows and macOS, so a reader hears "Open", or "Word Wrap,
check menu item, checked", as it goes. A menu is named after what opened
it, so opening one says "File menu", "Recent menu" or "Add menu" (a
context menu stays unnamed), and each item carries its place among the
menu's items for a reader that announces it ("3 of 5"). -
A submenu opened from the keyboard closed itself. Right, Enter or
Space on a submenu row, or its mnemonic, opened the submenu as a mouse
click would, and it closed about 165 ms later whenever the mouse rested
anywhere but on the row or the submenu, so a keyboard user could not
reach File > Recent in the example, or a docking layout's "Move to"
sides. It now stays open until Escape, Left, a choice or a click outside,
as one an assistive technology opens always did. -
Text fields told a screen reader nothing of the caret, of an empty
field, or of a password. In every field built onTextInputField
(TextInput,SearchField,SpinBox,PasswordField, the date and time
editors), an arrow, Home, End, a Shift selection or Ctrl+A reached no
platform: Orca 46.1 said nothing, and a reader was told the caret and
selection of the last edit. Each move and selection is now published as it
happens, and Orca speaks the character or the selection. An empty field now
offers the Text interface on AT-SPI, so the first character typed or pasted
into it, and the deletion that empties it, are reported. A masked password
field now exposes its mask as its text, one echo character per character and
never the plaintext, so Orca echoes each keystroke and deletion; aNoEcho
field exposes an empty text and reports nothing typed. -
A
PrivacySettingswith telemetry configured crashed every debug build.
Its consent switches are named by their row's label, which the switch's own
check for a missing name could not see, so the application panicked as its
accessibility tree was built, and the widget catalog's Settings tab exited
as it opened. The panel now stays up, and a screen reader reaches each
switch by Tab, named by its row ("Anonymous usage metrics", "Crash
reports", "Feature flags") and read with its state. Release builds did not
crash. -
A date field's calendar wrote a date over the field that the user had not
chosen. The calendar of aDateEdit, aDateTimeEditor a
DateRangeEditopened on the date the field held when it was built, or
wherever the last opening had left its cursor, or on the months view it was
closed on, and Enter wrote that day over the field's value without a word.
It now opens on the date the field holds (for aDateRangeEdit, the start;
for aDateEdit, the text typed in it counts), in the day view, and the
reader hears that day. ADateRangeEdit's calendar closed with one end of
a range picked kept that end, and one Enter in the next opening wrote a
range from it to the cursor over the field's and closed the calendar; each
opening now begins a new range. -
A calendar's months and years views could not be used from the keyboard
or by a screen reader. There the arrows moved the cursor of a day grid no
one could see, in silence, and Enter or Space selected that hidden day.
Now the arrows move over the months (the years), each heard with its year
("May 2026"), PageUp and PageDown move a year (a decade), Enter or Space
opens the one under the cursor, and Escape goes back to the days; nothing
is selected on the way. Activating the title takes the keyboard into the
grid, on the month shown. A month or a year offers a screen reader's click,
and they are one Tab stop, the grid's, where each month was a stop of its
own. The years view follows the cursor, or a header arrow, into the next
decade. Behaviour change: a month cell is named with its year. -
The fields of the date and time editors were read without a name, or
without their date. The field focus lands on in aDateEditor a
TimeEdithad no name, so Orca said "entry 05/02/2026"; it now carries the
editor's.label(), or "Date" and "Time", and anaccess_labelor
access_described_bygiven to the editor reaches it. ADateTimeEdit's
parts and aDateRangeEdit's halves reached Orca as date editors, read
without their text ("End date date editor."); they are now entries, read
with their date or time. Behaviour change: those parts are
Role::TextInput, no longerRole::DateInputorRole::TimeInput; the
editor around them keeps its date role. -
A custom overlay trigger could not be reached from the keyboard, or was
reached as an unnamed panel. The widget handed toDialog::trigger,
Snackbar::trigger,Wizard::triggeror aPopoverCustomcarried its
button role and name on one node and took focus on another. A popover over
a custom trigger was no Tab stop at all, and a screen reader could not
focus it either; a dialog's custom trigger took focus as the unnamed panel
it wrapped, which Orca 46.1 read as "panel.". The node that takes focus is
now the trigger's button: Tab lands on it, the reader hears its name, and
Enter, Space and the reader's own activation open the overlay. A trigger
that is already a control, such as aButton, stays the one Tab stop under
its own name, carries the popup state, and now opens the overlay when a
screen reader activates it, where before nothing happened; the wrapper
around it is no longer a second button, which for aSnackbarhad been
named with the snackbar's message. Behaviour change: a custom trigger
around a widget that takes no focus is now a Tab stop, including the filter
glyph of a filterableTableViewcolumn. -
Opening a popover with nothing to focus in it put the reader on a node
with no name and no role. Orca 46.1 said nothing, and Tab from there went
to the first control of the window. Focus now lands on the popover's
dialog, so the reader hears its name and the text it holds; Tab goes on to
the control after the trigger and Shift+Tab back to the trigger, and either
closes the popover. A popover's dialog given nosurface_nameis now named
by its trigger, where before it had no name. Abare()popover with
nothing to focus leaves focus on its trigger. -
Focus in a
RichTextEditor,CodeEditor,PlainTextEditororLogView
reached no screen reader. Tabbing or clicking into one put focus on an
unnamed node with no text, which Orca 46.1 announced as "section." or not at
all, and no caret move that followed was ever reported, so every arrow key
was silent. A reader now lands on the text itself, an editable text or a
document, hears its name and the line at the caret, and hears each caret
move. Coming back to a rich text editor or viewer is heard too, where the
return used to be silent. An.access_label(..),.access_labelled_by(..),
.access_description(..)or tooltip attached to one of these widgets now
names or describes the text. Therich-text-editor,rich-text-viewer,
code_editorandlog_viewexamples name their surfaces. -
A keyboard user could not leave a
CodeEditoror aPlainTextEditor.
Tab indents in them, and so did Ctrl+Tab, while Ctrl+Shift+Tab dedented, so
every key meant to move focus out wrote into the document instead, and a
screen reader heard nothing of it. Ctrl+Tab and Ctrl+Shift+Tab now move
focus to the next and the previous control and leave the text alone, as
they already do out of a terminal and a table; Tab and Shift+Tab still
indent and dedent. The editor's text node now says so in its description
("Tab indents. Ctrl+Tab moves to the next control, Ctrl+Shift+Tab to the
previous one."), in the user's language, and Orca reads it as focus
arrives there. A read-only viewer does not take Tab, so Tab leaves it and
it carries no such description. -
Arrowing through an open
ComboBoxlist was silent, and changed the value
at every step. Focus stayed on the combo box (or on its search field)
while the arrows moved its value, and nothing told a screen reader which
option they had reached, so Orca heard nothing as a reader moved through a
long list, the font list or the list of languages. The keys now move a
highlight that the focused node names as its active descendant, so a
screen reader on Linux, Windows or macOS hears each option as it is reached,
and the option the list opens on. The value changes only on a commit: Enter
or Space in the open list, or a click on a row; Escape, Tab and closing the
list keep it.on_selecttherefore fires once, on that commit, and a
LanguageSwitcherno longer switches the application's language (nor a
ThemeSwitcherits theme) at each arrow while a reader listens to the
choices. Down from an empty combo box reaches the first item, where it
skipped to the second. A long list is one list box of named options: each
option used to sit inside an unnamed, unselected option of a second list
box, and on Linux the list box reported no selected option. A searchable
list's search field is named after the combo box ("Search" when it has no
label), and its placeholder is translated. Behaviour change: the arrows,
Home/End, the page keys and type-ahead no longer write the bound
selectedsignal or fireon_select; code that read the value after an
arrow press presses Enter first. -
Every change to the toast stack read every toast again and moved
keyboard focus. Showing, updating or dismissing any toast announced each
toast still on screen again, in no set order: with an Error toast shown
after three others, Orca spoke all four titles. A progress toast updated in
place announced its unchanged title at every step, twenty times over for
toast-demo's background job. Each change also replaced the control a
keyboard user was on and put focus on the oldest toast, so the Cancel of a
progress toast could not be pressed from the keyboard. A toast is now
announced when it appears and when its title changes, and nothing else is
read again. Focus stays where the reader put it, on a toast or on one of its
actions, and an action kept across an update runs the update's callback.
When an update removes the focused action, focus stays in that toast. -
A toast expired under keyboard focus. A reader who Tabbed to a toast's
action had the rest of its ten seconds to decide, then the toast went and
Orca said "frame". A toast now stays while keyboard or screen-reader focus
is on it or inside it, as it does under a resting pointer. -
A toast shown while another was up left with it. A second toast was
charged for the time before it appeared and expired at the first one's
deadline: shown 5 s after another it lasted 4.8 s instead of 10, and an
Error toast shown last lasted 2 s. Each toast now gets its own time. -
The notification bell and log were rebuilt on every notification.
Every toast is archived, and a background job archives each of its steps.
Focus on the bell was fired again on a new button, which cut the toast being
announced and said "Notifications push button" instead; an open bell
popover closed under the reader and marked the new notification read unseen;
and the log dialog threw focus back to Mark all read at every step of a job,
so Clear all could not be pressed. The bell, its popover and the log's
buttons now stay put and keep focus; only the unread badge and the rows whose
notification changed follow the archive. Clear all now says "No
notifications". -
The chosen segment of a
SegmentedControlwas read as "not selected".
Each segment is a radio button, and a screen reader tells a radio button's
state from whether it is checked, which no segment ever was. Orca read the
segment the user had just chosen as "not selected radio button", and by the
Windows and macOS adapters' own code NVDA was given no state for it at all
and VoiceOver no checked value. The chosen segment is now checked, as a
RadioButtonis, so Orca says "selected radio button". A segment also
stopped claiming to be selected in the list sense, which had Orca speak the
chosen segment of a control whenever the control came into view or its
choice changed, wherever the reader was: five cut "not selected radio
button" at the launch of chart-demo, and a Tab onto a field that scrolled a
control into view was cut short by that control's segment. Behaviour
change: a test or a probe that asserted a segment'sselectedasserts its
togglednow. -
A dialog's content was hidden from assistive technology. The panel
RecipeDialogStyledraws around aModalContainer's content called
set_hidden()to say it was only chrome, and the consumer every platform
adapter reads through treats a hidden node as hiding everything under it.
So on Linux, Windows and macOS a screen reader reached the focused control
of a dialog and nothing else: not the title or the message as text, not the
other fields and buttons, and not even the focused control when walking the
dialog, whose children it no longer listed. Orca's flat review gathers
what it reviews from those children (getOnScreenObjectsin its
script_utilities.py), so it had nothing to review, and a walk of the UIA
tree met the same empty dialog, since the Windows adapter lists children
through the same filter; what NVDA's object navigation made of it was not
observed. The panel is now a bareRole::GenericContainer, which the
adapters drop while keeping its children. EveryModalContaineris
affected, on the default style and on the macOS and Fluent presets, which
reuse it. A customDialogStyleshould do the same, as
DialogStyle::make_panelnow says. What that lets through is presented
once: aModalContainerwhose content is itself aRole::Dialogor
Role::AlertDialog, as aMessageBox's and aCommandPalette's is, now
publishes a bareRole::GenericContainerinstead of a second dialog of the
same name, which Orca would speak as focus entered each; and the
CommandPalette's content isLive::Off, so the polite palette is
announced by its name alone, and not row by row. Behaviour
change: the container of a presentedMessageBoxis no longer a
Role::Dialognamed by the title. A test or a probe that found the box's
buttons under that node finds them under the box'sRole::AlertDialog,
which holds them on this version and the last. -
The content of a
Card, aPanel, aToolbar, aStatusBar, a
snackbar and aRadioTilebody was hidden from assistive technology. The
framesRecipeCardStyle,RecipePanelStyle,RecipeSnackbarStyleand
RecipeRadioTileStyledraw, and aPanelmarkeda11y_presentational,
calledset_hidden()the way the dialog panel did. A screen reader found a
card, a panel, a toolbar, a status bar or a snackbar empty except for
whichever of its controls held focus, and never reached a tile'sbody.
Each is now a bareRole::GenericContainer, and the snackbar's frame is
alsoLive::Off, so a snackbar that announces its message says it once.
The macOS and Fluent presets
build their cards, panels and snackbars from these frames, and Material 3
its cards, so they are fixed with them. Themake_bodyofCardStyle,
PanelStyle,SnackbarStyleandRadioTileStylenow says what a custom
frame has to do. -
An open menu listed none of its items to assistive technology. The
panelRecipePopoverStyledraws for itsMenuvariant, under every
MenuList,ComboBoxdrop-down and search suggestion list, called
set_hidden(). Whatever held focus could still be heard, since the filter
lets a focused node and an active descendant through, but the menu listed
no children, so a screen reader could not review the items around it. The
surface is now a bareRole::GenericContainer, and
PopoverStyle::make_bodysays what a custom one has to do. -
Toasts were never announced and could not be reached. The
ToastHost
every toast is mounted under calledset_hidden(), so no toast ever
entered the tree a platform adapter reads. The AT-SPI, UIA and macOS
adapters all announce a live region as it enters that tree and pass over
one that is filtered out, so a toast'sRole::StatusorRole::Alertsaid
nothing on any platform, and its text and actions could not be reviewed or
used from a screen reader. The host is now a bareRole::GenericContainer,
and the toast's content isLive::Off, so a toast is announced by its
title, once; its body is its description, read on reaching it, and its
actions and close button stay reachable.ToastStyle::make_bodysays a
custom style has to do the same. -
A
TabWidget's panel, aStepper's step and anything inside a
Switcher,MaxSizeorAspectRatiowas hidden from assistive
technology. The three layout primitives calledset_hidden()to publish
no node of their own, and a hidden node takes its whole subtree out of
every platform's tree.TabWidgetandSteppershow their pages through a
Switcher, so the selectedRole::TabPaneland everything in it could not
be reached, and neither could aCalendar's day grid, the title bar's
maximize button or the debug inspector's panel, while a menu capped by
max_visible_itemslost its rows to theMaxSizearound them. A screen
reader met a control inside them only once focus reached it. All three are
now a bareRole::GenericContainer, pruned from the tree, and the page a
Switchershows takes its place; its other pages stay out of the tree as
before, because they are dormant. Behaviour change: none of the three
publishes a node of its own any more, so what one wraps is a child of the
node around it, in the updatesync_accessibilityreturns and in an
automation snapshot alike. The snapshot, which ignores the hidden flag,
held the same pages before, under the hidden node. -
A
Snackbar's button, the notification bell, aTitleBar's centre
content, an unlabelledSplitterpane and everyTreeTableViewcell were
hidden from assistive technology. Each sits inside a shell that called
set_hidden()to keep itself out of the tree: theSnackbararound its
trigger,NotificationCenterButtonaround itsIconButton, the title
bar's drag region aroundTitleBar::center, a splitter pane that has no
pane_label(or a labelled one while collapsed, sliver and all), and the
row inside eachTreeTableViewrow. A screen reader could reach any of
them only while it held focus, and a tree table's cells not at all. Each
shell is now a bareRole::GenericContainer. Behaviour change for a
test that read the drag region's hidden flag: it is no longer hidden, and
still no stop. -
A
Bannerwas followed by its description, its action and its dismiss
button. The banner is a polite live region named by its title, and every
node inside it that sets no politeness of its own inherits it, so the
AT-SPI, UIA and macOS adapters announced each of them as a message of its
own as the banner appeared, in the order of the consumer's hash set. Orca
speaks each announcement with interrupt set, so what was left to hear was
whichever came last. The banner's content
is nowLive::Off: its title is what it announces, once, and the rest
stays reachable. AStatusBarwithannounce_changes(true)keeps the
inheritance, which is what it is for. -
Opening a
MessageBoxannounced its default button, and not its
question. The box is an assertive live region so that its question is
announced as it appears, and every node inside inherited the setting. The
hidden dialog panel let only the focused default button through, and it
was announced, assertively, as a message of its own, while the box's name
never was: a confirmation opening on its default button announced "No",
and its question reached a reader only as the name of the dialog
presenting the box, around the focus. With the dialog's content reachable,
everything inside the box is nowLive::Off: the question, the box's own
name, is what it announces, once, and its text and buttons stay reachable. -
A focused
SpinBoxreported a nameless text field. Focus lands on the
spin box's editing field, while its name, value and range sat on a node
around it, so a screen reader named the field once or not at all, and could
not hear a step move the number. The editing field is now the spin button:
Role::SpinButton, thelabelas its name, the value, range and step,
IncrementandDecrement, and its own text runs. A focus change says the
name and value once, and a step changes the node the reader is on. What an
application gives the spin box's id (access_label, aFormLayoutlabel,
access_described_by, a tooltip) reaches that node through the new
Widget::accessibility_proxy. The accessible value is now exactly the text a
reader can review, so a paintedsuffixis no longer in it.
Behaviour change: a test that read the spin button off theSpinBox's
own id finds aGenericContainerthere; the spin button is the field,
first_focusable_descendantof that id. -
A
SpinBoxwith a customvalue_from_textcould not be typed in words.
The numeric input filter ran whatever the parser, so a month field that reads
"march"as 3 dropped the letters as they were typed, and the commit put the
old month back. A custom parser now receives every character and alone
decides, at commit, what it can read; the default parser keeps its filter. -
A
SpinBoxstep threw away what had just been typed. Up, Down, the
wheel, the step buttons and an assistiveIncrementstepped from the value
last committed, so 35 typed over 10 in a field stepping by 5 became 15 on
Up. A step now reads the typed text first, asEnterwould, and moves on
from it: 40, with oneon_value_changed. Text that cannot be read steps
nothing and the held value is shown again, as in Qt.Enterand focus loss
now commit keystrokes delivered in the same batch as the key that commits
them, which they used to miss by a frame. Behaviour change. -
After a live change of language, a
SpinBoxwrote wrong values. Switched
to French, every spin box showed440,00, and then went on reading, stepping
and filtering in the language it was built in: focus turned the text back to
440.00, a step said-0.5, a grouped1 234 567was read "1,234,567",
which is a decimal to a French reader, and the comma typed into a decimal
field was dropped, so12,5wrote 125. The field now shows, reads, steps,
commits and accepts the number in the language switched to:12,5writes
12.5, shown and spoken12,50. -
A
SpinBoxat itsspecial_value_texttold a screen reader "0". Focus
replaced "Auto" with the minimum as it arrived, so a reader moving to a
timeout heard "Timeout 0 spin button" and never what 0 means there, while a
step back down to the minimum said "Auto". The special text now stays while
the field has focus, as Qt'sspecialValueTextdoes: arriving says "Timeout
Auto spin button", keyboard focus selects it so a number typed replaces it,
and leaving the field no longer rewrites its text, which Orca spoke as "Text
unselected.". Behaviour change: a click into the field puts the caret in
the special text, not in the number; select it, or step, to replace it. -
The calendar spoke English and ISO dates to a user in any language.
In French the grid was "Calendar, septembre 2026", a day was "lundi août 31,
2026", and the value read "2026-09-24 (selected: 2027-03-12)". Every string
Calendargives assistive technology is now in the user's language: the
words from the framework bundle, and every date written by ICU for the tree's
locale, in the locale's order and grammar, and on the Gregorian calendar the
grid is laid out in even where the locale prefers another. French says the
first of the month as "premier" ("1er" on screen), where ICU's bare "1" is
read "un" by a speech engine, and Italian and Romanian say it "primo" and
"întâi", where the digit is read "uno" and "unu" (Serianni, cited by the
Accademia della Crusca; DOOM2): "lunedì primo marzo 2027", "luni, întâi
martie 2027". The Italian date drawn under a range calendar writes "1º mar
2027", and the Romanian one keeps the digit, which is how Romanian writes
it. No other shipped language is rewritten: Spanish and Portuguese read the
digit right as it stands, and the languages that read every day as an
ordinal are a question for every day, not the first. The same grid now
reads "Calendrier, septembre 2026", "lundi 31 août 2026" and "jeudi 24
septembre 2026 (sélection : vendredi 12 mars 2027)", and a range is joined
by words ("du … au …"). With the cursor on the one selected day, which is
where a date field's popover opens, the day is said once, marked selected,
where the value used to say it twice: "vendredi 12 mars 2027
(sélectionné)". The title is ICU's month with its year, so Japanese reads
"2027年3月"; the decade title reads "2020 to 2029" instead of joining the
years with an em-dash; the line under a range calendar uses the locale's
medium date and now follows a range committed from the keyboard or by the
application, not only by a click.DateEdit,DateRangeEditand
DateTimeEditopen this calendar, so their popovers are fixed with it.
calendar-name-with-monthnow takes the month with its year as its one
$month, the unusedcalendar-cell-nameis gone, and
calendar-value-with-selection,calendar-value-on-selection,
calendar-date-rangeandcalendar-decadeare new, in all 23 locales.
Behaviour change for anything that read the grid's value as an ISO
date. -
DateEdit,DateRangeEditandDateTimeEditgave assistive technology an
ISO value. The value on the node standing for the whole field was
"2026-05-02", "2026-05-01/2026-05-10" or "2026-05-02T14:35:07", which UIA
and macOS hand to a screen reader as it stands. It is now the day in full in
the tree's locale, "samedi 2 mai 2026", the range joined by words, "du
vendredi premier mai 2026 au dimanche 10 mai 2026", and the day with its time,
"samedi 2 mai 2026 à 14:35", with the seconds only when the field shows
them. The editable text inside keeps its pattern. Behaviour change for
anything that parsed those values. -
A
Calendar's days and header buttons could not be reached, and
arrowing through its days said nothing on Linux. The day grid's body and
the header row calledset_hidden()so as not to be announced beside the
Role::Grid, and hid what they hold with them: the 42 cells, each naming
its date in full with its selected and today states, and the arrows and
the title button. A screen reader found nothing inside the grid to review,
and reached a header button only once it held focus. The day under the
keyboard cursor lived only in the grid's value, which UIA and macOS report
as a value change and AT-SPI carries on no interface, so Orca heard nothing
while the cursor moved. Both are now a bareRole::GenericContainer, so
the days are in the platform's tree, six rows of seven cells under the
grid, and so are the header's five buttons. While it holds focus in the day
view, the grid names the cell under the cursor as its active descendant,
and AccessKit reports that cell as the focus on all three platforms: each
arrow press, and each change of month from the keyboard, is a focus change
to the new day ("samedi 13 mars 2027"). The value still says the cursor
and the selection, for a client that reads the grid. A day offers
Action::Clickand, like a groupedRadioTile, no longer
Action::Focus: the dispatcher moved keyboard focus onto a day an
assistive technology focused, off the grid, and every arrow press after
that moved the cursor in silence.DateEdit,DateRangeEditand
DateTimeEditopen this calendar and are fixed with it. Behaviour
change for an automation client that focused a day, which is now
reported unhandled. -
A calendar said things twice. The grid was a polite live region, and a
live node speaks its name as it enters the tree and on every rename, with
every descendant inheriting the setting. Opening a date field's calendar
announced the grid's name, then each of the seven weekday headers, and then
focus said the name again; PageUp or PageDown in the grid announced the new
month while Orca spoke the rename of its focus as well; and each header
button Tab reached was announced as it appeared and again as it took focus.
The grid is no longer live, and the calendar's content isLive::Off, so
one placed inside an application's own live region lends it the grid's
name and no day, weekday or button. A change of month made from a header
arrow, where focus stays on the arrow, is announced once through the
tree's announcer, as the title now reads; the Today button announces the
day it moved to, in full. Neither announces while the grid itself holds focus.
Behaviour change for anything that listened to the grid's live region. -
The context menu opened from the keyboard acted somewhere else. Shift+F10
or the Menu key in aRichTextEditor, aCodeEditoror a single-line field
(TextInput,PasswordField,SpinBox,SearchField) moved the caret to
the middle of the surface before the menu opened, as a right-click there
would have. The reader heard nothing of it, or "Text unselected." when a
selection collapsed, and the menu's Paste then wrote into the middle of the
text while Cut and Copy found no selection. The caret and the selection now
stay where they were, and the menu acts there. A right-click still moves the
caret to the click. -
Closing a field's context menu selected the whole field. When focus came
back to a single-line field from its own menu (Escape, one of its commands,
a click outside it), the field treated it as a keyboard arrival and selected
everything: Orca said the field's text followed by "selected", and the next
key replaced it all, even right after the menu's Paste. Focus now comes back
to the caret and selection the menu found. Arriving by Tab still selects the
field.
Data views
- A single-selection data view could move its cursor off the selection.
Ctrl+arrow andCtrl(⌘ on macOS) +Home/End/Pagemoved the
cursor and left the selection behind, so a screen reader announced one row
while actions used another. They now move the selection with the cursor in
any of the five data views whose selection holds one entry: a
SelectionMode::Singlemodel — including one handed to a table in the
defaultMultiRowmode — or aTableSelectionMode::SingleRow/SingleCell
table. Multi-selection views are unchanged. Behaviour change. - A selection set by the application left the cursor behind. In a
single-selection data view the keyboard cursor stayed on the row the user
last reached, so a screen reader went on announcing it and the next arrow
key started from it. The cursor now moves with the selection when the
application or a model change moves the selection. Multi-selection views are
unchanged. Behaviour change. - A
GridViewnever said how many tiles were selected, and announced each
tile it realized. The count was an English value on a grid marked as a
live region. A grid takes its name from its label, so no platform announced
the value, while every tile inherited the live setting and was announced as
it scrolled into the realized window. The grid is no longer live. A click, a
key, an assistive click or a marquee that changes how many tiles are
selected now says the new count once, through the tree's announcer, in the
user's language ("3 éléments sélectionnés"), Space on a tile the grid has
not realized included. Moving a single selection says no count, since the
tile the reader lands on says it is selected. The grid's value
carries the same words.grid-view-selection-countis new, in all 23
locales. Behaviour change for anything that read the value in English. - A screen reader's focus on a
GridViewtile made Enter open another
tile. A tile offered a focus of its own, so UIA'sSetFocus, AT-SPI's
grab_focusor VoiceOver's keyboard focus following its cursor put
keyboard focus on that tile while the grid's cursor stayed where it was.
Enter and Space then acted on the cursor's tile, not the one the reader had
just heard, and the next key that changed the selection dropped focus onto
the window, where the keys did nothing. A tile no longer offers focus: the
request moves nothing, the reader's focus and the grid's cursor stay
together, and a click on a tile, which a reader's activation sends, chooses
it. Behaviour change for anything that focused a tile through
Action::Focus. - Every change of a
GridView's selection was heard as a move of focus.
The tile under the cursor came back as a tile the reader had never met, so
every platform reported a focus change to it, and Orca stopped reading the
selection count it had just been given to read the tile's name again. A
toggle was never heard as a change of the tile's selected state. The tile
under the cursor now stays the same tile through a change of selection,
its selected state changes where the reader is, and the count is heard in
full. - A double click on a selected
GridViewtile opened nothing. Its
first click set the selection again, which replaced every tile in view, so
the second click reached a tile that had not seen the first. A double click
on a tile that is already selected now opens it.
Internationalization
- A message with a plural or a selector was said as its message id when
no translations were installed. Without anI18nManager,tr!,
tr_widget!andtr_signal!gave a message in the source language only
when it was plain text and{ $var }; any other came back as its key. A
screen reader said "grid-view-selection-count" where "1 item selected" was
meant, and aCommandPalettegave its result count the same way. Such a
message is now formatted in the source language, plurals, functions and
the messages and terms it refers to included.
Terminal
- A
Terminalnever announced its new output. Each completed line went to
a politeRole::Statuslive region as its value. AStatusis named by its
label, and every platform adapter announces a live node's name, so no
platform said the line; only the automation ring, which read the value,
recorded it. The line is now the region's name.
Automation
type_textandtype_imeon a text editor's own node blurred the editor.
ARichTextEditor,CodeEditororLogViewis found as focused, and by
its name, on the node that holds its text. Typing into that node still
reached the document, but left the editor without its caret, input method
and focus ring. The keyboard now stays on the editor.list_live_regionslisted live regions no screen reader can reach. It
listed every node that declared a politeness: a hidden one, one inside a
hidden subtree, and the framework's two announcer nodes, which are hidden
while they have nothing to say. It now lists only the ones
accesskit_consumer's filter keeps, which are the ones a platform adapter
walks and can announce. The set comes from
teksilo_core::accessibility::audit::nodes_in_filtered_tree.
Behaviour change.
Full Changelog: v0.13.1...v0.14.0