mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Issues/111931 reland (#121576)
* Revert "Revert "Fix text.rich to merge widget span (#113461)" (#121562)" This reverts commit c8d80163b03f76e0cfde15567d065b6d1903cf8d. * Fixes tag to not pollute cached semantics configuration in rendering object
This commit is contained in:
parent
e340d8628e
commit
978d9b5514
@ -15,6 +15,7 @@ import 'package:flutter/semantics.dart';
|
||||
|
||||
import 'debug.dart';
|
||||
import 'layer.dart';
|
||||
import 'proxy_box.dart';
|
||||
|
||||
export 'package:flutter/foundation.dart' show
|
||||
DiagnosticPropertiesBuilder,
|
||||
@ -3227,7 +3228,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
||||
final SemanticsConfiguration config = _semanticsConfiguration;
|
||||
bool dropSemanticsOfPreviousSiblings = config.isBlockingSemanticsOfPreviouslyPaintedNodes;
|
||||
|
||||
final bool producesForkingFragment = !config.hasBeenAnnotated && !config.isSemanticBoundary;
|
||||
bool producesForkingFragment = !config.hasBeenAnnotated && !config.isSemanticBoundary;
|
||||
final bool childrenMergeIntoParent = mergeIntoParent || config.isMergingSemanticsOfDescendants;
|
||||
final List<SemanticsConfiguration> childConfigurations = <SemanticsConfiguration>[];
|
||||
final bool explicitChildNode = config.explicitChildNodes || parent is! RenderObject;
|
||||
@ -3235,6 +3236,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
||||
final Map<SemanticsConfiguration, _InterestingSemanticsFragment> configToFragment = <SemanticsConfiguration, _InterestingSemanticsFragment>{};
|
||||
final List<_InterestingSemanticsFragment> mergeUpFragments = <_InterestingSemanticsFragment>[];
|
||||
final List<List<_InterestingSemanticsFragment>> siblingMergeFragmentGroups = <List<_InterestingSemanticsFragment>>[];
|
||||
final bool hasTags = config.tagsForChildren?.isNotEmpty ?? false;
|
||||
visitChildrenForSemantics((RenderObject renderChild) {
|
||||
assert(!_needsLayout);
|
||||
final _SemanticsFragment parentFragment = renderChild._getSemanticsForParent(
|
||||
@ -3250,7 +3252,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
||||
}
|
||||
for (final _InterestingSemanticsFragment fragment in parentFragment.mergeUpFragments) {
|
||||
fragment.addAncestor(this);
|
||||
fragment.addTags(config.tagsForChildren);
|
||||
if (hasTags) {
|
||||
fragment.addTags(config.tagsForChildren!);
|
||||
}
|
||||
if (hasChildConfigurationsDelegate && fragment.config != null) {
|
||||
// This fragment need to go through delegate to determine whether it
|
||||
// merge up or not.
|
||||
@ -3266,7 +3270,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
||||
for (final List<_InterestingSemanticsFragment> siblingMergeGroup in parentFragment.siblingMergeGroups) {
|
||||
for (final _InterestingSemanticsFragment siblingMergingFragment in siblingMergeGroup) {
|
||||
siblingMergingFragment.addAncestor(this);
|
||||
siblingMergingFragment.addTags(config.tagsForChildren);
|
||||
if (hasTags) {
|
||||
siblingMergingFragment.addTags(config.tagsForChildren!);
|
||||
}
|
||||
}
|
||||
siblingMergeFragmentGroups.add(siblingMergeGroup);
|
||||
}
|
||||
@ -3279,14 +3285,25 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
||||
for (final _InterestingSemanticsFragment fragment in mergeUpFragments) {
|
||||
fragment.markAsExplicit();
|
||||
}
|
||||
} else if (hasChildConfigurationsDelegate && childConfigurations.isNotEmpty) {
|
||||
} else if (hasChildConfigurationsDelegate) {
|
||||
final ChildSemanticsConfigurationsResult result = config.childConfigurationsDelegate!(childConfigurations);
|
||||
mergeUpFragments.addAll(
|
||||
result.mergeUp.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) => configToFragment[config]!),
|
||||
result.mergeUp.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) {
|
||||
final _InterestingSemanticsFragment? fragment = configToFragment[config];
|
||||
if (fragment == null) {
|
||||
// Parent fragment of Incomplete fragments can't be a forking
|
||||
// fragment since they need to be merged.
|
||||
producesForkingFragment = false;
|
||||
return _IncompleteSemanticsFragment(config: config, owner: this);
|
||||
}
|
||||
return fragment;
|
||||
}),
|
||||
);
|
||||
for (final Iterable<SemanticsConfiguration> group in result.siblingMergeGroups) {
|
||||
siblingMergeFragmentGroups.add(
|
||||
group.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) => configToFragment[config]!).toList()
|
||||
group.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) {
|
||||
return configToFragment[config] ?? _IncompleteSemanticsFragment(config: config, owner: this);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -4167,10 +4184,10 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
|
||||
Set<SemanticsTag>? _tagsForChildren;
|
||||
|
||||
/// Tag all children produced by [compileChildren] with `tags`.
|
||||
void addTags(Iterable<SemanticsTag>? tags) {
|
||||
if (tags == null || tags.isEmpty) {
|
||||
return;
|
||||
}
|
||||
///
|
||||
/// `tags` must not be empty.
|
||||
void addTags(Iterable<SemanticsTag> tags) {
|
||||
assert(tags.isNotEmpty);
|
||||
_tagsForChildren ??= <SemanticsTag>{};
|
||||
_tagsForChildren!.addAll(tags);
|
||||
}
|
||||
@ -4264,6 +4281,48 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment {
|
||||
}
|
||||
}
|
||||
|
||||
/// A fragment with partial information that must not form an explicit
|
||||
/// semantics node without merging into another _SwitchableSemanticsFragment.
|
||||
///
|
||||
/// This fragment is generated from synthetic SemanticsConfiguration returned from
|
||||
/// [SemanticsConfiguration.childConfigurationsDelegate].
|
||||
class _IncompleteSemanticsFragment extends _InterestingSemanticsFragment {
|
||||
_IncompleteSemanticsFragment({
|
||||
required this.config,
|
||||
required super.owner,
|
||||
}) : super(dropsSemanticsOfPreviousSiblings: false);
|
||||
|
||||
@override
|
||||
void addAll(Iterable<_InterestingSemanticsFragment> fragments) {
|
||||
assert(false, 'This fragment must be a leaf node');
|
||||
}
|
||||
|
||||
@override
|
||||
void compileChildren({
|
||||
required Rect? parentSemanticsClipRect,
|
||||
required Rect? parentPaintClipRect,
|
||||
required double elevationAdjustment,
|
||||
required List<SemanticsNode> result,
|
||||
required List<SemanticsNode> siblingNodes,
|
||||
}) {
|
||||
// There is nothing to do because this fragment must be a leaf node and
|
||||
// must not be explicit.
|
||||
}
|
||||
|
||||
@override
|
||||
final SemanticsConfiguration config;
|
||||
|
||||
@override
|
||||
void markAsExplicit() {
|
||||
assert(
|
||||
false,
|
||||
'SemanticsConfiguration created in '
|
||||
'SemanticsConfiguration.childConfigurationsDelegate must not produce '
|
||||
'its own semantics node'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An [_InterestingSemanticsFragment] that can be told to only add explicit
|
||||
/// [SemanticsNode]s to the parent.
|
||||
///
|
||||
@ -4542,6 +4601,17 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void addTags(Iterable<SemanticsTag> tags) {
|
||||
super.addTags(tags);
|
||||
// _ContainerSemanticsFragments add their tags to child fragments through
|
||||
// this method. This fragment must make sure its _config is in sync.
|
||||
if (tags.isNotEmpty) {
|
||||
_ensureConfigIsWritable();
|
||||
tags.forEach(_config.addTagForChildren);
|
||||
}
|
||||
}
|
||||
|
||||
void _ensureConfigIsWritable() {
|
||||
if (!_isConfigWritable) {
|
||||
_config = _config.copy();
|
||||
|
||||
@ -119,7 +119,9 @@ class RenderParagraph extends RenderBox
|
||||
|
||||
static final String _placeholderCharacter = String.fromCharCode(PlaceholderSpan.placeholderCodeUnit);
|
||||
final TextPainter _textPainter;
|
||||
AttributedString? _cachedAttributedLabel;
|
||||
|
||||
List<AttributedString>? _cachedAttributedLabels;
|
||||
|
||||
List<InlineSpanSemanticsInformation>? _cachedCombinedSemanticsInfos;
|
||||
|
||||
/// The text to display.
|
||||
@ -135,7 +137,7 @@ class RenderParagraph extends RenderBox
|
||||
break;
|
||||
case RenderComparison.paint:
|
||||
_textPainter.text = value;
|
||||
_cachedAttributedLabel = null;
|
||||
_cachedAttributedLabels = null;
|
||||
_cachedCombinedSemanticsInfos = null;
|
||||
_extractPlaceholderSpans(value);
|
||||
markNeedsPaint();
|
||||
@ -144,7 +146,7 @@ class RenderParagraph extends RenderBox
|
||||
case RenderComparison.layout:
|
||||
_textPainter.text = value;
|
||||
_overflowShader = null;
|
||||
_cachedAttributedLabel = null;
|
||||
_cachedAttributedLabels = null;
|
||||
_cachedCombinedSemanticsInfos = null;
|
||||
_extractPlaceholderSpans(value);
|
||||
markNeedsLayout();
|
||||
@ -1035,12 +1037,23 @@ class RenderParagraph extends RenderBox
|
||||
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
||||
super.describeSemanticsConfiguration(config);
|
||||
_semanticsInfo = text.getSemanticsInformation();
|
||||
bool needsAssembleSemanticsNode = false;
|
||||
bool needsChildConfigrationsDelegate = false;
|
||||
for (final InlineSpanSemanticsInformation info in _semanticsInfo!) {
|
||||
if (info.recognizer != null) {
|
||||
needsAssembleSemanticsNode = true;
|
||||
break;
|
||||
}
|
||||
needsChildConfigrationsDelegate = needsChildConfigrationsDelegate || info.isPlaceholder;
|
||||
}
|
||||
|
||||
if (_semanticsInfo!.any((InlineSpanSemanticsInformation info) => info.recognizer != null)) {
|
||||
if (needsAssembleSemanticsNode) {
|
||||
config.explicitChildNodes = true;
|
||||
config.isSemanticBoundary = true;
|
||||
} else if (needsChildConfigrationsDelegate) {
|
||||
config.childConfigurationsDelegate = _childSemanticsConfigurationsDelegate;
|
||||
} else {
|
||||
if (_cachedAttributedLabel == null) {
|
||||
if (_cachedAttributedLabels == null) {
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
int offset = 0;
|
||||
final List<StringAttribute> attributes = <StringAttribute>[];
|
||||
@ -1050,21 +1063,77 @@ class RenderParagraph extends RenderBox
|
||||
final TextRange originalRange = infoAttribute.range;
|
||||
attributes.add(
|
||||
infoAttribute.copy(
|
||||
range: TextRange(start: offset + originalRange.start,
|
||||
end: offset + originalRange.end)
|
||||
range: TextRange(
|
||||
start: offset + originalRange.start,
|
||||
end: offset + originalRange.end,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
buffer.write(label);
|
||||
offset += label.length;
|
||||
}
|
||||
_cachedAttributedLabel = AttributedString(buffer.toString(), attributes: attributes);
|
||||
_cachedAttributedLabels = <AttributedString>[AttributedString(buffer.toString(), attributes: attributes)];
|
||||
}
|
||||
config.attributedLabel = _cachedAttributedLabel!;
|
||||
config.attributedLabel = _cachedAttributedLabels![0];
|
||||
config.textDirection = textDirection;
|
||||
}
|
||||
}
|
||||
|
||||
ChildSemanticsConfigurationsResult _childSemanticsConfigurationsDelegate(List<SemanticsConfiguration> childConfigs) {
|
||||
final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
|
||||
int placeholderIndex = 0;
|
||||
int childConfigsIndex = 0;
|
||||
int attributedLabelCacheIndex = 0;
|
||||
InlineSpanSemanticsInformation? seenTextInfo;
|
||||
_cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!);
|
||||
for (final InlineSpanSemanticsInformation info in _cachedCombinedSemanticsInfos!) {
|
||||
if (info.isPlaceholder) {
|
||||
if (seenTextInfo != null) {
|
||||
builder.markAsMergeUp(_createSemanticsConfigForTextInfo(seenTextInfo, attributedLabelCacheIndex));
|
||||
attributedLabelCacheIndex += 1;
|
||||
}
|
||||
// Mark every childConfig belongs to this placeholder to merge up group.
|
||||
while (childConfigsIndex < childConfigs.length &&
|
||||
childConfigs[childConfigsIndex].tagsChildrenWith(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) {
|
||||
builder.markAsMergeUp(childConfigs[childConfigsIndex]);
|
||||
childConfigsIndex += 1;
|
||||
}
|
||||
placeholderIndex += 1;
|
||||
} else {
|
||||
seenTextInfo = info;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle plain text info at the end.
|
||||
if (seenTextInfo != null) {
|
||||
builder.markAsMergeUp(_createSemanticsConfigForTextInfo(seenTextInfo, attributedLabelCacheIndex));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
SemanticsConfiguration _createSemanticsConfigForTextInfo(InlineSpanSemanticsInformation textInfo, int cacheIndex) {
|
||||
assert(!textInfo.requiresOwnNode);
|
||||
final List<AttributedString> cachedStrings = _cachedAttributedLabels ??= <AttributedString>[];
|
||||
assert(cacheIndex <= cachedStrings.length);
|
||||
final bool hasCache = cacheIndex < cachedStrings.length;
|
||||
|
||||
late AttributedString attributedLabel;
|
||||
if (hasCache) {
|
||||
attributedLabel = cachedStrings[cacheIndex];
|
||||
} else {
|
||||
assert(cachedStrings.length == cacheIndex);
|
||||
attributedLabel = AttributedString(
|
||||
textInfo.semanticsLabel ?? textInfo.text,
|
||||
attributes: textInfo.stringAttributes,
|
||||
);
|
||||
cachedStrings.add(attributedLabel);
|
||||
}
|
||||
return SemanticsConfiguration()
|
||||
..textDirection = textDirection
|
||||
..attributedLabel = attributedLabel;
|
||||
}
|
||||
|
||||
// Caches [SemanticsNode]s created during [assembleSemanticsNode] so they
|
||||
// can be re-used when [assembleSemanticsNode] is called again. This ensures
|
||||
// stable ids for the [SemanticsNode]s of [TextSpan]s across
|
||||
|
||||
@ -346,7 +346,7 @@ class AttributedString {
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(string, attributes,);
|
||||
int get hashCode => Object.hash(string, attributes);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@ -3805,7 +3805,8 @@ class SemanticsConfiguration {
|
||||
/// which of them should be merged upwards into the parent SemanticsNode.
|
||||
///
|
||||
/// The input list of [SemanticsConfiguration]s can be empty if the rendering
|
||||
/// object of this semantics configuration is a leaf node.
|
||||
/// object of this semantics configuration is a leaf node or child rendering
|
||||
/// objects do not contribute to the semantics.
|
||||
ChildSemanticsConfigurationsDelegate? get childConfigurationsDelegate => _childConfigurationsDelegate;
|
||||
ChildSemanticsConfigurationsDelegate? _childConfigurationsDelegate;
|
||||
set childConfigurationsDelegate(ChildSemanticsConfigurationsDelegate? value) {
|
||||
|
||||
@ -60,6 +60,42 @@ void main() {
|
||||
semantics.dispose();
|
||||
}, semanticsEnabled: false);
|
||||
|
||||
testWidgets('Semantics tag only applies to immediate child', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Container(padding: const EdgeInsets.only(top: 20.0)),
|
||||
const Text('label'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(semantics, isNot(includesNodeWith(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
tags: <SemanticsTag>{RenderViewport.useTwoPaneSemantics},
|
||||
)));
|
||||
|
||||
await tester.pump();
|
||||
// Semantics should stay the same after a frame update.
|
||||
expect(semantics, isNot(includesNodeWith(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
tags: <SemanticsTag>{RenderViewport.useTwoPaneSemantics},
|
||||
)));
|
||||
|
||||
semantics.dispose();
|
||||
}, semanticsEnabled: false);
|
||||
|
||||
testWidgets('Semantics tooltip', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
|
||||
|
||||
@ -310,6 +310,160 @@ void main() {
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('semantics label is in order when uses widget span', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: <InlineSpan>[
|
||||
const TextSpan(text: 'before '),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Semantics(label: 'foo'),
|
||||
),
|
||||
const TextSpan(text: ' after'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
tester.getSemantics(find.byType(Text)),
|
||||
matchesSemantics(label: 'before \nfoo\n after'),
|
||||
);
|
||||
|
||||
// If the Paragraph is not dirty it should use the cache correctly.
|
||||
final RenderObject parent = tester.renderObject<RenderObject>(find.byType(Directionality));
|
||||
parent.markNeedsSemanticsUpdate();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
tester.getSemantics(find.byType(Text)),
|
||||
matchesSemantics(label: 'before \nfoo\n after'),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('semantics can handle some widget spans without semantics', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: <InlineSpan>[
|
||||
const TextSpan(text: 'before '),
|
||||
const WidgetSpan(
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(width: 10.0),
|
||||
),
|
||||
const TextSpan(text: ' mid'),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Semantics(label: 'foo'),
|
||||
),
|
||||
const TextSpan(text: ' after'),
|
||||
const WidgetSpan(
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(width: 10.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(tester.getSemantics(find.byType(Text)),
|
||||
matchesSemantics(label: 'before \n mid\nfoo\n after'));
|
||||
|
||||
// If the Paragraph is not dirty it should use the cache correctly.
|
||||
final RenderObject parent = tester.renderObject<RenderObject>(find.byType(Directionality));
|
||||
parent.markNeedsSemanticsUpdate();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.getSemantics(find.byType(Text)),
|
||||
matchesSemantics(label: 'before \n mid\nfoo\n after'));
|
||||
});
|
||||
|
||||
testWidgets('semantics can handle all widget spans without semantics', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: <InlineSpan>[
|
||||
TextSpan(text: 'before '),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(width: 10.0),
|
||||
),
|
||||
TextSpan(text: ' mid'),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(width: 10.0),
|
||||
),
|
||||
TextSpan(text: ' after'),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(width: 10.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(tester.getSemantics(find.byType(Text)),
|
||||
matchesSemantics(label: 'before \n mid\n after'));
|
||||
|
||||
// If the Paragraph is not dirty it should use the cache correctly.
|
||||
final RenderObject parent = tester.renderObject<RenderObject>(find.byType(Directionality));
|
||||
parent.markNeedsSemanticsUpdate();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.getSemantics(find.byType(Text)),
|
||||
matchesSemantics(label: 'before \n mid\n after'));
|
||||
});
|
||||
|
||||
testWidgets('semantics can handle widget spans with explicit semantics node', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: <InlineSpan>[
|
||||
const TextSpan(text: 'before '),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Semantics(label: 'inner', container: true),
|
||||
),
|
||||
const TextSpan(text: ' after'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
tester.getSemantics(find.byType(Text)),
|
||||
matchesSemantics(label: 'before \n after', children: <Matcher>[matchesSemantics(label: 'inner')]),
|
||||
);
|
||||
|
||||
// If the Paragraph is not dirty it should use the cache correctly.
|
||||
final RenderObject parent = tester.renderObject<RenderObject>(find.byType(Directionality));
|
||||
parent.markNeedsSemanticsUpdate();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
tester.getSemantics(find.byType(Text)),
|
||||
matchesSemantics(label: 'before \n after', children: <Matcher>[matchesSemantics(label: 'inner')]),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('semanticsLabel can be shorter than text', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
await tester.pumpWidget(Directionality(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user