Unverified Commit 9884b5f5 authored by chunhtai's avatar chunhtai Committed by GitHub

Fixes IgnorePointer and AbsorbPointer to only block user interactions… (#120619)

Fixes IgnorePointer and AbsorbPointer to only block user interactions…
parent c52042fb
......@@ -1317,7 +1317,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
key: editableTextKey,
controller: controller,
undoController: widget.undoController,
readOnly: widget.readOnly,
readOnly: widget.readOnly || !enabled,
toolbarOptions: widget.toolbarOptions,
showCursor: widget.showCursor,
showSelectionHandles: _showSelectionHandles,
......
......@@ -1403,7 +1403,6 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
items.add(DefaultTextStyle(
style: _textStyle!.copyWith(color: Theme.of(context).hintColor),
child: IgnorePointer(
ignoringSemantics: false,
child: _DropdownMenuItemContainer(
alignment: widget.alignment,
child: displayedHint,
......
......@@ -3434,6 +3434,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
}
final _SemanticsFragment fragment = _getSemanticsForParent(
mergeIntoParent: _semantics?.parent?.isPartOfNodeMerging ?? false,
blockUserActions: _semantics?.areUserActionsBlocked ?? false,
);
assert(fragment is _InterestingSemanticsFragment);
final _InterestingSemanticsFragment interestingFragment = fragment as _InterestingSemanticsFragment;
......@@ -3453,13 +3454,14 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// Returns the semantics that this node would like to add to its parent.
_SemanticsFragment _getSemanticsForParent({
required bool mergeIntoParent,
required bool blockUserActions,
}) {
assert(!_needsLayout, 'Updated layout information required for $this to calculate semantics.');
final SemanticsConfiguration config = _semanticsConfiguration;
bool dropSemanticsOfPreviousSiblings = config.isBlockingSemanticsOfPreviouslyPaintedNodes;
bool producesForkingFragment = !config.hasBeenAnnotated && !config.isSemanticBoundary;
final bool blockChildInteractions = blockUserActions || config.isBlockingUserActions;
final bool childrenMergeIntoParent = mergeIntoParent || config.isMergingSemanticsOfDescendants;
final List<SemanticsConfiguration> childConfigurations = <SemanticsConfiguration>[];
final bool explicitChildNode = config.explicitChildNodes || parent is! RenderObject;
......@@ -3472,6 +3474,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
assert(!_needsLayout);
final _SemanticsFragment parentFragment = renderChild._getSemanticsForParent(
mergeIntoParent: childrenMergeIntoParent,
blockUserActions: blockChildInteractions,
);
if (parentFragment.dropsSemanticsOfPreviousSiblings) {
childConfigurations.clear();
......@@ -3562,6 +3565,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
siblingMergeFragmentGroups.forEach(_marksExplicitInMergeGroup);
result = _SwitchableSemanticsFragment(
config: config,
blockUserActions: blockUserActions,
mergeIntoParent: mergeIntoParent,
siblingMergeGroups: siblingMergeFragmentGroups,
owner: this,
......@@ -4571,13 +4575,19 @@ class _IncompleteSemanticsFragment extends _InterestingSemanticsFragment {
class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
_SwitchableSemanticsFragment({
required bool mergeIntoParent,
required bool blockUserActions,
required SemanticsConfiguration config,
required List<List<_InterestingSemanticsFragment>> siblingMergeGroups,
required super.owner,
required super.dropsSemanticsOfPreviousSiblings,
}) : _siblingMergeGroups = siblingMergeGroups,
_mergeIntoParent = mergeIntoParent,
_config = config;
_config = config {
if (blockUserActions && !_config.isBlockingUserActions) {
_ensureConfigIsWritable();
_config.isBlockingUserActions = true;
}
}
final bool _mergeIntoParent;
SemanticsConfiguration _config;
......@@ -4603,12 +4613,8 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
final _SwitchableSemanticsFragment switchableFragment = fragment as _SwitchableSemanticsFragment;
switchableFragment._mergesToSibling = true;
node ??= fragment.owner._semantics;
if (configuration == null) {
switchableFragment._ensureConfigIsWritable();
configuration = switchableFragment.config;
} else {
configuration.absorb(switchableFragment.config!);
}
configuration ??= SemanticsConfiguration();
configuration.absorb(switchableFragment.config!);
// It is a child fragment of a _SwitchableFragment, it must have a
// geometry.
final _SemanticsGeometry geometry = switchableFragment._computeSemanticsGeometry(
......
......@@ -3572,9 +3572,7 @@ class RenderRepaintBoundary extends RenderProxyBox {
/// as usual. It just cannot be the target of located events, because its render
/// object returns false from [hitTest].
///
/// When [ignoringSemantics] is true, the subtree will be invisible to
/// the semantics layer (and thus e.g. accessibility tools). If
/// [ignoringSemantics] is null, it uses the value of [ignoring].
/// {@macro flutter.widgets.IgnorePointer.Semantics}
///
/// See also:
///
......@@ -3583,11 +3581,14 @@ class RenderRepaintBoundary extends RenderProxyBox {
class RenderIgnorePointer extends RenderProxyBox {
/// Creates a render object that is invisible to hit testing.
///
/// The [ignoring] argument must not be null. If [ignoringSemantics] is null,
/// this render object will be ignored for semantics if [ignoring] is true.
/// The [ignoring] argument must not be null.
RenderIgnorePointer({
RenderBox? child,
bool ignoring = true,
@Deprecated(
'Use ExcludeSemantics or create a custom ignore pointer widget instead. '
'This feature was deprecated after v3.8.0-12.0.pre.'
)
bool? ignoringSemantics,
}) : _ignoring = ignoring,
_ignoringSemantics = ignoringSemantics,
......@@ -3597,6 +3598,8 @@ class RenderIgnorePointer extends RenderProxyBox {
///
/// Regardless of whether this render object is ignored during hit testing, it
/// will still consume space during layout and be visible during painting.
///
/// {@macro flutter.widgets.IgnorePointer.Semantics}
bool get ignoring => _ignoring;
bool _ignoring;
set ignoring(bool value) {
......@@ -3604,55 +3607,60 @@ class RenderIgnorePointer extends RenderProxyBox {
return;
}
_ignoring = value;
if (_ignoringSemantics == null || !_ignoringSemantics!) {
if (ignoringSemantics == null) {
markNeedsSemanticsUpdate();
}
}
/// Whether the semantics of this render object is ignored when compiling the semantics tree.
///
/// If null, defaults to value of [ignoring].
/// {@macro flutter.widgets.IgnorePointer.Semantics}
///
/// See [SemanticsNode] for additional information about the semantics tree.
@Deprecated(
'Use ExcludeSemantics or create a custom ignore pointer widget instead. '
'This feature was deprecated after v3.8.0-12.0.pre.'
)
bool? get ignoringSemantics => _ignoringSemantics;
bool? _ignoringSemantics;
set ignoringSemantics(bool? value) {
if (value == _ignoringSemantics) {
return;
}
final bool oldEffectiveValue = _effectiveIgnoringSemantics;
_ignoringSemantics = value;
if (oldEffectiveValue != _effectiveIgnoringSemantics) {
markNeedsSemanticsUpdate();
}
markNeedsSemanticsUpdate();
}
bool get _effectiveIgnoringSemantics => ignoringSemantics ?? ignoring;
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
return !ignoring && super.hitTest(result, position: position);
}
// TODO(ianh): figure out a way to still include labels and flags in
// descendants, just make them non-interactive, even when
// _effectiveIgnoringSemantics is true
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (child != null && !_effectiveIgnoringSemantics) {
visitor(child!);
if (_ignoringSemantics ?? false) {
return;
}
super.visitChildrenForSemantics(visitor);
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
// Do not block user interactions if _ignoringSemantics is false; otherwise,
// delegate to _ignoring
config.isBlockingUserActions = _ignoring && (_ignoringSemantics ?? true);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('ignoring', ignoring));
properties.add(DiagnosticsProperty<bool>('ignoring', _ignoring));
properties.add(
DiagnosticsProperty<bool>(
'ignoringSemantics',
_effectiveIgnoringSemantics,
description: ignoringSemantics == null ? 'implicitly $_effectiveIgnoringSemantics' : null,
_ignoringSemantics,
description: _ignoringSemantics == null ? null : 'implicitly $_ignoringSemantics',
),
);
}
......@@ -3807,6 +3815,8 @@ class RenderOffstage extends RenderProxyBox {
/// its children from being the target of located events, because its render
/// object returns true from [hitTest].
///
/// {@macro flutter.widgets.AbsorbPointer.Semantics}
///
/// See also:
///
/// * [RenderIgnorePointer], which has the opposite effect: removing the
......@@ -3818,6 +3828,10 @@ class RenderAbsorbPointer extends RenderProxyBox {
RenderAbsorbPointer({
RenderBox? child,
bool absorbing = true,
@Deprecated(
'Use ExcludeSemantics or create a custom absorb pointer widget instead. '
'This feature was deprecated after v3.8.0-12.0.pre.'
)
bool? ignoringSemantics,
}) : _absorbing = absorbing,
_ignoringSemantics = ignoringSemantics,
......@@ -3828,6 +3842,8 @@ class RenderAbsorbPointer extends RenderProxyBox {
/// Regardless of whether this render object absorbs pointers during hit
/// testing, it will still consume space during layout and be visible during
/// painting.
///
/// {@macro flutter.widgets.AbsorbPointer.Semantics}
bool get absorbing => _absorbing;
bool _absorbing;
set absorbing(bool value) {
......@@ -3840,26 +3856,26 @@ class RenderAbsorbPointer extends RenderProxyBox {
}
}
/// Whether the semantics of this render object is ignored when compiling the semantics tree.
/// Whether the semantics of this render object is ignored when compiling the
/// semantics tree.
///
/// If null, defaults to value of [absorbing].
/// {@macro flutter.widgets.AbsorbPointer.Semantics}
///
/// See [SemanticsNode] for additional information about the semantics tree.
@Deprecated(
'Use ExcludeSemantics or create a custom absorb pointer widget instead. '
'This feature was deprecated after v3.8.0-12.0.pre.'
)
bool? get ignoringSemantics => _ignoringSemantics;
bool? _ignoringSemantics;
set ignoringSemantics(bool? value) {
if (value == _ignoringSemantics) {
return;
}
final bool oldEffectiveValue = _effectiveIgnoringSemantics;
_ignoringSemantics = value;
if (oldEffectiveValue != _effectiveIgnoringSemantics) {
markNeedsSemanticsUpdate();
}
markNeedsSemanticsUpdate();
}
bool get _effectiveIgnoringSemantics => ignoringSemantics ?? absorbing;
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
return absorbing
......@@ -3869,9 +3885,18 @@ class RenderAbsorbPointer extends RenderProxyBox {
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (child != null && !_effectiveIgnoringSemantics) {
visitor(child!);
if (_ignoringSemantics ?? false) {
return;
}
super.visitChildrenForSemantics(visitor);
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
// Do not block user interactions if _ignoringSemantics is false; otherwise,
// delegate to absorbing
config.isBlockingUserActions = absorbing && (_ignoringSemantics ?? true);
}
@override
......@@ -3881,8 +3906,8 @@ class RenderAbsorbPointer extends RenderProxyBox {
properties.add(
DiagnosticsProperty<bool>(
'ignoringSemantics',
_effectiveIgnoringSemantics,
description: ignoringSemantics == null ? 'implicitly $_effectiveIgnoringSemantics' : null,
ignoringSemantics,
description: ignoringSemantics == null ? null : 'implicitly $ignoringSemantics',
),
);
}
......@@ -4120,10 +4145,12 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
bool container = false,
bool explicitChildNodes = false,
bool excludeSemantics = false,
bool blockUserActions = false,
TextDirection? textDirection,
}) : _container = container,
_explicitChildNodes = explicitChildNodes,
_excludeSemantics = excludeSemantics,
_blockUserActions = blockUserActions,
_textDirection = textDirection,
_properties = properties,
super(child) {
......@@ -4198,6 +4225,21 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate();
}
/// Whether to block user interactions for the semantics subtree.
///
/// Setting this true prevents user from activating pointer related
/// [SemanticsAction]s, such as [SemanticsAction.tap] or
/// [SemanticsAction.longPress].
bool get blockUserActions => _blockUserActions;
bool _blockUserActions;
set blockUserActions(bool value) {
if (_blockUserActions == value) {
return;
}
_blockUserActions = value;
markNeedsSemanticsUpdate();
}
void _updateAttributedFields(SemanticsProperties value) {
_attributedLabel = _effectiveAttributedLabel(value);
_attributedValue = _effectiveAttributedValue(value);
......@@ -4274,6 +4316,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
super.describeSemanticsConfiguration(config);
config.isSemanticBoundary = container;
config.explicitChildNodes = explicitChildNodes;
config.isBlockingUserActions = blockUserActions;
assert(
((_properties.scopesRoute ?? false) && explicitChildNodes) || !(_properties.scopesRoute ?? false),
'explicitChildNodes must be set to true if scopes route is true',
......
......@@ -6,7 +6,7 @@ import 'dart:ui' as ui show Color;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart';
import 'package:flutter/semantics.dart';
import 'layer.dart';
import 'object.dart';
......@@ -204,17 +204,18 @@ class RenderSliverOpacity extends RenderProxySliver {
/// child as usual. It just cannot be the target of located events, because its
/// render object returns false from [hitTest].
///
/// When [ignoringSemantics] is true, the subtree will be invisible to the
/// semantics layer (and thus e.g. accessibility tools). If [ignoringSemantics]
/// is null, it uses the value of [ignoring].
/// {@macro flutter.widgets.IgnorePointer.Semantics}
class RenderSliverIgnorePointer extends RenderProxySliver {
/// Creates a render object that is invisible to hit testing.
///
/// The [ignoring] argument must not be null. If [ignoringSemantics] is null,
/// this render object will be ignored for semantics if [ignoring] is true.
/// The [ignoring] argument must not be null.
RenderSliverIgnorePointer({
RenderSliver? sliver,
bool ignoring = true,
@Deprecated(
'Create a custom sliver ignore pointer widget instead. '
'This feature was deprecated after v3.8.0-12.0.pre.'
)
bool? ignoringSemantics,
}) : _ignoring = ignoring,
_ignoringSemantics = ignoringSemantics {
......@@ -225,6 +226,8 @@ class RenderSliverIgnorePointer extends RenderProxySliver {
///
/// Regardless of whether this render object is ignored during hit testing, it
/// will still consume space during layout and be visible during painting.
///
/// {@macro flutter.widgets.IgnorePointer.Semantics}
bool get ignoring => _ignoring;
bool _ignoring;
set ignoring(bool value) {
......@@ -232,7 +235,7 @@ class RenderSliverIgnorePointer extends RenderProxySliver {
return;
}
_ignoring = value;
if (_ignoringSemantics == null || !_ignoringSemantics!) {
if (ignoringSemantics == null) {
markNeedsSemanticsUpdate();
}
}
......@@ -240,24 +243,21 @@ class RenderSliverIgnorePointer extends RenderProxySliver {
/// Whether the semantics of this render object is ignored when compiling the
/// semantics tree.
///
/// If null, defaults to value of [ignoring].
///
/// See [SemanticsNode] for additional information about the semantics tree.
/// {@macro flutter.widgets.IgnorePointer.Semantics}
@Deprecated(
'Create a custom sliver ignore pointer widget instead. '
'This feature was deprecated after v3.8.0-12.0.pre.'
)
bool? get ignoringSemantics => _ignoringSemantics;
bool? _ignoringSemantics;
set ignoringSemantics(bool? value) {
if (value == _ignoringSemantics) {
return;
}
final bool oldEffectiveValue = _effectiveIgnoringSemantics;
_ignoringSemantics = value;
if (oldEffectiveValue != _effectiveIgnoringSemantics) {
markNeedsSemanticsUpdate();
}
markNeedsSemanticsUpdate();
}
bool get _effectiveIgnoringSemantics => ignoringSemantics ?? ignoring;
@override
bool hitTest(SliverHitTestResult result, {required double mainAxisPosition, required double crossAxisPosition}) {
return !ignoring
......@@ -270,16 +270,31 @@ class RenderSliverIgnorePointer extends RenderProxySliver {
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (child != null && !_effectiveIgnoringSemantics) {
visitor(child!);
if (_ignoringSemantics ?? false) {
return;
}
super.visitChildrenForSemantics(visitor);
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
// Do not block user interactions if _ignoringSemantics is false; otherwise,
// delegate to absorbing
config.isBlockingUserActions = ignoring && (_ignoringSemantics ?? true);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('ignoring', ignoring));
properties.add(DiagnosticsProperty<bool>('ignoringSemantics', _effectiveIgnoringSemantics, description: ignoringSemantics == null ? 'implicitly $_effectiveIgnoringSemantics' : null));
properties.add(
DiagnosticsProperty<bool>(
'ignoringSemantics',
ignoringSemantics,
description: ignoringSemantics == null ? null : 'implicitly $ignoringSemantics',
),
);
}
}
......
......@@ -68,6 +68,9 @@ typedef SemanticsUpdateCallback = void Function(ui.SemanticsUpdate update);
/// value.
typedef ChildSemanticsConfigurationsDelegate = ChildSemanticsConfigurationsResult Function(List<SemanticsConfiguration>);
final int _kUnblockedUserActions = SemanticsAction.didGainAccessibilityFocus.index
| SemanticsAction.didLoseAccessibilityFocus.index;
/// A tag for a [SemanticsNode].
///
/// Tags can be interpreted by the parent of a [SemanticsNode]
......@@ -1802,6 +1805,22 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_markDirty();
}
/// Whether the user can interact with this node in assistive technologies.
///
/// This node can still receive accessibility focus even if this is true.
/// Setting this to true prevents the user from activating pointer related
/// [SemanticsAction]s, such as [SemanticsAction.tap] or
/// [SemanticsAction.longPress].
bool get areUserActionsBlocked => _areUserActionsBlocked;
bool _areUserActionsBlocked = false;
set areUserActionsBlocked(bool value) {
if (_areUserActionsBlocked == value) {
return;
}
_areUserActionsBlocked = value;
_markDirty();
}
/// Whether this node is taking part in a merge of semantic information.
///
/// This returns true if the node is either merged into an ancestor node or if
......@@ -2062,7 +2081,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|| platformViewId != config.platformViewId
|| _maxValueLength != config._maxValueLength
|| _currentValueLength != config._currentValueLength
|| _mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants;
|| _mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants
|| _areUserActionsBlocked != config.isBlockingUserActions;
}
// TAGS, LABELS, ACTIONS
......@@ -2070,6 +2090,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
Map<SemanticsAction, SemanticsActionHandler> _actions = _kEmptyConfig._actions;
Map<CustomSemanticsAction, VoidCallback> _customSemanticsActions = _kEmptyConfig._customSemanticsActions;
int get _effectiveActionsAsBits => _areUserActionsBlocked ? _actionsAsBits & _kUnblockedUserActions : _actionsAsBits;
int _actionsAsBits = _kEmptyConfig._actionsAsBits;
/// The [SemanticsTag]s this node is tagged with.
......@@ -2415,6 +2436,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_platformViewId = config._platformViewId;
_maxValueLength = config._maxValueLength;
_currentValueLength = config._currentValueLength;
_areUserActionsBlocked = config.isBlockingUserActions;
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
assert(
......@@ -2435,6 +2457,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
/// returned data matches the data on this node.
SemanticsData getSemanticsData() {
int flags = _flags;
// Can't use _effectiveActionsAsBits here. The filtering of action bits
// must be done after the merging the its descendants.
int actions = _actionsAsBits;
AttributedString attributedLabel = _attributedLabel;
AttributedString attributedValue = _attributedValue;
......@@ -2480,7 +2504,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_visitDescendants((SemanticsNode node) {
assert(node.isMergedIntoParent);
flags |= node._flags;
actions |= node._actionsAsBits;
actions |= node._effectiveActionsAsBits;
textDirection ??= node._textDirection;
textSelection ??= node._textSelection;
scrollChildCount ??= node._scrollChildCount;
......@@ -2547,7 +2572,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
return SemanticsData(
flags: flags,
actions: actions,
actions: _areUserActionsBlocked ? actions & _kUnblockedUserActions : actions,
attributedLabel: attributedLabel,
attributedValue: attributedValue,
attributedIncreasedValue: attributedIncreasedValue,
......@@ -2721,6 +2746,15 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
SystemChannels.accessibility.send(event.toMap(nodeId: id));
}
bool _debugIsActionBlocked(SemanticsAction action) {
bool result = false;
assert((){
result = (_effectiveActionsAsBits & action.index) == 0;
return true;
}());
return result;
}
@override
String toStringShort() => '${objectRuntimeType(this, 'SemanticsNode')}#$id';
......@@ -2751,7 +2785,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
properties.add(DiagnosticsProperty<Rect>('rect', rect, description: description, showName: false));
}
properties.add(IterableProperty<String>('tags', tags?.map((SemanticsTag tag) => tag.name), defaultValue: null));
final List<String> actions = _actions.keys.map<String>((SemanticsAction action) => describeEnum(action)).toList()..sort();
final List<String> actions = _actions.keys.map<String>((SemanticsAction action) => '${describeEnum(action)}${_debugIsActionBlocked(action) ? '🚫️' : ''}').toList()..sort();
final List<String?> customSemanticsActions = _customSemanticsActions.keys
.map<String?>((CustomSemanticsAction action) => action.label)
.toList();
......@@ -3340,6 +3374,22 @@ class SemanticsConfiguration {
_isSemanticBoundary = value;
}
/// Whether to block pointer related user actions for the rendering subtree.
///
/// Setting this to true will prevent users from interacting with the
/// rendering object produces this semantics configuration and its subtree
/// through pointer-related [SemanticsAction]s in assistive technologies.
///
/// The [SemanticsNode] created from this semantics configuration is still
/// focusable by assistive technologies. Only pointer-related
/// [SemanticsAction]s, such as [SemanticsAction.tap] or its friends, are
/// blocked.
///
/// If this semantics configuration is merged into a parent semantics node,
/// only the [SemanticsAction]s from this rendering object and the rendering
/// objects in the subtree are blocked.
bool isBlockingUserActions = false;
/// Whether the configuration forces all children of the owning [RenderObject]
/// that want to contribute semantic information to the semantics tree to do
/// so in the form of explicit [SemanticsNode]s.
......@@ -3391,6 +3441,7 @@ class SemanticsConfiguration {
/// * [addAction] to add an action.
final Map<SemanticsAction, SemanticsActionHandler> _actions = <SemanticsAction, SemanticsActionHandler>{};
int get _effectiveActionsAsBits => isBlockingUserActions ? _actionsAsBits & _kUnblockedUserActions : _actionsAsBits;
int _actionsAsBits = 0;
/// Adds an `action` to the semantics tree.
......@@ -4628,10 +4679,17 @@ class SemanticsConfiguration {
if (!child.hasBeenAnnotated) {
return;
}
_actions.addAll(child._actions);
if (child.isBlockingUserActions) {
child._actions.forEach((SemanticsAction key, SemanticsActionHandler value) {
if (_kUnblockedUserActions & key.index > 0) {
_actions[key] = value;
}
});
} else {
_actions.addAll(child._actions);
}
_actionsAsBits |= child._effectiveActionsAsBits;
_customSemanticsActions.addAll(child._customSemanticsActions);
_actionsAsBits |= child._actionsAsBits;
_flags |= child._flags;
_textSelection ??= child._textSelection;
_scrollPosition ??= child._scrollPosition;
......@@ -4710,7 +4768,8 @@ class SemanticsConfiguration {
.._maxValueLength = _maxValueLength
.._currentValueLength = _currentValueLength
.._actions.addAll(_actions)
.._customSemanticsActions.addAll(_customSemanticsActions);
.._customSemanticsActions.addAll(_customSemanticsActions)
..isBlockingUserActions = isBlockingUserActions;
}
}
......
......@@ -6740,10 +6740,6 @@ class RepaintBoundary extends SingleChildRenderObjectWidget {
/// as usual. It just cannot be the target of located events, because it returns
/// false from [RenderBox.hitTest].
///
/// When [ignoringSemantics] is true, the subtree will be invisible to
/// the semantics layer (and thus e.g. accessibility tools). If
/// [ignoringSemantics] is null, it uses the value of [ignoring].
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=qV9pqHWxYgI}
///
/// {@tool dartpad}
......@@ -6756,6 +6752,24 @@ class RepaintBoundary extends SingleChildRenderObjectWidget {
/// ** See code in examples/api/lib/widgets/basic/ignore_pointer.0.dart **
/// {@end-tool}
///
/// ## Semantics
///
/// Using this widget may also affect how the semantics subtree underneath this
/// widget is collected.
///
/// {@template flutter.widgets.IgnorePointer.Semantics}
/// If [ignoringSemantics] is true, the semantics subtree is dropped. Therefore,
/// the subtree will be invisible to assistive technologies.
///
/// If [ignoringSemantics] is false, the semantics subtree is collected as
/// usual.
///
/// If [ignoringSemantics] is not set, then [ignoring] decides how the
/// semantics subtree is collected. If [ignoring] is true, pointer-related
/// [SemanticsAction]s are removed from the semantics subtree. Otherwise, the
/// subtree remains untouched.
/// {@endtemplate}
///
/// See also:
///
/// * [AbsorbPointer], which also prevents its children from receiving pointer
......@@ -6764,11 +6778,14 @@ class RepaintBoundary extends SingleChildRenderObjectWidget {
class IgnorePointer extends SingleChildRenderObjectWidget {
/// Creates a widget that is invisible to hit testing.
///
/// The [ignoring] argument must not be null. If [ignoringSemantics] is null,
/// this render object will be ignored for semantics if [ignoring] is true.
/// The [ignoring] argument must not be null.
const IgnorePointer({
super.key,
this.ignoring = true,
@Deprecated(
'Use ExcludeSemantics or create a custom ignore pointer widget instead. '
'This feature was deprecated after v3.8.0-12.0.pre.'
)
this.ignoringSemantics,
super.child,
});
......@@ -6777,13 +6794,22 @@ class IgnorePointer extends SingleChildRenderObjectWidget {
///
/// Regardless of whether this widget is ignored during hit testing, it will
/// still consume space during layout and be visible during painting.
///
/// {@macro flutter.widgets.IgnorePointer.Semantics}
///
/// Defaults to true.
final bool ignoring;
/// Whether the semantics of this widget is ignored when compiling the semantics tree.
/// Whether the semantics of this widget is ignored when compiling the
/// semantics subtree.
///
/// If null, defaults to value of [ignoring].
/// {@macro flutter.widgets.IgnorePointer.Semantics}
///
/// See [SemanticsNode] for additional information about the semantics tree.
@Deprecated(
'Use ExcludeSemantics or create a custom ignore pointer widget instead. '
'This feature was deprecated after v3.8.0-12.0.pre.'
)
final bool? ignoringSemantics;
@override
......@@ -6817,6 +6843,9 @@ class IgnorePointer extends SingleChildRenderObjectWidget {
/// from being the target of located events, because it returns true from
/// [RenderBox.hitTest].
///
/// When [ignoringSemantics] is true, the subtree will be invisible to
/// the semantics layer (and thus e.g. accessibility tools).
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=65HoWqBboI8}
///
/// {@tool dartpad}
......@@ -6827,6 +6856,23 @@ class IgnorePointer extends SingleChildRenderObjectWidget {
/// ** See code in examples/api/lib/widgets/basic/absorb_pointer.0.dart **
/// {@end-tool}
///
/// ## Semantics
///
/// Using this widget may also affect how the semantics subtree underneath this
/// widget is collected.
///
/// {@template flutter.widgets.AbsorbPointer.Semantics}
/// If [ignoringSemantics] is true, the semantics subtree is dropped.
///
/// If [ignoringSemantics] is false, the semantics subtree is collected as
/// usual.
///
/// If [ignoringSemantics] is not set, then [absorbing] decides how the
/// semantics subtree is collected. If [absorbing] is true, pointer-related
/// [SemanticsAction]s are removed from the semantics subtree. Otherwise, the
/// subtree remains untouched.
/// {@endtemplate}
///
/// See also:
///
/// * [IgnorePointer], which also prevents its children from receiving pointer
......@@ -6838,8 +6884,12 @@ class AbsorbPointer extends SingleChildRenderObjectWidget {
const AbsorbPointer({
super.key,
this.absorbing = true,
super.child,
@Deprecated(
'Use ExcludeSemantics or create a custom absorb pointer widget instead. '
'This feature was deprecated after v3.8.0-12.0.pre.'
)
this.ignoringSemantics,
super.child,
});
/// Whether this widget absorbs pointers during hit testing.
......@@ -6847,14 +6897,22 @@ class AbsorbPointer extends SingleChildRenderObjectWidget {
/// Regardless of whether this render object absorbs pointers during hit
/// testing, it will still consume space during layout and be visible during
/// painting.
///
/// {@macro flutter.widgets.AbsorbPointer.Semantics}
///
/// Defaults to true.
final bool absorbing;
/// Whether the semantics of this render object is ignored when compiling the
/// semantics tree.
///
/// If null, defaults to the value of [absorbing].
/// {@macro flutter.widgets.AbsorbPointer.Semantics}
///
/// See [SemanticsNode] for additional information about the semantics tree.
@Deprecated(
'Use ExcludeSemantics or create a custom absorb pointer widget instead. '
'This feature was deprecated after v3.8.0-12.0.pre.'
)
final bool? ignoringSemantics;
@override
......@@ -6968,6 +7026,7 @@ class Semantics extends SingleChildRenderObjectWidget {
bool container = false,
bool explicitChildNodes = false,
bool excludeSemantics = false,
bool blockUserActions = false,
bool? enabled,
bool? checked,
bool? mixed,
......@@ -7033,6 +7092,7 @@ class Semantics extends SingleChildRenderObjectWidget {
container: container,
explicitChildNodes: explicitChildNodes,
excludeSemantics: excludeSemantics,
blockUserActions: blockUserActions,
properties: SemanticsProperties(
enabled: enabled,
checked: checked,
......@@ -7108,6 +7168,7 @@ class Semantics extends SingleChildRenderObjectWidget {
this.container = false,
this.explicitChildNodes = false,
this.excludeSemantics = false,
this.blockUserActions = false,
required this.properties,
});
......@@ -7151,12 +7212,48 @@ class Semantics extends SingleChildRenderObjectWidget {
/// an [ExcludeSemantics] widget and then another [Semantics] widget.
final bool excludeSemantics;
/// Whether to block user interactions for the rendering subtree.
///
/// Setting this to true will prevent users from interacting with The
/// rendering object configured by this widget and its subtree through
/// pointer-related [SemanticsAction]s in assistive technologies.
///
/// The [SemanticsNode] created from this widget is still focusable by
/// assistive technologies. Only pointer-related [SemanticsAction]s, such as
/// [SemanticsAction.tap] or its friends, are blocked.
///
/// If this widget is merged into a parent semantics node, only the
/// [SemanticsAction]s of this widget and the widgets in the subtree are
/// blocked.
///
/// For example:
/// ```dart
/// void _myTap() { }
/// void _myLongPress() { }
///
/// Widget build(BuildContext context) {
/// return Semantics(
/// onTap: _myTap,
/// child: Semantics(
/// blockUserActions: true,
/// onLongPress: _myLongPress,
/// child: const Text('label'),
/// ),
/// );
/// }
/// ```
///
/// The result semantics node will still have `_myTap`, but the `_myLongPress`
/// will be blocked.
final bool blockUserActions;
@override
RenderSemanticsAnnotations createRenderObject(BuildContext context) {
return RenderSemanticsAnnotations(
container: container,
explicitChildNodes: explicitChildNodes,
excludeSemantics: excludeSemantics,
blockUserActions: blockUserActions,
properties: properties,
textDirection: _getTextDirection(context),
);
......@@ -7186,6 +7283,7 @@ class Semantics extends SingleChildRenderObjectWidget {
..container = container
..explicitChildNodes = explicitChildNodes
..excludeSemantics = excludeSemantics
..blockUserActions = blockUserActions
..properties = properties
..textDirection = _getTextDirection(context);
}
......
......@@ -896,10 +896,12 @@ class _DragAvatar<T extends Object> extends Drag {
return Positioned(
left: _lastOffset!.dx - overlayTopLeft.dx,
top: _lastOffset!.dy - overlayTopLeft.dy,
child: IgnorePointer(
ignoring: ignoringFeedbackPointer,
ignoringSemantics: ignoringFeedbackSemantics,
child: feedback,
child: ExcludeSemantics(
excluding: ignoringFeedbackSemantics,
child: IgnorePointer(
ignoring: ignoringFeedbackPointer,
child: feedback,
),
),
);
}
......
......@@ -884,7 +884,6 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
child: IgnorePointer(
key: _ignorePointerKey,
ignoring: _shouldIgnorePointer,
ignoringSemantics: false,
child: widget.viewportBuilder(context, position),
),
),
......
......@@ -171,8 +171,7 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> with WidgetsBindi
child: Listener(
onPointerDown: _handlePointerDown,
behavior: HitTestBehavior.opaque,
child: IgnorePointer(
ignoringSemantics: false,
child: _IgnorePointerWithSemantics(
child: widget.child,
),
),
......@@ -393,3 +392,22 @@ class _SemanticsDebuggerPainter extends CustomPainter {
canvas.restore();
}
}
/// A widget ignores pointer event but still keeps semantics actions.
class _IgnorePointerWithSemantics extends SingleChildRenderObjectWidget {
const _IgnorePointerWithSemantics({
super.child,
});
@override
_RenderIgnorePointerWithSemantics createRenderObject(BuildContext context) {
return _RenderIgnorePointerWithSemantics();
}
}
class _RenderIgnorePointerWithSemantics extends RenderProxyBox {
_RenderIgnorePointerWithSemantics();
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) => false;
}
......@@ -1178,9 +1178,7 @@ class SliverOpacity extends SingleChildRenderObjectWidget {
/// child as usual. It just cannot be the target of located events, because it
/// returns false from [RenderSliver.hitTest].
///
/// When [ignoringSemantics] is true, the subtree will be invisible to
/// the semantics layer (and thus e.g. accessibility tools). If
/// [ignoringSemantics] is null, it uses the value of [ignoring].
/// {@macro flutter.widgets.IgnorePointer.Semantics}
///
/// See also:
///
......@@ -1188,11 +1186,14 @@ class SliverOpacity extends SingleChildRenderObjectWidget {
class SliverIgnorePointer extends SingleChildRenderObjectWidget {
/// Creates a sliver widget that is invisible to hit testing.
///
/// The [ignoring] argument must not be null. If [ignoringSemantics] is null,
/// this render object will be ignored for semantics if [ignoring] is true.
/// The [ignoring] argument must not be null.
const SliverIgnorePointer({
super.key,
this.ignoring = true,
@Deprecated(
'Create a custom sliver ignore pointer widget instead. '
'This feature was deprecated after v3.8.0-12.0.pre.'
)
this.ignoringSemantics,
Widget? sliver,
}) : super(child: sliver);
......@@ -1201,14 +1202,18 @@ class SliverIgnorePointer extends SingleChildRenderObjectWidget {
///
/// Regardless of whether this sliver is ignored during hit testing, it will
/// still consume space during layout and be visible during painting.
///
/// {@macro flutter.widgets.IgnorePointer.Semantics}
final bool ignoring;
/// Whether the semantics of this sliver is ignored when compiling the
/// semantics tree.
///
/// If null, defaults to value of [ignoring].
///
/// See [SemanticsNode] for additional information about the semantics tree.
/// {@macro flutter.widgets.IgnorePointer.Semantics}
@Deprecated(
'Create a custom sliver ignore pointer widget instead. '
'This feature was deprecated after v3.8.0-12.0.pre.'
)
final bool? ignoringSemantics;
@override
......
......@@ -204,10 +204,6 @@ class Visibility extends StatelessWidget {
/// visible to accessibility tools when it is hidden from the user. If this
/// flag is set to true, then accessibility tools will report the widget as if
/// it was present.
///
/// Dynamically changing this value may cause the current state of the
/// subtree to be lost (and a new instance of the subtree, with new [State]
/// objects, to be immediately created if [visible] is true).
final bool maintainSemantics;
/// Whether to allow the widget to be interactive when hidden.
......@@ -217,10 +213,6 @@ class Visibility extends StatelessWidget {
/// By default, with [maintainInteractivity] set to false, touch events cannot
/// reach the [child] when it is hidden from the user. If this flag is set to
/// true, then touch events will nonetheless be passed through.
///
/// Dynamically changing this value may cause the current state of the
/// subtree to be lost (and a new instance of the subtree, with new [State]
/// objects, to be immediately created if [visible] is true).
final bool maintainInteractivity;
/// Tells the visibility state of an element in the tree based off its
......@@ -254,17 +246,13 @@ class Visibility extends StatelessWidget {
Widget build(BuildContext context) {
Widget result = child;
if (maintainSize) {
if (!maintainInteractivity) {
result = IgnorePointer(
ignoring: !visible,
ignoringSemantics: !visible && !maintainSemantics,
child: child,
);
}
result = _Visibility(
visible: visible,
maintainSemantics: maintainSemantics,
child: result,
child: IgnorePointer(
ignoring: !visible && !maintainInteractivity,
child: result,
),
);
} else {
assert(!maintainInteractivity);
......@@ -272,7 +260,7 @@ class Visibility extends StatelessWidget {
assert(!maintainSize);
if (maintainState) {
if (!maintainAnimation) {
result = TickerMode(enabled: visible, child: child);
result = TickerMode(enabled: visible, child: result);
}
result = Offstage(
offstage: !visible,
......@@ -498,10 +486,6 @@ class SliverVisibility extends StatelessWidget {
/// visible to accessibility tools when it is hidden from the user. If this
/// flag is set to true, then accessibility tools will report the widget as if
/// it was present.
///
/// Dynamically changing this value may cause the current state of the
/// subtree to be lost (and a new instance of the subtree, with new [State]
/// objects, to be immediately created if [visible] is true).
final bool maintainSemantics;
/// Whether to allow the sliver to be interactive when hidden.
......@@ -511,23 +495,16 @@ class SliverVisibility extends StatelessWidget {
/// By default, with [maintainInteractivity] set to false, touch events cannot
/// reach the [sliver] when it is hidden from the user. If this flag is set to
/// true, then touch events will nonetheless be passed through.
///
/// Dynamically changing this value may cause the current state of the
/// subtree to be lost (and a new instance of the subtree, with new [State]
/// objects, to be immediately created if [visible] is true).
final bool maintainInteractivity;
@override
Widget build(BuildContext context) {
if (maintainSize) {
Widget result = sliver;
if (!maintainInteractivity) {
result = SliverIgnorePointer(
sliver: sliver,
ignoring: !visible,
ignoringSemantics: !visible && !maintainSemantics,
);
}
result = SliverIgnorePointer(
ignoring: !visible && !maintainInteractivity,
sliver: result,
);
return _SliverVisibility(
visible: visible,
maintainSemantics: maintainSemantics,
......@@ -571,19 +548,19 @@ class SliverVisibility extends StatelessWidget {
// different layers. This can be significantly more expensive, so the issue is avoided by a
// specialized render object that does not ever force compositing.
class _Visibility extends SingleChildRenderObjectWidget {
const _Visibility({ required this.visible, required this.maintainSemantics, super.child });
const _Visibility({ required this.visible, required this.maintainSemantics, super.child });
final bool visible;
final bool maintainSemantics;
@override
RenderObject createRenderObject(BuildContext context) {
_RenderVisibility createRenderObject(BuildContext context) {
return _RenderVisibility(visible, maintainSemantics);
}
@override
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) {
(renderObject as _RenderVisibility)
void updateRenderObject(BuildContext context, _RenderVisibility renderObject) {
renderObject
..visible = visible
..maintainSemantics = maintainSemantics;
}
......@@ -647,8 +624,8 @@ class _SliverVisibility extends SingleChildRenderObjectWidget {
}
@override
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) {
(renderObject as _RenderSliverVisibility)
void updateRenderObject(BuildContext context, _RenderSliverVisibility renderObject) {
renderObject
..visible = visible
..maintainSemantics = maintainSemantics;
}
......
......@@ -2817,7 +2817,6 @@ class _WidgetInspectorState extends State<WidgetInspector>
child: IgnorePointer(
ignoring: isSelectMode,
key: _ignorePointerKey,
ignoringSemantics: false,
child: widget.child,
),
),
......
......@@ -7444,6 +7444,8 @@ void main() {
),
matchesSemantics(
hasEnabledState: true,
isTextField: true,
isReadOnly: true,
),
);
});
......
......@@ -6248,7 +6248,6 @@ void main() {
testWidgets('Disabled text field does not have tap action', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
const MaterialApp(
home: Material(
......@@ -6263,7 +6262,27 @@ void main() {
);
expect(semantics, isNot(includesNodeWith(actions: <SemanticsAction>[SemanticsAction.tap])));
semantics.dispose();
});
testWidgets('Disabled text field semantics node still contains value', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: TextEditingController(text: 'text'),
maxLength: 10,
enabled: false,
),
),
),
),
);
expect(semantics, includesNodeWith(actions: <SemanticsAction>[], value: 'text'));
semantics.dispose();
});
......
......@@ -631,6 +631,30 @@ void main() {
);
});
test('blocked actions debug properties', () {
final SemanticsConfiguration config = SemanticsConfiguration()
..isBlockingUserActions = true
..onScrollUp = () { }
..onLongPress = () { }
..onShowOnScreen = () { }
..onDidGainAccessibilityFocus = () { };
final SemanticsNode blocked = SemanticsNode()
..rect = const Rect.fromLTWH(50.0, 10.0, 20.0, 30.0)
..transform = Matrix4.translation(Vector3(10.0, 10.0, 0.0))
..updateWith(config: config);
expect(
blocked.toStringDeep(),
equalsIgnoringHashCodes(
'SemanticsNode#1\n'
' STALE\n'
' owner: null\n'
' Rect.fromLTRB(60.0, 20.0, 80.0, 50.0)\n'
' actions: didGainAccessibilityFocus, longPress🚫️, scrollUp🚫️,\n'
' showOnScreen🚫️\n',
),
);
});
test('Custom actions debug properties', () {
final SemanticsConfiguration configuration = SemanticsConfiguration();
const CustomSemanticsAction action1 = CustomSemanticsAction(label: 'action1');
......
......@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart';
......@@ -28,41 +28,77 @@ void main() {
expect(tapped, true);
});
testWidgets('AbsorbPointers semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
AbsorbPointer(
child: Semantics(
label: 'test',
textDirection: TextDirection.ltr,
group('AbsorbPointer semantics', () {
testWidgets('does not change semantics when not absorbing', (WidgetTester tester) async {
final UniqueKey key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: AbsorbPointer(
absorbing: false,
child: ElevatedButton(
key: key,
onPressed: () { },
child: const Text('button'),
),
),
),
),
);
expect(semantics, hasSemantics(TestSemantics.root(), ignoreId: true, ignoreRect: true, ignoreTransform: true));
);
expect(
tester.getSemantics(find.byKey(key)),
matchesSemantics(
label: 'button',
hasTapAction: true,
isButton: true,
isFocusable: true,
hasEnabledState: true,
isEnabled: true,
),
);
});
await tester.pumpWidget(
AbsorbPointer(
absorbing: false,
child: Semantics(
label: 'test',
textDirection: TextDirection.ltr,
testWidgets('drops semantics when its ignoreSemantics is true', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final UniqueKey key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: AbsorbPointer(
ignoringSemantics: true,
child: ElevatedButton(
key: key,
onPressed: () { },
child: const Text('button'),
),
),
),
),
);
);
expect(semantics, isNot(includesNodeWith(label: 'button')));
semantics.dispose();
});
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
label: 'test',
textDirection: TextDirection.ltr,
testWidgets('ignores user interactions', (WidgetTester tester) async {
final UniqueKey key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: AbsorbPointer(
child: ElevatedButton(
key: key,
onPressed: () { },
child: const Text('button'),
),
),
],
),
ignoreId: true,
ignoreRect: true,
ignoreTransform: true,
));
semantics.dispose();
),
);
expect(
tester.getSemantics(find.byKey(key)),
// Tap action is blocked.
matchesSemantics(
label: 'button',
isButton: true,
isFocusable: true,
hasEnabledState: true,
isEnabled: true,
),
);
});
});
}
......@@ -16,6 +16,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart';
void main() {
group('RawImage', () {
testWidgets('properties', (WidgetTester tester) async {
......@@ -831,6 +833,199 @@ void main() {
logs.clear();
});
group('IgnorePointer semantics', () {
testWidgets('does not change semantics when not ignoring', (WidgetTester tester) async {
final UniqueKey key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: IgnorePointer(
ignoring: false,
child: ElevatedButton(
key: key,
onPressed: () { },
child: const Text('button'),
),
),
),
);
expect(
tester.getSemantics(find.byKey(key)),
matchesSemantics(
label: 'button',
hasTapAction: true,
isButton: true,
isFocusable: true,
hasEnabledState: true,
isEnabled: true,
),
);
});
testWidgets('can toggle the ignoring.', (WidgetTester tester) async {
final UniqueKey key1 = UniqueKey();
final UniqueKey key2 = UniqueKey();
final UniqueKey key3 = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: TestIgnorePointer(
child: Semantics(
key: key1,
label: '1',
onTap: (){ },
container: true,
child: Semantics(
key: key2,
label: '2',
onTap: (){ },
container: true,
child: Semantics(
key: key3,
label: '3',
onTap: (){ },
container: true,
child: const SizedBox(width: 10, height: 10),
),
),
),
),
),
);
expect(
tester.getSemantics(find.byKey(key1)),
matchesSemantics(
label: '1',
),
);
expect(
tester.getSemantics(find.byKey(key2)),
matchesSemantics(
label: '2',
),
);
expect(
tester.getSemantics(find.byKey(key3)),
matchesSemantics(
label: '3',
),
);
final TestIgnorePointerState state = tester.state<TestIgnorePointerState>(find.byType(TestIgnorePointer));
state.setIgnore(false);
await tester.pump();
expect(
tester.getSemantics(find.byKey(key1)),
matchesSemantics(
label: '1',
hasTapAction: true,
),
);
expect(
tester.getSemantics(find.byKey(key2)),
matchesSemantics(
label: '2',
hasTapAction: true,
),
);
expect(
tester.getSemantics(find.byKey(key3)),
matchesSemantics(
label: '3',
hasTapAction: true,
),
);
state.setIgnore(true);
await tester.pump();
expect(
tester.getSemantics(find.byKey(key1)),
matchesSemantics(
label: '1',
),
);
expect(
tester.getSemantics(find.byKey(key2)),
matchesSemantics(
label: '2',
),
);
expect(
tester.getSemantics(find.byKey(key3)),
matchesSemantics(
label: '3',
),
);
state.setIgnore(false);
await tester.pump();
expect(
tester.getSemantics(find.byKey(key1)),
matchesSemantics(
label: '1',
hasTapAction: true,
),
);
expect(
tester.getSemantics(find.byKey(key2)),
matchesSemantics(
label: '2',
hasTapAction: true,
),
);
expect(
tester.getSemantics(find.byKey(key3)),
matchesSemantics(
label: '3',
hasTapAction: true,
),
);
});
testWidgets('drops semantics when its ignoringSemantics is true', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final UniqueKey key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: IgnorePointer(
ignoringSemantics: true,
child: ElevatedButton(
key: key,
onPressed: () { },
child: const Text('button'),
),
),
),
);
expect(semantics, isNot(includesNodeWith(label: 'button')));
semantics.dispose();
});
testWidgets('ignores user interactions', (WidgetTester tester) async {
final UniqueKey key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: IgnorePointer(
child: ElevatedButton(
key: key,
onPressed: () { },
child: const Text('button'),
),
),
),
);
expect(
tester.getSemantics(find.byKey(key)),
// Tap action is blocked.
matchesSemantics(
label: 'button',
isButton: true,
isFocusable: true,
hasEnabledState: true,
isEnabled: true,
),
);
});
});
testWidgets('AbsorbPointer absorbs pointers', (WidgetTester tester) async {
final List<String> logs = <String>[];
Widget target({required bool absorbing}) => Align(
......@@ -1005,3 +1200,30 @@ class _MockCanvas extends Fake implements Canvas {
paints.add(paint);
}
}
class TestIgnorePointer extends StatefulWidget {
const TestIgnorePointer({super.key, required this.child});
final Widget child;
@override
State<StatefulWidget> createState() => TestIgnorePointerState();
}
class TestIgnorePointerState extends State<TestIgnorePointer> {
bool ignore = true;
void setIgnore(bool newIgnore) {
setState(() {
ignore = newIgnore;
});
}
@override
Widget build(BuildContext context) {
return IgnorePointer(
ignoring: ignore,
child: widget.child,
);
}
}
......@@ -286,7 +286,7 @@ void main() {
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n'
' │ ignoring: false\n'
' │ ignoringSemantics: false\n'
' │ ignoringSemantics: null\n'
' │\n'
' └─child: RenderViewport#00000\n'
' │ needs compositing\n'
......@@ -460,7 +460,7 @@ void main() {
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n'
' │ ignoring: false\n'
' │ ignoringSemantics: false\n'
' │ ignoringSemantics: null\n'
' │\n'
' └─child: RenderViewport#00000\n'
' │ needs compositing\n'
......
......@@ -53,7 +53,7 @@ void main() {
),
SizedBox(
height: 10.0,
child: IgnorePointer(
child: ExcludeSemantics(
child: Semantics(
label: 'child1',
textDirection: TextDirection.ltr,
......@@ -94,8 +94,8 @@ void main() {
),
SizedBox(
height: 10.0,
child: IgnorePointer(
ignoring: false,
child: ExcludeSemantics(
excluding: false,
child: Semantics(
label: 'child2',
textDirection: TextDirection.ltr,
......@@ -148,7 +148,7 @@ void main() {
),
SizedBox(
height: 10.0,
child: IgnorePointer(
child: ExcludeSemantics(
child: Semantics(
label: 'child2',
textDirection: TextDirection.ltr,
......@@ -189,8 +189,8 @@ void main() {
),
SizedBox(
height: 10.0,
child: IgnorePointer(
ignoring: false,
child: ExcludeSemantics(
excluding: false,
child: Semantics(
label: 'child2',
textDirection: TextDirection.ltr,
......
......@@ -33,8 +33,8 @@ void main() {
),
SizedBox(
height: 10.0,
child: IgnorePointer(
ignoring: false,
child: ExcludeSemantics(
excluding: false,
child: Semantics(
label: 'child2',
textDirection: TextDirection.ltr,
......@@ -87,7 +87,7 @@ void main() {
),
SizedBox(
height: 10.0,
child: IgnorePointer(
child: ExcludeSemantics(
child: Semantics(
label: 'child2',
textDirection: TextDirection.ltr,
......@@ -128,8 +128,8 @@ void main() {
),
SizedBox(
height: 10.0,
child: IgnorePointer(
ignoring: false,
child: ExcludeSemantics(
excluding: false,
child: Semantics(
label: 'child2',
textDirection: TextDirection.ltr,
......
......@@ -6,7 +6,6 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart';
......@@ -1693,6 +1692,118 @@ void main() {
expect(node.transform, null); // Make sure the zero transform didn't end up on the root somehow.
expect(node.childrenCount, 0);
});
testWidgets('blocking user interaction works on explicit child node.', (WidgetTester tester) async {
final UniqueKey key1 = UniqueKey();
final UniqueKey key2 = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Semantics(
blockUserActions: true,
explicitChildNodes: true,
child: Column(
children: <Widget>[
Semantics(
key: key1,
label: 'label1',
onTap: () {},
child: const SizedBox(width: 10, height: 10),
),
Semantics(
key: key2,
label: 'label2',
onTap: () {},
child: const SizedBox(width: 10, height: 10),
),
],
),
),
),
);
expect(
tester.getSemantics(find.byKey(key1)),
// Tap action is blocked.
matchesSemantics(
label: 'label1',
),
);
expect(
tester.getSemantics(find.byKey(key2)),
// Tap action is blocked.
matchesSemantics(
label: 'label2',
),
);
});
testWidgets('blocking user interaction on a merged child', (WidgetTester tester) async {
final UniqueKey key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Semantics(
key: key,
container: true,
child: Column(
children: <Widget>[
Semantics(
blockUserActions: true,
label: 'label1',
onTap: () { },
child: const SizedBox(width: 10, height: 10),
),
Semantics(
label: 'label2',
onLongPress: () { },
child: const SizedBox(width: 10, height: 10),
),
],
),
),
),
);
expect(
tester.getSemantics(find.byKey(key)),
// Tap action in label1 is blocked,
matchesSemantics(
label: 'label1\nlabel2',
hasLongPressAction: true,
),
);
});
testWidgets('does not merge conflicting actions even if one of them is blocked', (WidgetTester tester) async {
final UniqueKey key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Semantics(
key: key,
container: true,
child: Column(
children: <Widget>[
Semantics(
blockUserActions: true,
label: 'label1',
onTap: () { },
child: const SizedBox(width: 10, height: 10),
),
Semantics(
label: 'label2',
onTap: () { },
child: const SizedBox(width: 10, height: 10),
),
],
),
),
),
);
final SemanticsNode node = tester.getSemantics(find.byKey(key));
expect(
node,
matchesSemantics(
children: <Matcher>[containsSemantics(label: 'label1'), containsSemantics(label: 'label2')],
),
);
});
}
class CustomSortKey extends OrdinalSortKey {
......
......@@ -256,9 +256,9 @@ void main() {
expect(renderSliver.geometry!.scrollExtent, 14.0);
expect(renderSliver.constraints.crossAxisExtent, 800.0);
expect(semantics.nodesWith(label: 'a true'), hasLength(1));
expect(log, <String>['created new state']);
expect(log, <String>[]);
await tester.tap(find.byKey(anchor), warnIfMissed: false);
expect(log, <String>['created new state']);
expect(log, <String>[]);
log.clear();
// visible: false, maintain state, animation, size.
......
......@@ -869,6 +869,22 @@ void main() {
semantics.dispose();
});
testWidgets('ignoring only block semantics actions', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(boilerPlate(
SliverIgnorePointer(
sliver: SliverToBoxAdapter(
child: GestureDetector(
child: const Text('a'),
onTap: () { },
),
),
),
));
expect(semantics, includesNodeWith(label: 'a', actions: <SemanticsAction>[]));
semantics.dispose();
});
testWidgets('ignores pointer events & semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final List<String> events = <String>[];
......
......@@ -62,6 +62,20 @@ void main() {
ignoreTransform: true,
);
final Matcher expectedSemanticsWhenPresentWithIgnorePointer = hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
label: 'a true',
textDirection: TextDirection.rtl,
),
],
),
ignoreId: true,
ignoreRect: true,
ignoreTransform: true,
);
final Matcher expectedSemanticsWhenAbsent = hasSemantics(TestSemantics.root());
// We now run a sequence of pumpWidget calls one after the other. In
......@@ -218,10 +232,10 @@ void main() {
expect(find.byType(Placeholder), findsNothing);
expect(find.byType(Visibility), paintsNothing);
expect(tester.getSize(find.byType(Visibility)), const Size(84.0, 14.0));
expect(semantics, expectedSemanticsWhenPresent);
expect(log, <String>['created new state']);
expect(semantics, expectedSemanticsWhenPresentWithIgnorePointer);
expect(log, <String>[]);
await tester.tap(find.byType(Visibility), warnIfMissed: false);
expect(log, <String>['created new state']);
expect(log, <String>[]);
log.clear();
await tester.pumpWidget(Center(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment