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.
|
/// that would compete with this wrapper's focus handling.
|
||||||
final bool descendantsAreFocusable;
|
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({
|
const FocusableWrapper({
|
||||||
super.key,
|
super.key,
|
||||||
required this.child,
|
required this.child,
|
||||||
@@ -232,6 +238,7 @@ class FocusableWrapper extends StatefulWidget {
|
|||||||
this.useFocusGlow = false,
|
this.useFocusGlow = false,
|
||||||
this.delegateFocusBorder = false,
|
this.delegateFocusBorder = false,
|
||||||
this.descendantsAreFocusable = true,
|
this.descendantsAreFocusable = true,
|
||||||
|
this.includeFocusSemantics = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -587,6 +594,7 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
|
|||||||
Widget result = Focus(
|
Widget result = Focus(
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
autofocus: widget.autofocus,
|
autofocus: widget.autofocus,
|
||||||
|
includeSemantics: widget.includeFocusSemantics,
|
||||||
descendantsAreFocusable: widget.descendantsAreFocusable,
|
descendantsAreFocusable: widget.descendantsAreFocusable,
|
||||||
onFocusChange: _handleFocusChange,
|
onFocusChange: _handleFocusChange,
|
||||||
onKeyEvent: _handleKeyEvent,
|
onKeyEvent: _handleKeyEvent,
|
||||||
|
|||||||
@@ -118,6 +118,10 @@ class _FocusableMediaCardState extends State<FocusableMediaCard> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FocusableWrapper(
|
return FocusableWrapper(
|
||||||
focusNode: widget.focusNode,
|
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(),
|
onSelect: () => _mediaCardKey.currentState?.handleTap(),
|
||||||
onLongPress: () => _mediaCardKey.currentState?.showContextMenu(),
|
onLongPress: () => _mediaCardKey.currentState?.showContextMenu(),
|
||||||
onNavigateUp: widget.onNavigateUp,
|
onNavigateUp: widget.onNavigateUp,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/semantics.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:plezy/focus/focus_glow_overlay.dart';
|
import 'package:plezy/focus/focus_glow_overlay.dart';
|
||||||
import 'package:plezy/focus/focus_theme.dart';
|
import 'package:plezy/focus/focus_theme.dart';
|
||||||
@@ -316,6 +317,54 @@ void main() {
|
|||||||
semantics.dispose();
|
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 {
|
testWidgets('TV cards collapse pointer-only detail semantics without a screen reader', (tester) async {
|
||||||
final semantics = tester.ensureSemantics();
|
final semantics = tester.ensureSemantics();
|
||||||
TvDetectionService.debugSetAppleTVOverride(true);
|
TvDetectionService.debugSetAppleTVOverride(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user