[A11y] Add UI for AccessibilityOverridesPane (#9892)
* AccessibilityOverridesPane UI
* 1
* resolve comments
* Update overrides_pane.dart
* small fix
* resolve comments
* lint
diff --git a/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart b/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart
index 69f6dd1..887ddf5 100644
--- a/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart
+++ b/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart
@@ -8,6 +8,17 @@
import '../../shared/framework/screen.dart';
import '../../shared/framework/screen_controllers.dart';
+/// Modes for brightness override in the accessibility controls.
+enum BrightnessOverride {
+ system('System Default'),
+ light('Light Mode'),
+ dark('Dark Mode');
+
+ const BrightnessOverride(this.display);
+
+ final String display;
+}
+
/// Controller for the Accessibility screen.
class AccessibilityController extends DevToolsScreenController
with AutoDisposeControllerMixin {
@@ -49,7 +60,9 @@
final screenId = ScreenMetaData.accessibility.id;
// --- Accessibility Overrides State ---
- final brightness = ValueNotifier<String>('System');
+ final brightness = ValueNotifier<BrightnessOverride>(
+ BrightnessOverride.system,
+ );
final textScale = ValueNotifier<double>(1.0);
final boldText = ValueNotifier<bool>(false);
final screenReader = ValueNotifier<bool>(false);
diff --git a/packages/devtools_app/lib/src/screens/accessibility/accessibility_screen.dart b/packages/devtools_app/lib/src/screens/accessibility/accessibility_screen.dart
index 6c3efd2..fc38754 100644
--- a/packages/devtools_app/lib/src/screens/accessibility/accessibility_screen.dart
+++ b/packages/devtools_app/lib/src/screens/accessibility/accessibility_screen.dart
@@ -8,8 +8,12 @@
import '../../shared/framework/screen.dart';
import '../../shared/globals.dart';
-import '../../shared/ui/common_widgets.dart';
import 'accessibility_controller.dart';
+import 'overrides_pane.dart';
+import 'semantics_tree_pane.dart';
+
+export 'overrides_pane.dart';
+export 'semantics_tree_pane.dart';
/// A screen that displays accessibility information.
class AccessibilityScreen extends Screen {
@@ -58,37 +62,3 @@
return screenSize.width > 1000 ? Axis.horizontal : Axis.vertical;
}
}
-
-/// A pane that displays the semantics tree of the connected app.
-class AccessibilitySemanticsTreePane extends StatelessWidget {
- const AccessibilitySemanticsTreePane({super.key});
-
- @override
- Widget build(BuildContext context) {
- return const DevToolsAreaPane(
- header: AreaPaneHeader(title: Text('Semantics Tree')),
- child: CenteredMessage(
- message:
- 'Accessibility semantics tree placeholder.\n'
- '// TODO(hannah-hyj): Implement semantics tree view and details explorer.',
- ),
- );
- }
-}
-
-/// A pane that displays the accessibility overrides controls.
-class AccessibilityOverridesPane extends StatelessWidget {
- const AccessibilityOverridesPane({super.key});
-
- @override
- Widget build(BuildContext context) {
- return const DevToolsAreaPane(
- header: AreaPaneHeader(title: Text('Accessibility Overrides')),
- child: CenteredMessage(
- message:
- 'Accessibility overrides placeholder.\n'
- '// TODO(hannah-hyj): Implement setting overrides (brightness, text scale, bold text, screen reader, high contrast).',
- ),
- );
- }
-}
diff --git a/packages/devtools_app/lib/src/screens/accessibility/overrides_pane.dart b/packages/devtools_app/lib/src/screens/accessibility/overrides_pane.dart
new file mode 100644
index 0000000..f50ee3a
--- /dev/null
+++ b/packages/devtools_app/lib/src/screens/accessibility/overrides_pane.dart
@@ -0,0 +1,204 @@
+// Copyright 2026 The Flutter Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
+
+import 'package:devtools_app_shared/ui.dart';
+import 'package:flutter/material.dart';
+
+import '../../shared/globals.dart';
+import '../../shared/ui/common_widgets.dart';
+import 'accessibility_controller.dart';
+
+/// A pane that displays the accessibility overrides controls.
+class AccessibilityOverridesPane extends StatelessWidget {
+ const AccessibilityOverridesPane({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final controller = screenControllers.lookup<AccessibilityController>();
+ return DevToolsAreaPane(
+ header: const AreaPaneHeader(
+ title: Text('Accessibility Overrides'),
+ roundedTopBorder: false,
+ includeTopBorder: false,
+ ),
+ child: Scrollbar(
+ child: SingleChildScrollView(
+ padding: const EdgeInsets.all(defaultSpacing),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Simulate and test accessibility settings on the connected device in real-time.',
+ style: theme.subtleTextStyle,
+ ),
+ const SizedBox(height: defaultSpacing),
+ const Divider(),
+ const SizedBox(height: denseSpacing),
+ _BrightnessOverride(controller: controller),
+ const SizedBox(height: defaultSpacing),
+ _TextScaleOverride(controller: controller),
+ const SizedBox(height: defaultSpacing),
+ _SwitchOverride(
+ label: 'Bold Text',
+ description: 'Forces all text in the application to be bold.',
+ notifier: controller.boldText,
+ ),
+ const SizedBox(height: defaultSpacing),
+ _SwitchOverride(
+ label: 'Screen Reader Debugger',
+ description: 'Debug and test screen reader layouts.',
+ notifier: controller.screenReader,
+ ),
+ const SizedBox(height: defaultSpacing),
+ _SwitchOverride(
+ label: 'High Contrast',
+ description: 'Increases the contrast of text and icons.',
+ notifier: controller.highContrast,
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _AccessibilityPanelLabel extends StatelessWidget {
+ const _AccessibilityPanelLabel({
+ required this.label,
+ required this.description,
+ });
+
+ final String label;
+ final String description;
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(label, style: theme.boldTextStyle),
+ const SizedBox(height: densePadding),
+ Text(description, style: theme.subtleTextStyle),
+ ],
+ );
+ }
+}
+
+class _BrightnessOverride extends StatelessWidget {
+ const _BrightnessOverride({required this.controller});
+
+ final AccessibilityController controller;
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const _AccessibilityPanelLabel(
+ label: 'Brightness',
+ description: 'Override the color scheme mode of the app.',
+ ),
+ const SizedBox(height: denseSpacing),
+ ValueListenableBuilder<BrightnessOverride>(
+ valueListenable: controller.brightness,
+ builder: (context, value, _) {
+ return RoundedDropDownButton<BrightnessOverride>(
+ isExpanded: true,
+ value: value,
+ items: BrightnessOverride.values.map((option) {
+ return DropdownMenuItem<BrightnessOverride>(
+ value: option,
+ child: Text(option.display),
+ );
+ }).toList(),
+ onChanged: (newValue) {
+ if (newValue != null) {
+ controller.brightness.value = newValue;
+ }
+ },
+ );
+ },
+ ),
+ ],
+ );
+ }
+}
+
+class _TextScaleOverride extends StatelessWidget {
+ const _TextScaleOverride({required this.controller});
+
+ final AccessibilityController controller;
+
+ static const _minTextScale = 0.5;
+ static const _maxTextScale = 3.0;
+ static const _textScaleDivisions = 25;
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ return ValueListenableBuilder<double>(
+ valueListenable: controller.textScale,
+ builder: (context, value, _) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const _AccessibilityPanelLabel(
+ label: 'Text Scale',
+ description: 'Scale the system font size.',
+ ),
+ Text(
+ '${value.toStringAsFixed(2)}x',
+ style: theme.boldTextStyle,
+ ),
+ ],
+ ),
+ const SizedBox(height: densePadding),
+ Slider(
+ value: value,
+ min: _minTextScale,
+ max: _maxTextScale,
+ divisions: _textScaleDivisions,
+ onChanged: (newValue) {
+ controller.textScale.value = newValue;
+ },
+ ),
+ ],
+ );
+ },
+ );
+ }
+}
+
+class _SwitchOverride extends StatelessWidget {
+ const _SwitchOverride({
+ required this.label,
+ required this.description,
+ required this.notifier,
+ });
+
+ final String label;
+ final String description;
+ final ValueNotifier<bool> notifier;
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ children: [
+ Expanded(
+ child: _AccessibilityPanelLabel(
+ label: label,
+ description: description,
+ ),
+ ),
+ NotifierSwitch(notifier: notifier),
+ ],
+ );
+ }
+}
diff --git a/packages/devtools_app/lib/src/screens/accessibility/semantics_tree_pane.dart b/packages/devtools_app/lib/src/screens/accessibility/semantics_tree_pane.dart
new file mode 100644
index 0000000..8081874
--- /dev/null
+++ b/packages/devtools_app/lib/src/screens/accessibility/semantics_tree_pane.dart
@@ -0,0 +1,29 @@
+// Copyright 2026 The Flutter Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
+
+import 'package:devtools_app_shared/ui.dart';
+import 'package:flutter/material.dart';
+
+import '../../shared/ui/common_widgets.dart';
+
+/// A pane that displays the semantics tree of the connected app.
+class AccessibilitySemanticsTreePane extends StatelessWidget {
+ const AccessibilitySemanticsTreePane({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const DevToolsAreaPane(
+ header: AreaPaneHeader(
+ title: Text('Semantics Tree'),
+ includeTopBorder: false,
+ roundedTopBorder: false,
+ ),
+ child: CenteredMessage(
+ message:
+ 'Accessibility semantics tree placeholder.\n'
+ '// TODO(hannah-hyj): Implement semantics tree view and details explorer.',
+ ),
+ );
+ }
+}
diff --git a/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart b/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart
index 595cf4a..ba5e53e 100644
--- a/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart
+++ b/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart
@@ -76,5 +76,110 @@
// Semantics Tree pane should be visible
expect(find.byType(AccessibilitySemanticsTreePane), findsOneWidget);
});
+
+ testWidgetsWithWindowSize(
+ 'renders all override controls in AccessibilityOverridesPane',
+ windowSize,
+ (WidgetTester tester) async {
+ await pumpAccessibilityScreen(tester);
+ await tester.pumpAndSettle();
+
+ expect(find.text('Accessibility Overrides'), findsOneWidget);
+ expect(
+ find.text(
+ 'Simulate and test accessibility settings on the connected device in real-time.',
+ ),
+ findsOneWidget,
+ );
+
+ // Brightness controls
+ expect(find.text('Brightness'), findsOneWidget);
+ expect(
+ find.text('Override the color scheme mode of the app.'),
+ findsOneWidget,
+ );
+ expect(
+ find.byType(RoundedDropDownButton<BrightnessOverride>),
+ findsOneWidget,
+ );
+
+ // Text Scale controls
+ expect(find.text('Text Scale'), findsOneWidget);
+ expect(find.text('Scale the system font size.'), findsOneWidget);
+ expect(find.text('1.00x'), findsOneWidget);
+ expect(find.byType(Slider), findsOneWidget);
+
+ // Switches
+ expect(find.text('Bold Text'), findsOneWidget);
+ expect(
+ find.text('Forces all text in the application to be bold.'),
+ findsOneWidget,
+ );
+ expect(find.text('Screen Reader Debugger'), findsOneWidget);
+ expect(
+ find.text('Debug and test screen reader layouts.'),
+ findsOneWidget,
+ );
+ expect(find.text('High Contrast'), findsOneWidget);
+ expect(
+ find.text('Increases the contrast of text and icons.'),
+ findsOneWidget,
+ );
+ expect(find.byType(Switch), findsNWidgets(3));
+ },
+ );
+
+ testWidgetsWithWindowSize(
+ 'interacting with override controls updates controller state',
+ windowSize,
+ (WidgetTester tester) async {
+ await pumpAccessibilityScreen(tester);
+ await tester.pumpAndSettle();
+
+ Finder findSwitchFor(String label) {
+ return find.descendant(
+ of: find.ancestor(of: find.text(label), matching: find.byType(Row)),
+ matching: find.byType(Switch),
+ );
+ }
+
+ // 1. Test Brightness Dropdown
+ expect(controller.brightness.value, BrightnessOverride.system);
+ await tester.tap(
+ find.byType(RoundedDropDownButton<BrightnessOverride>),
+ );
+ await tester.pumpAndSettle();
+ await tester.tap(find.text('Light Mode').last);
+ await tester.pumpAndSettle();
+ expect(controller.brightness.value, BrightnessOverride.light);
+
+ // 2. Test Bold Text Switch
+ expect(controller.boldText.value, isFalse);
+ final boldTextSwitch = findSwitchFor('Bold Text');
+ await tester.ensureVisible(boldTextSwitch);
+ await tester.pumpAndSettle();
+ await tester.tap(boldTextSwitch);
+ await tester.pumpAndSettle();
+ expect(controller.boldText.value, isTrue);
+
+ // 3. Test Screen Reader Debugger Switch
+ expect(controller.screenReader.value, isFalse);
+ final screenReaderSwitch = findSwitchFor('Screen Reader Debugger');
+ await tester.ensureVisible(screenReaderSwitch);
+ await tester.pumpAndSettle();
+ await tester.tap(screenReaderSwitch);
+ await tester.pumpAndSettle();
+ expect(controller.screenReader.value, isTrue);
+
+ // 4. Test High Contrast Switch
+ expect(controller.highContrast.value, isFalse);
+ final highContrastSwitch = findSwitchFor('High Contrast');
+ await tester.ensureVisible(highContrastSwitch);
+ await tester.pumpAndSettle();
+ await tester.tap(highContrastSwitch);
+ await tester.pumpAndSettle();
+ expect(controller.highContrast.value, isTrue);
+ },
+ );
});
}