Unverified Commit f66ee3e4 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Add semanticsLabel parameter to TextSpan (#30837)

parent 676fcc4b
......@@ -58,6 +58,7 @@ class TextSpan extends DiagnosticableTree {
this.text,
this.children,
this.recognizer,
this.semanticsLabel,
});
/// The style to apply to the [text] and the [children].
......@@ -156,6 +157,19 @@ class TextSpan extends DiagnosticableTree {
/// {@end-tool}
final GestureRecognizer recognizer;
/// An alternative semantics label for this text.
///
/// If present, the semantics of this span will contain this value instead
/// of the actual text.
///
/// This is useful for replacing abbreviations or shorthands with the full
/// text value:
///
/// ```dart
/// TextSpan(text: r'$$', semanticsLabel: 'Double dollars')
/// ```
final String semanticsLabel;
/// Apply the [style], [text], and [children] of this object to the
/// given [ParagraphBuilder], from which a [Paragraph] can be obtained.
/// [Paragraph] objects can be drawn on [Canvas] objects.
......@@ -220,12 +234,18 @@ class TextSpan extends DiagnosticableTree {
/// Flattens the [TextSpan] tree into a single string.
///
/// Styles are not honored in this process.
String toPlainText() {
/// Styles are not honored in this process. If `includeSemanticsLabels` is
/// true, then the text returned will include the [semanticsLabel]s instead of
/// the text contents when they are present.
String toPlainText({bool includeSemanticsLabels = true}) {
assert(debugAssertIsValid());
final StringBuffer buffer = StringBuffer();
visitTextSpan((TextSpan span) {
buffer.write(span.text);
if (span.semanticsLabel != null && includeSemanticsLabels) {
buffer.write(span.semanticsLabel);
} else {
buffer.write(span.text);
}
return true;
});
return buffer.toString();
......@@ -324,11 +344,12 @@ class TextSpan extends DiagnosticableTree {
return typedOther.text == text
&& typedOther.style == style
&& typedOther.recognizer == recognizer
&& typedOther.semanticsLabel == semanticsLabel
&& listEquals<TextSpan>(typedOther.children, children);
}
@override
int get hashCode => hashValues(style, text, recognizer, hashList(children));
int get hashCode => hashValues(style, text, recognizer, semanticsLabel, hashList(children));
@override
String toStringShort() => '$runtimeType';
......@@ -348,6 +369,12 @@ class TextSpan extends DiagnosticableTree {
defaultValue: null,
));
if (semanticsLabel != null) {
properties.add(StringProperty('semanticsLabel', semanticsLabel));
}
properties.add(StringProperty('text', text, showName: false, defaultValue: null));
if (style == null && text == null && children == null)
properties.add(DiagnosticsNode.message('(empty)'));
......
......@@ -475,8 +475,9 @@ class RenderParagraph extends RenderBox {
int offset = 0;
text.visitTextSpan((TextSpan span) {
if (span.recognizer != null && (span.recognizer is TapGestureRecognizer || span.recognizer is LongPressGestureRecognizer)) {
final int length = span.semanticsLabel?.length ?? span.text.length;
_recognizerOffsets.add(offset);
_recognizerOffsets.add(offset + span.text.length);
_recognizerOffsets.add(offset + length);
_recognizers.add(span.recognizer);
}
offset += span.text.length;
......
......@@ -348,7 +348,8 @@ class Text extends StatelessWidget {
/// An alternative semantics label for this text.
///
/// If present, the semantics of this widget will contain this value instead
/// of the actual text.
/// of the actual text. This will overwrite any of the semantics labels applied
/// directly to the [TextSpan]s.
///
/// This is useful for replacing abbreviations or shorthands with the full
/// text value:
......
......@@ -61,4 +61,27 @@ void main() {
' "c"\n'
));
});
test('TextSpan toPlainText', () {
const TextSpan textSpan = TextSpan(
text: 'a',
children: <TextSpan>[
TextSpan(text: 'b'),
TextSpan(text: 'c'),
],
);
expect(textSpan.toPlainText(), 'abc');
});
test('TextSpan toPlainText with semanticsLabel', () {
const TextSpan textSpan = TextSpan(
text: 'a',
children: <TextSpan>[
TextSpan(text: 'b', semanticsLabel: 'foo'),
TextSpan(text: 'c'),
],
);
expect(textSpan.toPlainText(), 'afooc');
expect(textSpan.toPlainText(includeSemanticsLabels: false), 'abc');
});
}
......@@ -184,6 +184,51 @@ void main() {
semantics.dispose();
});
testWidgets('recognizers split semantic nodes with text span labels', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const TextStyle textStyle = TextStyle(fontFamily: 'Ahem');
await tester.pumpWidget(
Text.rich(
TextSpan(
children: <TextSpan>[
const TextSpan(text: 'hello '),
TextSpan(text: 'world', recognizer: TapGestureRecognizer()..onTap = () { }),
const TextSpan(text: ' this is a '),
const TextSpan(text: 'cat-astrophe', semanticsLabel: 'regrettable event'),
],
style: textStyle,
),
textDirection: TextDirection.ltr,
),
);
final TestSemantics expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
children: <TestSemantics>[
TestSemantics(
label: 'hello ',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'world',
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[
SemanticsAction.tap,
],
),
TestSemantics(
label: ' regrettable event',
textDirection: TextDirection.ltr,
),
],
),
],
);
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true));
semantics.dispose();
});
testWidgets('recognizers split semantic node - bidi', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const TextStyle textStyle = TextStyle(fontFamily: 'Ahem');
......
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