Improve message action accessibility

Co-authored-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
This commit is contained in:
Kenny Lopez
2026-08-15 12:39:25 +01:00
committed by Princess Donut
parent aab75fb9a9
commit e87ee8a7ba
4 changed files with 88 additions and 16 deletions
@@ -73,6 +73,11 @@ enum NativeMessageActionSurfaceLayout {
@available(iOS 16.0, *)
enum NativeMessageActionSurfaceAppearance {
// The native list is only one sibling inside the Flutter-owned dialog.
// Keeping it non-modal leaves the reaction tray and dismiss barrier
// reachable to VoiceOver.
static let actionListAccessibilityViewIsModal = false
static func interfaceStyle(from value: Any?) -> UIUserInterfaceStyle {
switch value as? String {
case "dark":
@@ -280,7 +285,8 @@ final class NativeMessageActionSurfacePlatformView: NSObject,
surfaceView.backgroundColor = .clear
surfaceView.clipsToBounds = false
surfaceView.accessibilityViewIsModal = true
surfaceView.accessibilityViewIsModal =
NativeMessageActionSurfaceAppearance.actionListAccessibilityViewIsModal
surfaceView.overrideUserInterfaceStyle = interfaceStyle
backdropView.translatesAutoresizingMaskIntoConstraints = false
+6
View File
@@ -482,6 +482,12 @@ class RunnerTests: XCTestCase {
)
}
func testNativeMessageActionListDoesNotHideDialogSiblings() {
XCTAssertFalse(
NativeMessageActionSurfaceAppearance.actionListAccessibilityViewIsModal
)
}
func testNativeMessageActionSurfaceMatchesFlutterInterfaceStyle() {
XCTAssertEqual(
NativeMessageActionSurfaceAppearance.interfaceStyle(from: "dark"),
@@ -1,6 +1,7 @@
part of '../message_actions.dart';
const _messageActionRowHeight = 48.0;
const _messageActionRowVerticalPadding = Grid.xxs;
const _messageActionSeparatorHeight = 0.5;
const _messageActionVerticalInset = Grid.half;
const _messageActionMenuMaxWidth = 288.0;
@@ -524,11 +525,10 @@ class _MessageActionsPopover extends StatelessWidget {
final availableHeight = math.max(1.0, safeBottom - safeTop);
final trayWidth = math.min(_reactionTrayMaxWidth, availableWidth);
final menuWidth = math.min(_messageActionMenuMaxWidth, availableWidth);
final preferredMenuHeight = _messageActionSurfacePreferredHeight(
actions,
);
final menuLayout = _MessageActionSurfaceLayout.from(context, actions);
final preferredMenuHeight = menuLayout.preferredHeight;
final menuBudget = math.max(
_messageActionRowHeight,
menuLayout.rowHeight,
availableHeight -
_reactionTrayMaxHeight -
(_messageActionGap * 2) -
@@ -805,6 +805,7 @@ class _MessageActionSurface extends StatelessWidget {
@override
Widget build(BuildContext context) {
final menuLayout = _MessageActionSurfaceLayout.from(context, actions);
return Material(
key: const ValueKey('message-action-surface'),
color: context.colors.surface,
@@ -841,6 +842,7 @@ class _MessageActionSurface extends StatelessWidget {
),
_MessageActionRow(
action: actions[index],
height: menuLayout.rowHeight,
onSelected: onSelected,
),
],
@@ -854,9 +856,14 @@ class _MessageActionSurface extends StatelessWidget {
class _MessageActionRow extends StatelessWidget {
final _PopoverMessageAction action;
final double height;
final ValueChanged<String> onSelected;
const _MessageActionRow({required this.action, required this.onSelected});
const _MessageActionRow({
required this.action,
required this.height,
required this.onSelected,
});
@override
Widget build(BuildContext context) {
@@ -874,7 +881,7 @@ class _MessageActionRow extends StatelessWidget {
onSelected(action.id);
},
child: SizedBox(
height: _messageActionRowHeight,
height: height,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: Grid.xs),
child: Row(
@@ -905,14 +912,45 @@ class _MessageActionRow extends StatelessWidget {
}
}
double _messageActionSurfacePreferredHeight(
List<_PopoverMessageAction> actions,
) {
var separatorCount = 0;
for (var index = 1; index < actions.length; index++) {
if (actions[index - 1].group != actions[index].group) separatorCount += 1;
class _MessageActionSurfaceLayout {
final double rowHeight;
final double preferredHeight;
const _MessageActionSurfaceLayout({
required this.rowHeight,
required this.preferredHeight,
});
factory _MessageActionSurfaceLayout.from(
BuildContext context,
List<_PopoverMessageAction> actions,
) {
final textPainter = TextPainter(
text: TextSpan(
text: 'Message action',
style: context.textTheme.bodyLarge,
),
textDirection: Directionality.of(context),
textScaler: MediaQuery.textScalerOf(context),
maxLines: 1,
)..layout();
final rowHeight = math.max(
_messageActionRowHeight,
textPainter.height + (_messageActionRowVerticalPadding * 2),
);
textPainter.dispose();
var separatorCount = 0;
for (var index = 1; index < actions.length; index++) {
if (actions[index - 1].group != actions[index].group) separatorCount += 1;
}
final preferredHeight =
(_messageActionVerticalInset * 2) +
(actions.length * rowHeight) +
(separatorCount * _messageActionSeparatorHeight);
return _MessageActionSurfaceLayout(
rowHeight: rowHeight,
preferredHeight: preferredHeight,
);
}
return (_messageActionVerticalInset * 2) +
(actions.length * _messageActionRowHeight) +
(separatorCount * _messageActionSeparatorHeight);
}
@@ -214,6 +214,7 @@ Future<_MessageActionsPopoverHarness> _pumpMessageActionsPopover(
ReminderService? reminderService,
bool disableAnimations = false,
EdgeInsets viewInsets = EdgeInsets.zero,
TextScaler textScaler = TextScaler.noScaling,
FocusNode? composerFocusNode,
bool composerInitiallyFocused = false,
Rect anchorRect = const Rect.fromLTWH(32, 260, 300, 72),
@@ -239,6 +240,7 @@ Future<_MessageActionsPopoverHarness> _pumpMessageActionsPopover(
data: MediaQuery.of(context).copyWith(
disableAnimations: disableAnimations,
viewInsets: viewInsets,
textScaler: textScaler,
),
child: child!,
),
@@ -792,6 +794,26 @@ void main() {
await _dismissMessageActionsPopover(tester);
});
testWidgets('fallback action rows grow with accessibility text', (
tester,
) async {
final prefs = await _mockPrefs();
await _pumpMessageActionsPopover(
tester,
message: _message(rootId: 'root-9'),
prefs: prefs,
textScaler: const TextScaler.linear(3),
);
final rowFinder = find.byKey(
const ValueKey('message-action-followThread'),
);
expect(tester.getSize(rowFinder).height, greaterThan(48));
expect(tester.takeException(), isNull);
await _dismissMessageActionsPopover(tester);
});
testWidgets('orders primary, utility, and destructive action groups', (
tester,
) async {