feat: enable more granular a11y feature management (#48625)

* feat: enable more granular a11y feature management

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* Update docs/api/app.md

Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org>

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
This commit is contained in:
trop[bot] 2025-10-23 12:22:17 -04:00 committed by GitHub
commit 1056280b0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 213 additions and 2 deletions

View file

@ -1398,7 +1398,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()`

View file

@ -1164,6 +1164,86 @@ bool App::IsAccessibilitySupportEnabled() {
return mode.has_mode(ui::kAXModeComplete.flags());
}
v8::Local<v8::Value> 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<v8::Local<v8::Value>> 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<v8::Array> 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<std::string>& 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()) {
@ -1835,6 +1915,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",

View file

@ -214,6 +214,10 @@ class App final : public gin::Wrappable<App>,
void DisableHardwareAcceleration(gin_helper::ErrorThrower thrower);
void DisableDomainBlockingFor3DAPIs(gin_helper::ErrorThrower thrower);
bool IsAccessibilitySupportEnabled();
v8::Local<v8::Value> GetAccessibilitySupportFeatures();
void SetAccessibilitySupportFeatures(
gin_helper::ErrorThrower thrower,
const std::vector<std::string>& features);
void SetAccessibilitySupportEnabled(gin_helper::ErrorThrower thrower,
bool enabled);
v8::Local<v8::Value> GetLoginItemSettings(gin::Arguments* args);

View file

@ -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)', () => {