Skip to content

v0.14.0

Choose a tag to compare

@github-actions github-actions released this 26 Sep 19:42
· 162 commits to main since this release
f48cac4

Added

Accessibility

  • teksilo_platform::key_report: each key reported to the AT-SPI registry.
    KeyReportGate reports every key press and release to
    org.a11y.atspi.DeviceEventController.NotifyListenersSync while an
    assistive technology is attached, and says which keys the screen reader
    took, so they are not dispatched. teksilo-app runs every KeyboardInput
    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.py runs 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; tabwalk and
    tree need 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, a FormLayout'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 offers Action::Focus while 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_tree lists the nodes accesskit_consumer's
    filter keeps, which are the ones a platform adapter walks and can
    announce.

  • RichTextEditor, CodeEditor, PlainTextEditor and LogView take 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
    through BuildContext::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 at DateStyle::Long
    ("lundi 31 août 2026", "Monday, August 31, 2026"); or YearMonth, 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 plain String, 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 default fa-IR writes
    24 September 2026 as the 2nd of Mehr 1405, and th-TH counts its year 2569.

Core

  • EventContext::context_menu_trigger: what asked for a context menu. A
    context_menu factory 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 says ContextMenuTrigger::Pointer (a
    secondary click, or a hold), Keyboard (the Menu key, Shift+F10, Ctrl+Shift+M
    on macOS) or Accessibility (an assistive technology's ShowContextMenu),
    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 TreeView over an index SelectionModel keeps 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. A Snackbar that 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 (a PasswordField) 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_geometry built 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 run TextRunSource::flat always
    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 (a MessageBox, a Dialog, any Centered overlay), 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
    announce in 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 a TreeUpdate of 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_accessibility returns, which the automation bridge and tests read,
    are unchanged. An integration that feeds an AccessKit adapter itself hands
    it WidgetTree::deliver_accessibility and routes the adapter's action
    requests through WidgetTree::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 a ListView,
    TreeView, GridView or 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_announcements that reads it, recorded a live node's value before
    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 a Role::Label, and only for a node in the filtered tree. So a
    hidden live region, one inside a hidden subtree, and a Status or Alert
    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 update sync_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 with WidgetTree::new(), as every headless tree and every test is,
    and the tree of a window teksilo-app opens only when
    install_automation_bridge_in_debug() installed the bridge, which it does
    in a debug build with the automation feature. The new
    WidgetTree::set_records_announcements switches 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_speaks kept 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
    Status holding 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_text and the label audits took a value for a
    name.
    For any role but Role::Label, announced_text fell back to the
    node's value when it had no label, and audit::duplicate_label_leaks
    and labels_without_text_ranges read names the same way, while no
    platform adapter takes a value as the name of anything but a label. A
    Status holding 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 a Role::Label's value and any other node's label,
    and nothing else. Behaviour change for a test that found a node by a
    value through announced_text.

  • A merged name took the text from under a hidden descendant.
    access_merge_subtree skipped a hidden descendant's own name but went on
    merging its children, so text an application hid with access_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 of AccessNodeBuilder::set_hidden, clear_hidden and
    access_hidden said 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::GenericContainer instead.

  • A field's validation message was never read on arriving at the field.
    access_described_by, and the four stock inputs that wire their
    ValidationStrip with 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 each described_by target's text
    into the node's description, 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 carrying described_by now 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 on subscribe_frame_tick_throttled set the pace not
    only of its own tick but of every request_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, RadioButton and Slider
    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
    Checkbox in a ListView or TreeView row checked with Space on the row.
    A Slider also gives the platform its figures as they are written: an
    f32 0.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 the f32 sums come to.

  • A screen reader heard nothing as the highlight moved through a menu. In
    every MenuList, 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 on TextInputField
    (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; a NoEcho
    field exposes an empty text and reports nothing typed.

  • A PrivacySettings with 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 a DateEdit, a DateTimeEdit or a
    DateRangeEdit opened 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 a DateRangeEdit, the start;
    for a DateEdit, the text typed in it counts), in the day view, and the
    reader hears that day. A DateRangeEdit'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 a DateEdit or a
    TimeEdit had no name, so Orca said "entry 05/02/2026"; it now carries the
    editor's .label(), or "Date" and "Time", and an access_label or
    access_described_by given to the editor reaches it. A DateTimeEdit's
    parts and a DateRangeEdit'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 longer Role::DateInput or Role::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 to Dialog::trigger,
    Snackbar::trigger, Wizard::trigger or a PopoverCustom carried 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 a Button, 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 a Snackbar had 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 filterable TableView column.

  • 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 no surface_name is now named
    by its trigger, where before it had no name. A bare() popover with
    nothing to focus leaves focus on its trigger.

  • Focus in a RichTextEditor, CodeEditor, PlainTextEditor or LogView
    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. The rich-text-editor, rich-text-viewer,
    code_editor and log_view examples name their surfaces.

  • A keyboard user could not leave a CodeEditor or a PlainTextEditor.
    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 ComboBox list 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_select therefore fires once, on that commit, and a
    LanguageSwitcher no longer switches the application's language (nor a
    ThemeSwitcher its 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
    selected signal or fire on_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 SegmentedControl was 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
    RadioButton is, 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's selected asserts its
    toggled now.

  • A dialog's content was hidden from assistive technology. The panel
    RecipeDialogStyle draws around a ModalContainer'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 (getOnScreenObjects in 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 bare Role::GenericContainer, which the
    adapters drop while keeping its children. Every ModalContainer is
    affected, on the default style and on the macOS and Fluent presets, which
    reuse it. A custom DialogStyle should do the same, as
    DialogStyle::make_panel now says. What that lets through is presented
    once: a ModalContainer whose content is itself a Role::Dialog or
    Role::AlertDialog, as a MessageBox's and a CommandPalette's is, now
    publishes a bare Role::GenericContainer instead of a second dialog of the
    same name, which Orca would speak as focus entered each; and the
    CommandPalette's content is Live::Off, so the polite palette is
    announced by its name alone, and not row by row. Behaviour
    change
    : the container of a presented MessageBox is no longer a
    Role::Dialog named by the title. A test or a probe that found the box's
    buttons under that node finds them under the box's Role::AlertDialog,
    which holds them on this version and the last.

  • The content of a Card, a Panel, a Toolbar, a StatusBar, a
    snackbar and a RadioTile body was hidden from assistive technology.
    The
    frames RecipeCardStyle, RecipePanelStyle, RecipeSnackbarStyle and
    RecipeRadioTileStyle draw, and a Panel marked a11y_presentational,
    called set_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's body.
    Each is now a bare Role::GenericContainer, and the snackbar's frame is
    also Live::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. The make_body of CardStyle,
    PanelStyle, SnackbarStyle and RadioTileStyle now says what a custom
    frame has to do.

  • An open menu listed none of its items to assistive technology. The
    panel RecipePopoverStyle draws for its Menu variant, under every
    MenuList, ComboBox drop-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 bare Role::GenericContainer, and
    PopoverStyle::make_body says what a custom one has to do.

  • Toasts were never announced and could not be reached. The ToastHost
    every toast is mounted under called set_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's Role::Status or Role::Alert said
    nothing on any platform, and its text and actions could not be reviewed or
    used from a screen reader. The host is now a bare Role::GenericContainer,
    and the toast's content is Live::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_body says a
    custom style has to do the same.

  • A TabWidget's panel, a Stepper's step and anything inside a
    Switcher, MaxSize or AspectRatio was hidden from assistive
    technology.
    The three layout primitives called set_hidden() to publish
    no node of their own, and a hidden node takes its whole subtree out of
    every platform's tree. TabWidget and Stepper show their pages through a
    Switcher, so the selected Role::TabPanel and everything in it could not
    be reached, and neither could a Calendar's day grid, the title bar's
    maximize button or the debug inspector's panel, while a menu capped by
    max_visible_items lost its rows to the MaxSize around them. A screen
    reader met a control inside them only once focus reached it. All three are
    now a bare Role::GenericContainer, pruned from the tree, and the page a
    Switcher shows 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 update sync_accessibility returns 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, a TitleBar's centre
    content, an unlabelled Splitter pane and every TreeTableView cell were
    hidden from assistive technology.
    Each sits inside a shell that called
    set_hidden() to keep itself out of the tree: the Snackbar around its
    trigger, NotificationCenterButton around its IconButton, the title
    bar's drag region around TitleBar::center, a splitter pane that has no
    pane_label (or a labelled one while collapsed, sliver and all), and the
    row inside each TreeTableView row. 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 bare Role::GenericContainer. Behaviour change for a
    test that read the drag region's hidden flag: it is no longer hidden, and
    still no stop.

  • A Banner was 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 now Live::Off: its title is what it announces, once, and the rest
    stays reachable. A StatusBar with announce_changes(true) keeps the
    inheritance, which is what it is for.

  • Opening a MessageBox announced 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 now Live::Off: the question, the box's own
    name, is what it announces, once, and its text and buttons stay reachable.

  • A focused SpinBox reported 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, the label as its name, the value, range and step,
    Increment and Decrement, 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, a FormLayout label,
    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 painted suffix is no longer in it.
    Behaviour change: a test that read the spin button off the SpinBox's
    own id finds a GenericContainer there; the spin button is the field,
    first_focusable_descendant of that id.

  • A SpinBox with a custom value_from_text could 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 SpinBox step threw away what had just been typed. Up, Down, the
    wheel, the step buttons and an assistive Increment stepped 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, as Enter would, and moves on
    from it: 40, with one on_value_changed. Text that cannot be read steps
    nothing and the held value is shown again, as in Qt. Enter and 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 SpinBox wrote wrong values. Switched
    to French, every spin box showed 440,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 grouped 1 234 567 was read "1,234,567",
    which is a decimal to a French reader, and the comma typed into a decimal
    field was dropped, so 12,5 wrote 125. The field now shows, reads, steps,
    commits and accepts the number in the language switched to: 12,5 writes
    12.5, shown and spoken 12,50.

  • A SpinBox at its special_value_text told 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's specialValueText does: 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
    Calendar gives 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, DateRangeEdit and
    DateTimeEdit open this calendar, so their popovers are fixed with it.
    calendar-name-with-month now takes the month with its year as its one
    $month, the unused calendar-cell-name is gone, and
    calendar-value-with-selection, calendar-value-on-selection,
    calendar-date-range and calendar-decade are new, in all 23 locales.
    Behaviour change for anything that read the grid's value as an ISO
    date.

  • DateEdit, DateRangeEdit and DateTimeEdit gave 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 called set_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 bare Role::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::Click and, like a grouped RadioTile, 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, DateRangeEdit and
    DateTimeEdit open 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 is Live::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 a RichTextEditor, a CodeEditor or 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 and Ctrl (⌘ on macOS) + Home / End / Page moved 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::Single model — including one handed to a table in the
    default MultiRow mode — or a TableSelectionMode::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 GridView never 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-count is new, in all 23
    locales. Behaviour change for anything that read the value in English.
  • A screen reader's focus on a GridView tile made Enter open another
    tile.
    A tile offered a focus of its own, so UIA's SetFocus, AT-SPI's
    grab_focus or 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 GridView tile 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 an I18nManager, tr!,
    tr_widget! and tr_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 a CommandPalette gave 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 Terminal never announced its new output. Each completed line went to
    a polite Role::Status live region as its value. A Status is 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_text and type_ime on a text editor's own node blurred the editor.
    A RichTextEditor, CodeEditor or LogView is 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_regions listed 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