From 92cbc042e84786785e90072d10a32c21e78a320c Mon Sep 17 00:00:00 2001 From: "trop[bot]" <37223003+trop[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 09:06:59 +0100 Subject: [PATCH] feat: enable more granular a11y feature management (#48627) * feat: enable more granular a11y feature management Co-authored-by: Shelley Vohr * Update docs/api/app.md Co-authored-by: John Kleinschmidt Co-authored-by: Shelley Vohr --------- Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Shelley Vohr --- docs/api/app.md | 70 +++++++++++++++++++++- shell/browser/api/electron_api_app.cc | 84 +++++++++++++++++++++++++++ shell/browser/api/electron_api_app.h | 4 ++ spec/api-app-spec.ts | 57 +++++++++++++++++- 4 files changed, 213 insertions(+), 2 deletions(-) diff --git a/docs/api/app.md b/docs/api/app.md index e16d3dc7c33f..fc5582c6b11a 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -1392,7 +1392,75 @@ details. Disabled by default. This API must be called after the `ready` event is emitted. > [!NOTE] -> Rendering accessibility tree can significantly affect the performance of your app. It should not be enabled by default. +> Rendering accessibility tree can significantly affect the performance of your app. It should not be enabled by default. Calling this method will enable the following accessibility support features: `nativeAPIs`, `webContents`, `inlineTextBoxes`, and `extendedProperties`. + +### `app.getAccessibilitySupportFeatures()` _macOS_ _Windows_ + +Returns `string[]` - Array of strings naming currently enabled accessibility support components. Possible values: + +* `nativeAPIs` - Native OS accessibility APIs integration enabled. +* `webContents` - Web contents accessibility tree exposure enabled. +* `inlineTextBoxes` - Inline text boxes (character bounding boxes) enabled. +* `extendedProperties` - Extended accessibility properties enabled. +* `screenReader` - Screen reader specific mode enabled. +* `html` - HTML accessibility tree construction enabled. +* `labelImages` - Accessibility support for automatic image annotations. +* `pdfPrinting` - Accessibility support for PDF printing enabled. + +Notes: + +* The array may be empty if no accessibility modes are active. +* Use `app.isAccessibilitySupportEnabled()` for the legacy boolean check; + prefer this method for granular diagnostics or telemetry. + +Example: + +```js +const { app } = require('electron') + +app.whenReady().then(() => { + if (app.getAccessibilitySupportFeatures().includes('screenReader')) { + // Change some app UI to better work with Screen Readers. + } +}) +``` + +### `app.setAccessibilitySupportFeatures(features)` _macOS_ _Windows_ + +* `features` string[] - An array of the accessibility features to enable. + +Possible values are: + +* `nativeAPIs` - Native OS accessibility APIs integration enabled. +* `webContents` - Web contents accessibility tree exposure enabled. +* `inlineTextBoxes` - Inline text boxes (character bounding boxes) enabled. +* `extendedProperties` - Extended accessibility properties enabled. +* `screenReader` - Screen reader specific mode enabled. +* `html` - HTML accessibility tree construction enabled. +* `labelImages` - Accessibility support for automatic image annotations. +* `pdfPrinting` - Accessibility support for PDF printing enabled. + +To disable all supported features, pass an empty array `[]`. + +Example: + +```js +const { app } = require('electron') + +app.whenReady().then(() => { + // Enable a subset of features: + app.setAccessibilitySupportFeatures([ + 'screenReader', + 'pdfPrinting', + 'webContents' + ]) + + // Other logic + + // Some time later, disable all features: + app.setAccessibilitySupportFeatures([]) +}) +``` ### `app.showAboutPanel()` diff --git a/shell/browser/api/electron_api_app.cc b/shell/browser/api/electron_api_app.cc index dd49315e7ce8..14f8eec31245 100644 --- a/shell/browser/api/electron_api_app.cc +++ b/shell/browser/api/electron_api_app.cc @@ -1162,6 +1162,86 @@ bool App::IsAccessibilitySupportEnabled() { return mode.has_mode(ui::kAXModeComplete.flags()); } +v8::Local App::GetAccessibilitySupportFeatures() { + v8::Isolate* isolate = JavascriptEnvironment::GetIsolate(); + v8::EscapableHandleScope handle_scope(isolate); + auto* ax_state = content::BrowserAccessibilityState::GetInstance(); + ui::AXMode mode = ax_state->GetAccessibilityMode(); + + std::vector> features; + auto push = [&](const char* name) { + features.push_back(v8::String::NewFromUtf8(isolate, name).ToLocalChecked()); + }; + + if (mode.has_mode(ui::AXMode::kNativeAPIs)) + push("nativeAPIs"); + if (mode.has_mode(ui::AXMode::kWebContents)) + push("webContents"); + if (mode.has_mode(ui::AXMode::kInlineTextBoxes)) + push("inlineTextBoxes"); + if (mode.has_mode(ui::AXMode::kExtendedProperties)) + push("extendedProperties"); + if (mode.has_mode(ui::AXMode::kHTML)) + push("html"); + if (mode.has_mode(ui::AXMode::kLabelImages)) + push("labelImages"); + if (mode.has_mode(ui::AXMode::kPDFPrinting)) + push("pdfPrinting"); + if (mode.has_mode(ui::AXMode::kScreenReader)) + push("screenReader"); + + v8::Local arr = v8::Array::New(isolate, features.size()); + for (uint32_t i = 0; i < features.size(); ++i) { + arr->Set(isolate->GetCurrentContext(), i, features[i]).Check(); + } + return handle_scope.Escape(arr); +} + +void App::SetAccessibilitySupportFeatures( + gin_helper::ErrorThrower thrower, + const std::vector& features) { + if (!Browser::Get()->is_ready()) { + thrower.ThrowError( + "app.setAccessibilitySupportFeatures() can only be called after app " + "is ready"); + return; + } + + ui::AXMode mode; + for (const auto& f : features) { + if (f == "nativeAPIs") { + mode.set_mode(ui::AXMode::kNativeAPIs, true); + } else if (f == "webContents") { + mode.set_mode(ui::AXMode::kWebContents, true); + } else if (f == "inlineTextBoxes") { + mode.set_mode(ui::AXMode::kInlineTextBoxes, true); + } else if (f == "extendedProperties") { + mode.set_mode(ui::AXMode::kExtendedProperties, true); + } else if (f == "screenReader") { + mode.set_mode(ui::AXMode::kScreenReader, true); + } else if (f == "html") { + mode.set_mode(ui::AXMode::kHTML, true); + } else if (f == "labelImages") { + mode.set_mode(ui::AXMode::kLabelImages, true); + } else if (f == "pdfPrinting") { + mode.set_mode(ui::AXMode::kPDFPrinting, true); + } else { + thrower.ThrowError("Unknown accessibility feature: " + f); + return; + } + } + + if (mode.is_mode_off()) { + scoped_accessibility_mode_.reset(); + } else { + scoped_accessibility_mode_ = + content::BrowserAccessibilityState::GetInstance() + ->CreateScopedModeForProcess(mode); + } + + Browser::Get()->OnAccessibilitySupportChanged(); +} + void App::SetAccessibilitySupportEnabled(gin_helper::ErrorThrower thrower, bool enabled) { if (!Browser::Get()->is_ready()) { @@ -1820,6 +1900,10 @@ gin::ObjectTemplateBuilder App::GetObjectTemplateBuilder(v8::Isolate* isolate) { .SetMethod("relaunch", &App::Relaunch) .SetMethod("isAccessibilitySupportEnabled", &App::IsAccessibilitySupportEnabled) + .SetMethod("getAccessibilitySupportFeatures", + &App::GetAccessibilitySupportFeatures) + .SetMethod("setAccessibilitySupportFeatures", + &App::SetAccessibilitySupportFeatures) .SetMethod("setAccessibilitySupportEnabled", &App::SetAccessibilitySupportEnabled) .SetMethod("disableHardwareAcceleration", diff --git a/shell/browser/api/electron_api_app.h b/shell/browser/api/electron_api_app.h index 8c591df0819d..a0e82fd16170 100644 --- a/shell/browser/api/electron_api_app.h +++ b/shell/browser/api/electron_api_app.h @@ -211,6 +211,10 @@ class App final : public ElectronBrowserClient::Delegate, void DisableHardwareAcceleration(gin_helper::ErrorThrower thrower); void DisableDomainBlockingFor3DAPIs(gin_helper::ErrorThrower thrower); bool IsAccessibilitySupportEnabled(); + v8::Local GetAccessibilitySupportFeatures(); + void SetAccessibilitySupportFeatures( + gin_helper::ErrorThrower thrower, + const std::vector& features); void SetAccessibilitySupportEnabled(gin_helper::ErrorThrower thrower, bool enabled); v8::Local GetLoginItemSettings(gin::Arguments* args); diff --git a/spec/api-app-spec.ts b/spec/api-app-spec.ts index 34983c484e9e..30e5e96abcb7 100644 --- a/spec/api-app-spec.ts +++ b/spec/api-app-spec.ts @@ -1019,7 +1019,7 @@ describe('app module', () => { }); }); - ifdescribe(process.platform !== 'linux')('accessibilitySupportEnabled property', () => { + ifdescribe(process.platform !== 'linux')('accessibility support functionality', () => { it('is mutable', () => { const values = [false, true, false]; const setters: Array<(arg: boolean) => void> = [ @@ -1037,6 +1037,61 @@ describe('app module', () => { } } }); + + it('getAccessibilitySupportFeatures returns an array with accessibility properties', () => { + const values = [ + 'nativeAPIs', + 'webContents', + 'inlineTextBoxes', + 'extendedProperties', + 'screenReader', + 'html', + 'labelImages', + 'pdfPrinting' + ]; + + app.setAccessibilitySupportEnabled(false); + + const disabled = app.getAccessibilitySupportFeatures(); + expect(disabled).to.be.an('array'); + expect(disabled.includes('complete')).to.equal(false); + + app.setAccessibilitySupportEnabled(true); + const enabled = app.getAccessibilitySupportFeatures(); + expect(enabled).to.be.an('array').with.length.greaterThan(0); + + const boolEnabled = app.isAccessibilitySupportEnabled(); + if (boolEnabled) { + expect(enabled.some(f => values.includes(f))).to.equal(true); + } + }); + + it('setAccessibilitySupportFeatures can enable a subset of features', () => { + app.setAccessibilitySupportEnabled(false); + expect(app.isAccessibilitySupportEnabled()).to.equal(false); + expect(app.getAccessibilitySupportFeatures()).to.be.an('array').that.is.empty(); + + const subsetA = ['webContents', 'html']; + app.setAccessibilitySupportFeatures(subsetA); + const afterSubsetA = app.getAccessibilitySupportFeatures(); + expect(afterSubsetA).to.deep.equal(subsetA); + + const subsetB = [ + 'nativeAPIs', + 'webContents', + 'inlineTextBoxes', + 'extendedProperties' + ]; + app.setAccessibilitySupportFeatures(subsetB); + const afterSubsetB = app.getAccessibilitySupportFeatures(); + expect(afterSubsetB).to.deep.equal(subsetB); + }); + + it('throws when an unknown accessibility feature is requested', () => { + expect(() => { + app.setAccessibilitySupportFeatures(['unknownFeature']); + }).to.throw('Unknown accessibility feature: unknownFeature'); + }); }); ifdescribe(process.platform === 'win32')('setJumpList(categories)', () => {