mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user