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:
parent
4fda94be9b
commit
1056280b0a
4 changed files with 213 additions and 2 deletions
|
|
@ -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()`
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue