perf(tv): trim media card focus semantics
This commit is contained in:
@@ -199,6 +199,12 @@ class FocusableWrapper extends StatefulWidget {
|
||||
/// that would compete with this wrapper's focus handling.
|
||||
final bool descendantsAreFocusable;
|
||||
|
||||
/// Whether the [Focus] node contributes focusable/focused semantics.
|
||||
///
|
||||
/// Keep this enabled unless an equivalent child semantic action remains and
|
||||
/// accessibility navigation is known to be inactive.
|
||||
final bool includeFocusSemantics;
|
||||
|
||||
const FocusableWrapper({
|
||||
super.key,
|
||||
required this.child,
|
||||
@@ -232,6 +238,7 @@ class FocusableWrapper extends StatefulWidget {
|
||||
this.useFocusGlow = false,
|
||||
this.delegateFocusBorder = false,
|
||||
this.descendantsAreFocusable = true,
|
||||
this.includeFocusSemantics = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -587,6 +594,7 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
|
||||
Widget result = Focus(
|
||||
focusNode: _focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
includeSemantics: widget.includeFocusSemantics,
|
||||
descendantsAreFocusable: widget.descendantsAreFocusable,
|
||||
onFocusChange: _handleFocusChange,
|
||||
onKeyEvent: _handleKeyEvent,
|
||||
|
||||
@@ -118,6 +118,10 @@ class _FocusableMediaCardState extends State<FocusableMediaCard> {
|
||||
Widget build(BuildContext context) {
|
||||
return FocusableWrapper(
|
||||
focusNode: widget.focusNode,
|
||||
// The MediaCard already exposes the complete static button semantics.
|
||||
// Avoid invalidating a dense TV grid's semantics tree on every D-pad
|
||||
// focus change unless an accessibility service needs focused state.
|
||||
includeFocusSemantics: !PlatformDetector.isTV() || MediaQuery.accessibleNavigationOf(context),
|
||||
onSelect: () => _mediaCardKey.currentState?.handleTap(),
|
||||
onLongPress: () => _mediaCardKey.currentState?.showContextMenu(),
|
||||
onNavigateUp: widget.onNavigateUp,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/focus/focus_glow_overlay.dart';
|
||||
import 'package:plezy/focus/focus_theme.dart';
|
||||
@@ -316,6 +317,54 @@ void main() {
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('TV cards keep focus semantics only for accessible navigation', (tester) async {
|
||||
final semantics = tester.ensureSemantics();
|
||||
TvDetectionService.debugSetAppleTVOverride(true);
|
||||
final focusNode = FocusNode(debugLabel: 'semantic_card');
|
||||
addTearDown(focusNode.dispose);
|
||||
final item = testMediaItem(id: 'focus_semantic_movie', kind: MediaKind.movie, title: 'Focus Semantic Movie');
|
||||
|
||||
Widget card({required bool accessibleNavigation}) => _TestApp(
|
||||
child: MediaQuery(
|
||||
data: MediaQueryData(accessibleNavigation: accessibleNavigation),
|
||||
child: SizedBox(
|
||||
width: 200,
|
||||
height: 330,
|
||||
child: FocusableMediaCard(item: item, forceGridMode: true, focusNode: focusNode, isOffline: true),
|
||||
),
|
||||
),
|
||||
);
|
||||
bool hasFocusedSemantics() {
|
||||
final nodes = <SemanticsNode>[];
|
||||
void collect(SemanticsNode node) {
|
||||
nodes.add(node);
|
||||
node.visitChildren((child) {
|
||||
collect(child);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
collect(tester.binding.renderViews.single.owner!.semanticsOwner!.rootSemanticsNode!);
|
||||
return nodes.any((node) => node.getSemanticsData().flagsCollection.isFocused == ui.Tristate.isTrue);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(card(accessibleNavigation: false));
|
||||
focusNode.requestFocus();
|
||||
await tester.pump();
|
||||
|
||||
final cardData = tester.getSemantics(find.bySemanticsLabel(mediaCardSemanticLabel(item))).getSemanticsData();
|
||||
expect(cardData.flagsCollection.isButton, isTrue);
|
||||
expect(cardData.hasAction(ui.SemanticsAction.tap), isTrue);
|
||||
expect(hasFocusedSemantics(), isFalse);
|
||||
|
||||
await tester.pumpWidget(card(accessibleNavigation: true));
|
||||
focusNode.requestFocus();
|
||||
await tester.pump();
|
||||
|
||||
expect(hasFocusedSemantics(), isTrue);
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('TV cards collapse pointer-only detail semantics without a screen reader', (tester) async {
|
||||
final semantics = tester.ensureSemantics();
|
||||
TvDetectionService.debugSetAppleTVOverride(true);
|
||||
|
||||
Reference in New Issue
Block a user