mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-01 22:17:53 -06:00
Merge PR #3260: LR-BEHAV-ACCESS: a dialog is exposed to screen readers as a dialog, not a group
Conflicts: docs unioned
This commit is contained in:
@@ -198,6 +198,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context, blocked: bool) {
|
||||
}
|
||||
}
|
||||
if let Some(w) = shown {
|
||||
crate::widgets::announce_dialog(ctx, &w.response, crate::widgets::Modal::Dialog);
|
||||
ctx.memory_mut(|m| m.set_modal_layer(w.response.layer_id));
|
||||
if let Some(body_h) = body_h {
|
||||
let measured = (w.response.rect.height() - body_h).max(0.0);
|
||||
|
||||
@@ -121,7 +121,7 @@ pub(crate) fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
||||
|
||||
if app.updates_rt.manual_window_open {
|
||||
let mut open = true;
|
||||
egui::Window::new(crate::i18n::tr("LightCraft Updates"))
|
||||
let shown = egui::Window::new(crate::i18n::tr("LightCraft Updates"))
|
||||
.open(&mut open)
|
||||
.collapsible(false)
|
||||
.resizable(false)
|
||||
@@ -136,6 +136,9 @@ pub(crate) fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
||||
app.updates_rt.manual_window_open = false;
|
||||
}
|
||||
});
|
||||
if let Some(w) = &shown {
|
||||
crate::widgets::announce_dialog(ctx, &w.response, crate::widgets::Modal::Window);
|
||||
}
|
||||
if !open {
|
||||
app.updates_rt.manual_window_open = false;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ use crate::widgets::register;
|
||||
/// windows and their dim.
|
||||
const DIM_ID: &str = "library-problem-dim";
|
||||
const ERROR_WINDOW_ID: &str = "library-problem";
|
||||
/// What the error window says about the library's safety and the way on (drawn as it is, not translated).
|
||||
const EXPLANATION: &str = "Your photos and edits in it are untouched. You can try again (e.g. after closing another program that has it open \
|
||||
or reconnecting its drive), open a different library or create a new one, or continue without a library — \
|
||||
then nothing you do is saved.";
|
||||
const CHOOSER_WINDOW_ID: &str = "library-chooser";
|
||||
|
||||
/// The named `Foreground` layer used by the dim and the two blocking library windows.
|
||||
@@ -1016,6 +1020,12 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context, blocked: bool) {
|
||||
dim(ctx);
|
||||
let mut choice = None;
|
||||
let frame = egui::Frame::window(&ctx.global_style()).inner_margin(egui::Margin::symmetric(18, 14));
|
||||
// what the window says, in one piece, for a screen reader: the labels below are separate nodes
|
||||
let description = if problem.path.is_empty() {
|
||||
format!("{} {} {EXPLANATION}", crate::i18n::tr("LightCraft couldn't find where to keep your library."), problem.error)
|
||||
} else {
|
||||
format!("{} {}. {} {EXPLANATION}", crate::i18n::tr("LightCraft couldn't open the library at"), problem.path, problem.error)
|
||||
};
|
||||
let shown = egui::Window::new(crate::i18n::tr("Your library couldn't be opened"))
|
||||
.id(egui::Id::unique(ERROR_WINDOW_ID))
|
||||
.order(egui::Order::Foreground)
|
||||
@@ -1035,14 +1045,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context, blocked: bool) {
|
||||
ui.label(RichText::new(&problem.path).font(t.semibold(12.5)).color(t.text));
|
||||
}
|
||||
ui.add(egui::Label::new(RichText::new(&problem.error).color(t.caution)).wrap());
|
||||
ui.label(
|
||||
RichText::new(
|
||||
"Your photos and edits in it are untouched. You can try again (e.g. after closing another program that has it open \
|
||||
or reconnecting its drive), open a different library or create a new one, or continue without a library — \
|
||||
then nothing you do is saved.",
|
||||
)
|
||||
.color(t.text_dim),
|
||||
);
|
||||
ui.label(RichText::new(EXPLANATION).color(t.text_dim));
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
let mut button = |ui: &mut egui::Ui, id: &str, label: &str, enabled: bool, c: Choice| {
|
||||
@@ -1059,6 +1062,8 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context, blocked: bool) {
|
||||
});
|
||||
});
|
||||
if let Some(w) = &shown {
|
||||
crate::widgets::announce_dialog(ctx, &w.response, crate::widgets::Modal::Alert);
|
||||
crate::widgets::describe_dialog(ctx, &w.response, &description);
|
||||
// A press on the dim raises its area; keep this window (and its own buttons) above it.
|
||||
ctx.move_to_top(w.response.layer_id);
|
||||
}
|
||||
@@ -1186,6 +1191,7 @@ fn show_chooser(app: &mut LightcraftApp, ctx: &egui::Context, problem: &LibraryP
|
||||
});
|
||||
});
|
||||
if let Some(w) = &shown {
|
||||
crate::widgets::announce_dialog(ctx, &w.response, crate::widgets::Modal::Dialog);
|
||||
// A press on the dim raises its area; keep the launch list above it.
|
||||
ctx.move_to_top(w.response.layer_id);
|
||||
}
|
||||
|
||||
@@ -512,6 +512,8 @@ fn window(ctx: &egui::Context, id: &str, title: &str, text: &str, buttons: &[(&s
|
||||
});
|
||||
// A backdrop press can raise its area; keep the prompt buttons above it.
|
||||
if let Some(window) = shown {
|
||||
crate::widgets::announce_dialog(ctx, &window.response, crate::widgets::Modal::Alert);
|
||||
crate::widgets::describe_dialog(ctx, &window.response, text);
|
||||
ctx.move_to_top(window.response.layer_id);
|
||||
}
|
||||
input.button_ids = seen;
|
||||
|
||||
@@ -1214,6 +1214,39 @@ pub(crate) fn announce(resp: &egui::Response, name: impl Into<String>) {
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, resp.enabled(), name.clone()));
|
||||
}
|
||||
|
||||
/// What a window with its own title and buttons is, to a screen reader.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Modal {
|
||||
/// A dialog with a task (Export, Settings, a name prompt) that stops the rest of the application.
|
||||
Dialog,
|
||||
/// One that interrupts to say something that needs an answer (the quit prompt, a library that
|
||||
/// couldn't be opened) and stops the rest of the application: an alert dialog (AT-SPI and
|
||||
/// Windows tell it from a dialog; macOS reads both alike).
|
||||
Alert,
|
||||
/// A dialog that leaves the rest of the application reachable (the update check's answer).
|
||||
Window,
|
||||
}
|
||||
|
||||
/// Announces the window `response` drew as a dialog (an alert dialog for [`Modal::Alert`]), and for
|
||||
/// [`Modal::Dialog`] and [`Modal::Alert`] as modal, instead of as a group that carries the title:
|
||||
/// the platform's accessibility layer then sees a dialog and that the rest of the application is
|
||||
/// blocked while it is open.
|
||||
pub(crate) fn announce_dialog(ctx: &egui::Context, response: &Response, kind: Modal) {
|
||||
ctx.accesskit_node_builder(response.id, |node| {
|
||||
node.set_role(if kind == Modal::Alert { egui::accesskit::Role::AlertDialog } else { egui::accesskit::Role::Dialog });
|
||||
if kind != Modal::Window {
|
||||
node.set_modal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Gives the dialog `response` drew the words it says as its accessible description, for an alert
|
||||
/// whose content (the task names, the error) is drawn as separate labels: a screen reader then
|
||||
/// has what the alert is about when it announces the alert, not only its title.
|
||||
pub(crate) fn describe_dialog(ctx: &egui::Context, response: &Response, text: &str) {
|
||||
ctx.accesskit_node_builder(response.id, |node| node.set_description(text));
|
||||
}
|
||||
|
||||
/// The same for one that is on or off (a toggle, a selected row or swatch).
|
||||
pub(crate) fn announce_selected(resp: &egui::Response, name: impl Into<String>, selected: bool) {
|
||||
let name = name.into();
|
||||
|
||||
@@ -123,12 +123,13 @@ fn every_interactive_node_has_a_name_in_every_view_and_tool() {
|
||||
}
|
||||
h.run_steps(6);
|
||||
problems.extend(audit(&h, dialog));
|
||||
let windows = h.root().children_recursive().map(|n| n.accesskit_node()).filter(|n| n.role() == Role::Window).count();
|
||||
let is_window = |role| matches!(role, Role::Window | Role::Dialog | Role::AlertDialog);
|
||||
let windows = h.root().children_recursive().map(|n| n.accesskit_node()).filter(|n| is_window(n.role())).count();
|
||||
let untitled = h
|
||||
.root()
|
||||
.children_recursive()
|
||||
.map(|n| n.accesskit_node())
|
||||
.filter(|n| n.role() == Role::Window && n.label().unwrap_or_default().trim().is_empty())
|
||||
.filter(|n| is_window(n.role()) && n.label().unwrap_or_default().trim().is_empty())
|
||||
.count();
|
||||
if untitled > 0 {
|
||||
problems.push(format!("{dialog}: {untitled} of {windows} windows have no title"));
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
//! An open dialog is exposed to a screen reader as a dialog (a modal one, an alert dialog for the
|
||||
//! prompts), not as a group that happens to carry the dialog's title.
|
||||
use egui::accesskit::Role;
|
||||
use egui::vec2;
|
||||
use egui_kittest::{Harness, kittest::NodeT};
|
||||
use lightcraft_ui_egui::{LightcraftApp, Services};
|
||||
|
||||
fn harness() -> Harness<'static, LightcraftApp> {
|
||||
let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
|
||||
let mut h = Harness::builder().with_size(vec2(1500.0, 950.0)).build_ui_state(
|
||||
|ui, app: &mut LightcraftApp| {
|
||||
app.logic(&ui.ctx().clone());
|
||||
app.ui(ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(10);
|
||||
h
|
||||
}
|
||||
|
||||
/// `(role, title, modal)` of every window-like node in the tree.
|
||||
fn windows(h: &Harness<'_, LightcraftApp>) -> Vec<(Role, String, bool)> {
|
||||
h.root()
|
||||
.children_recursive()
|
||||
.map(|n| n.accesskit_node())
|
||||
.filter(|n| matches!(n.role(), Role::Window | Role::Dialog | Role::AlertDialog))
|
||||
.map(|n| (n.role(), n.label().unwrap_or_default(), n.data().is_modal()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_dialog_is_a_modal_dialog_with_its_title() {
|
||||
for command in ["dialog.export", "dialog.rename", "dialog.newAlbum", "dialog.createPreset", "dialog.contactSheet", "dialog.allMetadata"] {
|
||||
let mut h = harness();
|
||||
h.state_mut().run(command, serde_json::json!({})).unwrap_or_else(|e| panic!("{command}: {e}"));
|
||||
h.run_steps(6);
|
||||
let found = windows(&h);
|
||||
assert!(!found.is_empty(), "{command}: the dialog is in the tree");
|
||||
for (role, title, modal) in found {
|
||||
assert_eq!(role, Role::Dialog, "{command}: {title:?} is announced as a dialog");
|
||||
assert!(!title.trim().is_empty(), "{command}: the dialog has its title");
|
||||
assert!(modal, "{command}: {title:?} is modal, so the rest of the window is not read");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_is_a_dialog_too() {
|
||||
let mut h = harness();
|
||||
h.state_mut().run("app.settings", serde_json::json!({})).expect("Settings opens");
|
||||
h.run_steps(6);
|
||||
let found = windows(&h);
|
||||
assert!(found.iter().any(|(role, title, _)| *role == Role::Dialog && title.as_str() == "Settings"), "{found:?}");
|
||||
assert!(found.iter().all(|(role, ..)| *role != Role::Window), "{found:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_quit_prompt_and_a_settings_notice_are_modal_alert_dialogs() {
|
||||
let mut h = harness();
|
||||
h.state_mut().quit_prompt = Some(lightcraft_ui_egui::QuitPrompt::Tasks("1 task is still running: Import. Quitting stops it.".into()));
|
||||
h.run_steps(6);
|
||||
let found = windows(&h);
|
||||
assert!(!found.is_empty(), "the quit prompt is in the tree");
|
||||
for (role, title, modal) in found {
|
||||
assert_eq!((role, modal), (Role::AlertDialog, true), "{title:?} interrupts with a question: a modal alert dialog");
|
||||
}
|
||||
let mut h = harness();
|
||||
h.state_mut().notices.push("Settings could not be saved.".into());
|
||||
h.run_steps(6);
|
||||
let found = windows(&h);
|
||||
assert!(!found.is_empty(), "the notice is in the tree");
|
||||
for (role, title, modal) in found {
|
||||
assert_eq!((role, modal), (Role::AlertDialog, true), "{title:?} is a modal alert dialog");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_library_that_could_not_be_opened_is_a_modal_alert_dialog() {
|
||||
let mut h = harness();
|
||||
h.state_mut().library_problem = Some(lightcraft_ui_egui::panels::library_problem::LibraryProblem::new("/no/such/library", "No catalog here."));
|
||||
h.run_steps(6);
|
||||
let found = windows(&h);
|
||||
assert!(!found.is_empty(), "the library problem is in the tree");
|
||||
for (role, title, modal) in found {
|
||||
assert_eq!((role, modal), (Role::AlertDialog, true), "{title:?} is a modal alert dialog");
|
||||
}
|
||||
}
|
||||
|
||||
/// The description of every alert dialog in the tree.
|
||||
fn alert_descriptions(h: &Harness<'_, LightcraftApp>) -> Vec<String> {
|
||||
h.root()
|
||||
.children_recursive()
|
||||
.map(|n| n.accesskit_node())
|
||||
.filter(|n| n.role() == Role::AlertDialog)
|
||||
.map(|n| n.data().description().unwrap_or_default().to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_alert_carries_what_it_says_as_its_description() {
|
||||
let mut h = harness();
|
||||
h.state_mut().quit_prompt = Some(lightcraft_ui_egui::QuitPrompt::Tasks("1 task is still running: Import. Quitting stops it.".into()));
|
||||
h.run_steps(6);
|
||||
let said = alert_descriptions(&h);
|
||||
assert!(said.iter().any(|d| d.contains("1 task is still running: Import")), "the quit prompt names the task: {said:?}");
|
||||
let mut h = harness();
|
||||
h.state_mut().library_problem = Some(lightcraft_ui_egui::panels::library_problem::LibraryProblem::new("/no/such/library", "No catalog here."));
|
||||
h.run_steps(6);
|
||||
let said = alert_descriptions(&h);
|
||||
assert!(
|
||||
said.iter().any(|d| d.contains("/no/such/library") && d.contains("No catalog here.") && d.contains("untouched")),
|
||||
"the library alert has its path, error and explanation: {said:?}"
|
||||
);
|
||||
}
|
||||
@@ -2,4 +2,5 @@
|
||||
//! `tests/shutdown.rs` stays a binary of its own: it closes the GPU for the whole process.
|
||||
|
||||
mod accessibility;
|
||||
mod dialog_role;
|
||||
mod docking_workflows;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Lightroom parity checklist
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-27 · **Change:** minor (LR-BEHAV-ACCESS: icon-only buttons are named by their label, with the shortcut as the description; MENU-HELP-HELP: on macOS the Help menu is registered with the application only once the menu bar is installed (muda ignores the call before), so AppKit knows which menu is Help; LR-BEHAV-ACCESS: a slider's value field is named `<slider> value`, with how to use it as its description; MENU-BAR: every shortcut the app shows on macOS lists its modifiers ⌃⌥⇧⌘, as the menu bar does; MENU-BAR: on macOS two menu rows that run the same command each get their own native row and label (File ▸ Export… and File ▸ Export with Preset ▸ Custom…; File ▸ New Album… and Photo ▸ Add to Album ▸ New Album…); LR-BEHAV-DIALOGS: a one-line prompt that is refused stays open and says why, and a New Folder on Disk says it was made; LR-FILT-SEARCH-META: `>=` / `<=` in fielded search tokens; MENU-FILE-CLOSE, KEY-CLOSEWINDOW: the notes say Close Window quits the app, after the same check as Quit; MENU-EDIT-SELECTNONE, MENU-EDIT-PASTESELECTED: Edit lists Select Only Active Photo after Deselect All and Paste Settings from Previous after Paste Selected Settings…; on macOS no doubled separators are left where Settings… moved to the app menu; LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-27 · **Change:** minor (LR-BEHAV-ACCESS: every dialog and prompt is exposed as a dialog (a modal one, an alert dialog for the prompts); LR-BEHAV-ACCESS: icon-only buttons are named by their label, with the shortcut as the description; MENU-HELP-HELP: on macOS the Help menu is registered with the application only once the menu bar is installed (muda ignores the call before), so AppKit knows which menu is Help; LR-BEHAV-ACCESS: a slider's value field is named `<slider> value`, with how to use it as its description; MENU-BAR: every shortcut the app shows on macOS lists its modifiers ⌃⌥⇧⌘, as the menu bar does; MENU-BAR: on macOS two menu rows that run the same command each get their own native row and label (File ▸ Export… and File ▸ Export with Preset ▸ Custom…; File ▸ New Album… and Photo ▸ Add to Album ▸ New Album…); LR-BEHAV-DIALOGS: a one-line prompt that is refused stays open and says why, and a New Folder on Disk says it was made; LR-FILT-SEARCH-META: `>=` / `<=` in fielded search tokens; MENU-FILE-CLOSE, KEY-CLOSEWINDOW: the notes say Close Window quits the app, after the same check as Quit; MENU-EDIT-SELECTNONE, MENU-EDIT-PASTESELECTED: Edit lists Select Only Active Photo after Deselect All and Paste Settings from Previous after Paste Selected Settings…; on macOS no doubled separators are left where Settings… moved to the app menu; LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
|
||||
The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we
|
||||
have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good
|
||||
@@ -969,3 +969,4 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
|
||||
| 2026-10-27 | patch | LR-BEHAV-ACCESS: a slider's value field is named "Exposure value" (the slider's name and "value"), and "Click to type a value, drag to change it" is its description, where the whole sentence was part of the name, read on every visit; status stays 🟡 |
|
||||
| 2026-10-27 | patch | MENU-HELP-HELP: on macOS the native Help menu is registered with the application (`NSApp.helpMenu`) only once the menu bar is installed (muda ignores the call before, so before it never was), so AppKit knows which menu is Help; status unchanged (✅) |
|
||||
| 2026-10-28 | patch | LR-BEHAV-ACCESS: an icon-only button's accessible name is its label without the key hint, and the hint ("E", "Shift+P", the platform's own glyphs for Import) is its accessible description; the tooltip is unchanged; status unchanged (🟡) |
|
||||
| 2026-10-28 | patch | LR-BEHAV-ACCESS: every dialog, the library chooser and the quit, library-problem and settings-file prompts are exposed as a modal dialog (an alert dialog for the prompts), and the update check's answer as a non-modal one, instead of a group with the title, and an alert dialog carries what it says (the library problem's path, error and explanation; a prompt's text) as its description (`tests/it/dialog_role.rs`); not tried with a real screen reader; status stays 🟡 |
|
||||
|
||||
@@ -28,6 +28,16 @@
|
||||
|
||||
## 2026-10-28
|
||||
|
||||
### Dialogs are exposed to screen readers as dialogs
|
||||
- Export, Settings and the other dialogs, the library chooser and the prompts that stop the window (the quit question, a
|
||||
library that could not be opened, a settings file problem) are now exposed to screen readers as dialogs: modal ones, so the
|
||||
rest of the window is marked as blocked while one is open, and an alert dialog for the prompts. The answer to Check for
|
||||
Updates is a dialog too, but not a modal one. Before, each was a plain group that only carried the dialog's title.
|
||||
- An alert dialog also carries what it says as its description: a screen reader can read the library problem's path, error and
|
||||
explanation, or a prompt's text, together with the dialog.
|
||||
|
||||
## 2026-10-28
|
||||
|
||||
### Screen readers no longer read the shortcut with every button name
|
||||
- Icon-only buttons (the tool strip, the view buttons and Before/After and Filmstrip in the bottom bar, the top bar's
|
||||
Import Photos…, the mask overlay and White Balance Selector buttons, the slideshow controls) are now announced by
|
||||
|
||||
Reference in New Issue
Block a user