Unverified Commit 00ee2e59 authored by chunhtai's avatar chunhtai Committed by GitHub

Adds text attributes support for semantics (#79599)

* Adds label attributes

* fix test

* comments

* list equal
parent a65328b4
...@@ -3757,7 +3757,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3757,7 +3757,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
/// ///
/// The [container] argument must not be null. /// The [container] argument must not be null.
/// ///
/// If the [label] is not null, the [textDirection] must also not be null. /// If the [attributedLabel] is not null, the [textDirection] must also not be null.
RenderSemanticsAnnotations({ RenderSemanticsAnnotations({
RenderBox? child, RenderBox? child,
bool container = false, bool container = false,
...@@ -3786,11 +3786,11 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3786,11 +3786,11 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
bool? liveRegion, bool? liveRegion,
int? maxValueLength, int? maxValueLength,
int? currentValueLength, int? currentValueLength,
String? label, AttributedString? attributedLabel,
String? value, AttributedString? attributedValue,
String? increasedValue, AttributedString? attributedIncreasedValue,
String? decreasedValue, AttributedString? attributedDecreasedValue,
String? hint, AttributedString? attributedHint,
SemanticsHintOverrides? hintOverrides, SemanticsHintOverrides? hintOverrides,
TextDirection? textDirection, TextDirection? textDirection,
SemanticsSortKey? sortKey, SemanticsSortKey? sortKey,
...@@ -3844,11 +3844,11 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3844,11 +3844,11 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_hidden = hidden, _hidden = hidden,
_image = image, _image = image,
_onDismiss = onDismiss, _onDismiss = onDismiss,
_label = label, _attributedLabel = attributedLabel,
_value = value, _attributedValue = attributedValue,
_increasedValue = increasedValue, _attributedIncreasedValue = attributedIncreasedValue,
_decreasedValue = decreasedValue, _attributedDecreasedValue = attributedDecreasedValue,
_hint = hint, _attributedHint = attributedHint,
_hintOverrides = hintOverrides, _hintOverrides = hintOverrides,
_textDirection = textDirection, _textDirection = textDirection,
_sortKey = sortKey, _sortKey = sortKey,
...@@ -4183,65 +4183,63 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -4183,65 +4183,63 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
/// If non-null, sets the [SemanticsNode.label] semantic to the given value. /// If non-null, sets the [SemanticsNode.attributedLabel] semantic to the given value.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
String? get label => _label; AttributedString? get attributedLabel => _attributedLabel;
String? _label; AttributedString? _attributedLabel;
set label(String? value) { set attributedLabel(AttributedString? value) {
if (_label == value) if (_attributedLabel == value)
return; return;
_label = value; _attributedLabel = value;
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
/// If non-null, sets the [SemanticsNode.value] semantic to the given value. /// If non-null, sets the [SemanticsNode.attributedValue] semantic to the given value.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
String? get value => _value; AttributedString? get attributedValue => _attributedValue;
String? _value; AttributedString? _attributedValue;
set value(String? value) { set attributedValue(AttributedString? value) {
if (_value == value) if (_attributedValue == value)
return; return;
_value = value; _attributedValue = value;
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
/// If non-null, sets the [SemanticsNode.increasedValue] semantic to the given /// If non-null, sets the [SemanticsNode.attributedIncreasedValue] semantic to the given value.
/// value.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
String? get increasedValue => _increasedValue; AttributedString? get attributedIncreasedValue => _attributedIncreasedValue;
String? _increasedValue; AttributedString? _attributedIncreasedValue;
set increasedValue(String? value) { set attributedIncreasedValue(AttributedString? value) {
if (_increasedValue == value) if (_attributedIncreasedValue == value)
return; return;
_increasedValue = value; _attributedIncreasedValue = value;
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
/// If non-null, sets the [SemanticsNode.decreasedValue] semantic to the given /// If non-null, sets the [SemanticsNode.attributedDecreasedValue] semantic to the given value.
/// value.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
String? get decreasedValue => _decreasedValue; AttributedString? get attributedDecreasedValue => _attributedDecreasedValue;
String? _decreasedValue; AttributedString? _attributedDecreasedValue;
set decreasedValue(String? value) { set attributedDecreasedValue(AttributedString? value) {
if (_decreasedValue == value) if (_attributedDecreasedValue == value)
return; return;
_decreasedValue = value; _attributedDecreasedValue = value;
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
/// If non-null, sets the [SemanticsNode.hint] semantic to the given value. /// If non-null, sets the [SemanticsNode.attributedHint] semantic to the given value.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
String? get hint => _hint; AttributedString? get attributedHint => _attributedHint;
String? _hint; AttributedString? _attributedHint;
set hint(String? value) { set attributedHint(AttributedString? value) {
if (_hint == value) if (_attributedHint == value)
return; return;
_hint = value; _attributedHint = value;
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
...@@ -4255,10 +4253,12 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -4255,10 +4253,12 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
/// If non-null, sets the [SemanticsNode.textDirection] semantic to the given value. /// If non-null, sets the [SemanticsNode.textDirection] semantic to the given
/// value.
/// ///
/// This must not be null if [label], [hint], [value], [increasedValue], or /// This must not be null if [attributedLabel], [attributedHint],
/// [decreasedValue] are not null. /// [attributedValue], [attributedIncreasedValue], or
/// [attributedDecreasedValue] are not null.
TextDirection? get textDirection => _textDirection; TextDirection? get textDirection => _textDirection;
TextDirection? _textDirection; TextDirection? _textDirection;
set textDirection(TextDirection? value) { set textDirection(TextDirection? value) {
...@@ -4765,16 +4765,16 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -4765,16 +4765,16 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.isHidden = hidden!; config.isHidden = hidden!;
if (image != null) if (image != null)
config.isImage = image!; config.isImage = image!;
if (label != null) if (attributedLabel != null)
config.label = label!; config.attributedLabel = attributedLabel!;
if (value != null) if (attributedValue != null)
config.value = value!; config.attributedValue = attributedValue!;
if (increasedValue != null) if (attributedIncreasedValue != null)
config.increasedValue = increasedValue!; config.attributedIncreasedValue = attributedIncreasedValue!;
if (decreasedValue != null) if (attributedDecreasedValue != null)
config.decreasedValue = decreasedValue!; config.attributedDecreasedValue = attributedDecreasedValue!;
if (hint != null) if (attributedHint != null)
config.hint = hint!; config.attributedHint = attributedHint!;
if (hintOverrides != null && hintOverrides!.isNotEmpty) if (hintOverrides != null && hintOverrides!.isNotEmpty)
config.hintOverrides = hintOverrides; config.hintOverrides = hintOverrides;
if (scopesRoute != null) if (scopesRoute != null)
......
...@@ -7305,10 +7305,15 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -7305,10 +7305,15 @@ class Semantics extends SingleChildRenderObjectWidget {
int? maxValueLength, int? maxValueLength,
int? currentValueLength, int? currentValueLength,
String? label, String? label,
AttributedString? attributedLabel,
String? value, String? value,
AttributedString? attributedValue,
String? increasedValue, String? increasedValue,
AttributedString? attributedIncreasedValue,
String? decreasedValue, String? decreasedValue,
AttributedString? attributedDecreasedValue,
String? hint, String? hint,
AttributedString? attributedHint,
String? onTapHint, String? onTapHint,
String? onLongPressHint, String? onLongPressHint,
TextDirection? textDirection, TextDirection? textDirection,
...@@ -7364,10 +7369,15 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -7364,10 +7369,15 @@ class Semantics extends SingleChildRenderObjectWidget {
maxValueLength: maxValueLength, maxValueLength: maxValueLength,
currentValueLength: currentValueLength, currentValueLength: currentValueLength,
label: label, label: label,
attributedLabel: attributedLabel,
value: value, value: value,
attributedValue: attributedValue,
increasedValue: increasedValue, increasedValue: increasedValue,
attributedIncreasedValue: attributedIncreasedValue,
decreasedValue: decreasedValue, decreasedValue: decreasedValue,
attributedDecreasedValue: attributedDecreasedValue,
hint: hint, hint: hint,
attributedHint: attributedHint,
textDirection: textDirection, textDirection: textDirection,
sortKey: sortKey, sortKey: sortKey,
tagForChildren: tagForChildren, tagForChildren: tagForChildren,
...@@ -7452,6 +7462,31 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -7452,6 +7462,31 @@ class Semantics extends SingleChildRenderObjectWidget {
/// an [ExcludeSemantics] widget and then another [Semantics] widget. /// an [ExcludeSemantics] widget and then another [Semantics] widget.
final bool excludeSemantics; final bool excludeSemantics;
AttributedString? get _effectiveAttributedLabel {
return properties.attributedLabel ??
(properties.label == null ? null : AttributedString(properties.label!));
}
AttributedString? get _effectiveAttributedValue {
return properties.attributedValue ??
(properties.value == null ? null : AttributedString(properties.value!));
}
AttributedString? get _effectiveAttributedIncreasedValue {
return properties.attributedIncreasedValue ??
(properties.increasedValue == null ? null : AttributedString(properties.increasedValue!));
}
AttributedString? get _effectiveAttributedDecreasedValue {
return properties.attributedDecreasedValue ??
(properties.decreasedValue == null ? null : AttributedString(properties.decreasedValue!));
}
AttributedString? get _effectiveAttributedHint {
return properties.attributedHint ??
(properties.hint == null ? null : AttributedString(properties.hint!));
}
@override @override
RenderSemanticsAnnotations createRenderObject(BuildContext context) { RenderSemanticsAnnotations createRenderObject(BuildContext context) {
return RenderSemanticsAnnotations( return RenderSemanticsAnnotations(
...@@ -7481,11 +7516,11 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -7481,11 +7516,11 @@ class Semantics extends SingleChildRenderObjectWidget {
namesRoute: properties.namesRoute, namesRoute: properties.namesRoute,
hidden: properties.hidden, hidden: properties.hidden,
image: properties.image, image: properties.image,
label: properties.label, attributedLabel: _effectiveAttributedLabel,
value: properties.value, attributedValue: _effectiveAttributedValue,
increasedValue: properties.increasedValue, attributedIncreasedValue: _effectiveAttributedIncreasedValue,
decreasedValue: properties.decreasedValue, attributedDecreasedValue: _effectiveAttributedDecreasedValue,
hint: properties.hint, attributedHint: _effectiveAttributedHint,
hintOverrides: properties.hintOverrides, hintOverrides: properties.hintOverrides,
textDirection: _getTextDirection(context), textDirection: _getTextDirection(context),
sortKey: properties.sortKey, sortKey: properties.sortKey,
...@@ -7518,7 +7553,10 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -7518,7 +7553,10 @@ class Semantics extends SingleChildRenderObjectWidget {
if (properties.textDirection != null) if (properties.textDirection != null)
return properties.textDirection; return properties.textDirection;
final bool containsText = properties.label != null || properties.value != null || properties.hint != null; final bool containsText = properties.attributedLabel != null ||
properties.label != null ||
properties.value != null ||
properties.hint != null;
if (!containsText) if (!containsText)
return null; return null;
...@@ -7554,11 +7592,11 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -7554,11 +7592,11 @@ class Semantics extends SingleChildRenderObjectWidget {
..liveRegion = properties.liveRegion ..liveRegion = properties.liveRegion
..maxValueLength = properties.maxValueLength ..maxValueLength = properties.maxValueLength
..currentValueLength = properties.currentValueLength ..currentValueLength = properties.currentValueLength
..label = properties.label ..attributedLabel = _effectiveAttributedLabel
..value = properties.value ..attributedValue = _effectiveAttributedValue
..increasedValue = properties.increasedValue ..attributedIncreasedValue = _effectiveAttributedIncreasedValue
..decreasedValue = properties.decreasedValue ..attributedDecreasedValue = _effectiveAttributedDecreasedValue
..hint = properties.hint ..attributedHint = _effectiveAttributedHint
..hintOverrides = properties.hintOverrides ..hintOverrides = properties.hintOverrides
..namesRoute = properties.namesRoute ..namesRoute = properties.namesRoute
..textDirection = _getTextDirection(context) ..textDirection = _getTextDirection(context)
......
...@@ -280,22 +280,22 @@ class _SemanticsDebuggerPainter extends CustomPainter { ...@@ -280,22 +280,22 @@ class _SemanticsDebuggerPainter extends CustomPainter {
if (isAdjustable) if (isAdjustable)
annotations.add('adjustable'); annotations.add('adjustable');
assert(data.label != null); assert(data.attributedLabel != null);
final String message; final String message;
if (data.label.isEmpty) { if (data.attributedLabel.string.isEmpty) {
message = annotations.join('; '); message = annotations.join('; ');
} else { } else {
final String label; final String label;
if (data.textDirection == null) { if (data.textDirection == null) {
label = '${Unicode.FSI}${data.label}${Unicode.PDI}'; label = '${Unicode.FSI}${data.attributedLabel.string}${Unicode.PDI}';
annotations.insert(0, 'MISSING TEXT DIRECTION'); annotations.insert(0, 'MISSING TEXT DIRECTION');
} else { } else {
switch (data.textDirection!) { switch (data.textDirection!) {
case TextDirection.rtl: case TextDirection.rtl:
label = '${Unicode.RLI}${data.label}${Unicode.PDF}'; label = '${Unicode.RLI}${data.attributedLabel.string}${Unicode.PDF}';
break; break;
case TextDirection.ltr: case TextDirection.ltr:
label = data.label; label = data.attributedLabel.string;
break; break;
} }
} }
......
...@@ -28,7 +28,7 @@ class TestTree { ...@@ -28,7 +28,7 @@ class TestTree {
child: RenderPositionedBox( child: RenderPositionedBox(
child: child = RenderConstrainedBox( child: child = RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(height: 20.0, width: 20.0), additionalConstraints: const BoxConstraints.tightFor(height: 20.0, width: 20.0),
child: RenderSemanticsAnnotations(label: 'Hello there foo', textDirection: TextDirection.ltr), child: RenderSemanticsAnnotations(attributedLabel: AttributedString('Hello there foo'), textDirection: TextDirection.ltr),
), ),
), ),
), ),
......
...@@ -11,7 +11,7 @@ import 'rendering_tester.dart'; ...@@ -11,7 +11,7 @@ import 'rendering_tester.dart';
void main() { void main() {
test('only send semantics update if semantics have changed', () { test('only send semantics update if semantics have changed', () {
final TestRender testRender = TestRender() final TestRender testRender = TestRender()
..label = 'hello' ..attributedLabel = AttributedString('hello')
..textDirection = TextDirection.ltr; ..textDirection = TextDirection.ltr;
final RenderConstrainedBox tree = RenderConstrainedBox( final RenderConstrainedBox tree = RenderConstrainedBox(
...@@ -46,7 +46,7 @@ void main() { ...@@ -46,7 +46,7 @@ void main() {
semanticsUpdateCount = 0; semanticsUpdateCount = 0;
// Change semantics and request update. // Change semantics and request update.
testRender.label = 'bye'; testRender.attributedLabel = AttributedString('bye');
testRender.markNeedsSemanticsUpdate(); testRender.markNeedsSemanticsUpdate();
pumpFrame(phase: EnginePhase.flushSemantics); pumpFrame(phase: EnginePhase.flushSemantics);
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math_64.dart'; import 'package:vector_math/vector_math_64.dart';
...@@ -63,6 +64,75 @@ void main() { ...@@ -63,6 +64,75 @@ void main() {
expect(node.getSemanticsData().tags, tags); expect(node.getSemanticsData().tags, tags);
}); });
test('SemanticsConfiguration can set both string label/value/hint and attributed version', () {
final SemanticsConfiguration config = SemanticsConfiguration();
config.label = 'label1';
expect(config.label, 'label1');
expect(config.attributedLabel.string, 'label1');
expect(config.attributedLabel.attributes.isEmpty, isTrue);
config.attributedLabel = AttributedString(
'label2',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end:1)),
]
);
expect(config.label, 'label2');
expect(config.attributedLabel.string, 'label2');
expect(config.attributedLabel.attributes.length, 1);
expect(config.attributedLabel.attributes[0] is SpellOutStringAttribute, isTrue);
expect(config.attributedLabel.attributes[0].range, const TextRange(start: 0, end: 1));
config.label = 'label3';
expect(config.label, 'label3');
expect(config.attributedLabel.string, 'label3');
expect(config.attributedLabel.attributes.isEmpty, isTrue);
config.value = 'value1';
expect(config.value, 'value1');
expect(config.attributedValue.string, 'value1');
expect(config.attributedValue.attributes.isEmpty, isTrue);
config.attributedValue = AttributedString(
'value2',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end:1)),
]
);
expect(config.value, 'value2');
expect(config.attributedValue.string, 'value2');
expect(config.attributedValue.attributes.length, 1);
expect(config.attributedValue.attributes[0] is SpellOutStringAttribute, isTrue);
expect(config.attributedValue.attributes[0].range, const TextRange(start: 0, end: 1));
config.value = 'value3';
expect(config.value, 'value3');
expect(config.attributedValue.string, 'value3');
expect(config.attributedValue.attributes.isEmpty, isTrue);
config.hint = 'hint1';
expect(config.hint, 'hint1');
expect(config.attributedHint.string, 'hint1');
expect(config.attributedHint.attributes.isEmpty, isTrue);
config.attributedHint = AttributedString(
'hint2',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end:1)),
]
);
expect(config.hint, 'hint2');
expect(config.attributedHint.string, 'hint2');
expect(config.attributedHint.attributes.length, 1);
expect(config.attributedHint.attributes[0] is SpellOutStringAttribute, isTrue);
expect(config.attributedHint.attributes[0].range, const TextRange(start: 0, end: 1));
config.hint = 'hint3';
expect(config.hint, 'hint3');
expect(config.attributedHint.string, 'hint3');
expect(config.attributedHint.attributes.isEmpty, isTrue);
});
test('mutate existing semantic node list errors', () { test('mutate existing semantic node list errors', () {
final SemanticsNode node = SemanticsNode() final SemanticsNode node = SemanticsNode()
..rect = const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0); ..rect = const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0);
...@@ -570,6 +640,26 @@ void main() { ...@@ -570,6 +640,26 @@ void main() {
); );
}); });
test('Attributed String can concate', () {
final AttributedString string1 = AttributedString(
'string1',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start:0, end:4)),
]
);
final AttributedString string2 = AttributedString(
'string2',
attributes: <StringAttribute>[
LocaleStringAttribute(locale: const Locale('es', 'MX'), range: const TextRange(start:0, end:4)),
]
);
final AttributedString result = string1 + string2;
expect(result.string, 'string1string2');
expect(result.attributes.length, 2);
expect(result.attributes[0].range, const TextRange(start:0, end:4));
expect(result.attributes[0] is SpellOutStringAttribute, isTrue);
});
test('Semantics id does not repeat', () { test('Semantics id does not repeat', () {
final SemanticsOwner owner = SemanticsOwner(); final SemanticsOwner owner = SemanticsOwner();
const int expectId = 1400; const int expectId = 1400;
......
...@@ -6,6 +6,7 @@ import 'dart:typed_data'; ...@@ -6,6 +6,7 @@ import 'dart:typed_data';
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';
void main() { void main() {
...@@ -83,6 +84,76 @@ void main() { ...@@ -83,6 +84,76 @@ void main() {
SemanticsUpdateBuilderSpy.observations.clear(); SemanticsUpdateBuilderSpy.observations.clear();
handle.dispose(); handle.dispose();
}); });
testWidgets('Semantics update receives attributed text', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
// Pumps a placeholder to trigger the warm up frame.
await tester.pumpWidget(
const Placeholder(),
// Stops right after the warm up frame.
null,
EnginePhase.build,
);
// The warm up frame will send update for an empty semantics tree. We
// ignore this one time update.
SemanticsUpdateBuilderSpy.observations.clear();
// Builds the real widget tree.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
attributedLabel: AttributedString(
'label',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)),
],
),
attributedValue: AttributedString(
'value',
attributes: <StringAttribute>[
LocaleStringAttribute(range: const TextRange(start: 0, end: 5), locale: const Locale('en', 'MX')),
],
),
attributedHint: AttributedString(
'hint',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)),
],
),
child: const Placeholder(),
),
),
);
expect(SemanticsUpdateBuilderSpy.observations.length, 2);
expect(SemanticsUpdateBuilderSpy.observations.containsKey(0), isTrue);
expect(SemanticsUpdateBuilderSpy.observations[0]!.childrenInTraversalOrder.length, 1);
expect(SemanticsUpdateBuilderSpy.observations[0]!.childrenInTraversalOrder[0], 1);
expect(SemanticsUpdateBuilderSpy.observations.containsKey(1), isTrue);
expect(SemanticsUpdateBuilderSpy.observations[1]!.childrenInTraversalOrder.length, 0);
expect(SemanticsUpdateBuilderSpy.observations[1]!.label, 'label');
expect(SemanticsUpdateBuilderSpy.observations[1]!.labelAttributes!.length, 1);
expect(SemanticsUpdateBuilderSpy.observations[1]!.labelAttributes![0] is SpellOutStringAttribute, isTrue);
expect(SemanticsUpdateBuilderSpy.observations[1]!.labelAttributes![0].range, const TextRange(start: 0, end: 5));
expect(SemanticsUpdateBuilderSpy.observations[1]!.value, 'value');
expect(SemanticsUpdateBuilderSpy.observations[1]!.valueAttributes!.length, 1);
expect(SemanticsUpdateBuilderSpy.observations[1]!.valueAttributes![0] is LocaleStringAttribute, isTrue);
final LocaleStringAttribute localeAttribute = SemanticsUpdateBuilderSpy.observations[1]!.valueAttributes![0] as LocaleStringAttribute;
expect(localeAttribute.range, const TextRange(start: 0, end: 5));
expect(localeAttribute.locale, const Locale('en', 'MX'));
expect(SemanticsUpdateBuilderSpy.observations[1]!.hint, 'hint');
expect(SemanticsUpdateBuilderSpy.observations[1]!.hintAttributes!.length, 1);
expect(SemanticsUpdateBuilderSpy.observations[1]!.hintAttributes![0] is SpellOutStringAttribute, isTrue);
expect(SemanticsUpdateBuilderSpy.observations[1]!.hintAttributes![0].range, const TextRange(start: 1, end: 2));
SemanticsUpdateBuilderSpy.observations.clear();
handle.dispose();
});
} }
class SemanticsUpdateTestBinding extends AutomatedTestWidgetsFlutterBinding { class SemanticsUpdateTestBinding extends AutomatedTestWidgetsFlutterBinding {
...@@ -114,18 +185,15 @@ class SemanticsUpdateBuilderSpy extends ui.SemanticsUpdateBuilder { ...@@ -114,18 +185,15 @@ class SemanticsUpdateBuilderSpy extends ui.SemanticsUpdateBuilder {
required double thickness, required double thickness,
required Rect rect, required Rect rect,
required String label, required String label,
// TODO(chunhtai): change the Object? to List<StringAttribute> when engine List<ui.StringAttribute>? labelAttributes,
// pr lands: https://github.com/flutter/engine/pull/25373.
// https://github.com/flutter/flutter/issues/79318.
Object? labelAttributes,
required String value, required String value,
Object? valueAttributes, List<ui.StringAttribute>? valueAttributes,
required String increasedValue, required String increasedValue,
Object? increasedValueAttributes, List<ui.StringAttribute>? increasedValueAttributes,
required String decreasedValue, required String decreasedValue,
Object? decreasedValueAttributes, List<ui.StringAttribute>? decreasedValueAttributes,
required String hint, required String hint,
Object? hintAttributes, List<ui.StringAttribute>? hintAttributes,
TextDirection? textDirection, TextDirection? textDirection,
required Float64List transform, required Float64List transform,
required Int32List childrenInTraversalOrder, required Int32List childrenInTraversalOrder,
...@@ -152,10 +220,15 @@ class SemanticsUpdateBuilderSpy extends ui.SemanticsUpdateBuilder { ...@@ -152,10 +220,15 @@ class SemanticsUpdateBuilderSpy extends ui.SemanticsUpdateBuilder {
thickness: thickness, thickness: thickness,
rect: rect, rect: rect,
label: label, label: label,
labelAttributes: labelAttributes,
hint: hint, hint: hint,
hintAttributes: hintAttributes,
value: value, value: value,
valueAttributes: valueAttributes,
increasedValue: increasedValue, increasedValue: increasedValue,
increasedValueAttributes: increasedValueAttributes,
decreasedValue: decreasedValue, decreasedValue: decreasedValue,
decreasedValueAttributes: decreasedValueAttributes,
textDirection: textDirection, textDirection: textDirection,
transform: transform, transform: transform,
childrenInTraversalOrder: childrenInTraversalOrder, childrenInTraversalOrder: childrenInTraversalOrder,
...@@ -184,10 +257,15 @@ class SemanticsNodeUpdateObservation { ...@@ -184,10 +257,15 @@ class SemanticsNodeUpdateObservation {
required this.thickness, required this.thickness,
required this.rect, required this.rect,
required this.label, required this.label,
required this.hint, this.labelAttributes,
required this.value, required this.value,
this.valueAttributes,
required this.increasedValue, required this.increasedValue,
this.increasedValueAttributes,
required this.decreasedValue, required this.decreasedValue,
this.decreasedValueAttributes,
required this.hint,
this.hintAttributes,
this.textDirection, this.textDirection,
required this.transform, required this.transform,
required this.childrenInTraversalOrder, required this.childrenInTraversalOrder,
...@@ -212,10 +290,15 @@ class SemanticsNodeUpdateObservation { ...@@ -212,10 +290,15 @@ class SemanticsNodeUpdateObservation {
final double thickness; final double thickness;
final Rect rect; final Rect rect;
final String label; final String label;
final String hint; final List<ui.StringAttribute>? labelAttributes;
final String value; final String value;
final List<ui.StringAttribute>? valueAttributes;
final String increasedValue; final String increasedValue;
final List<ui.StringAttribute>? increasedValueAttributes;
final String decreasedValue; final String decreasedValue;
final List<ui.StringAttribute>? decreasedValueAttributes;
final String hint;
final List<ui.StringAttribute>? hintAttributes;
final TextDirection? textDirection; final TextDirection? textDirection;
final Float64List transform; final Float64List transform;
final Int32List childrenInTraversalOrder; final Int32List childrenInTraversalOrder;
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
...@@ -211,6 +212,152 @@ void main() { ...@@ -211,6 +212,152 @@ void main() {
}); });
}); });
group('Semantics', () {
testWidgets('Semantics can set attributed Text', (WidgetTester tester) async {
final UniqueKey key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Semantics(
key: key,
attributedLabel: AttributedString(
'label',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)),
],
),
attributedValue: AttributedString(
'value',
attributes: <StringAttribute>[
LocaleStringAttribute(range: const TextRange(start: 0, end: 5), locale: const Locale('en', 'MX')),
],
),
attributedHint: AttributedString(
'hint',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)),
],
),
child: const Placeholder(),
)
),
)
);
final AttributedString attributedLabel = tester.getSemantics(find.byKey(key)).attributedLabel;
expect(attributedLabel.string, 'label');
expect(attributedLabel.attributes.length, 1);
expect(attributedLabel.attributes[0] is SpellOutStringAttribute, isTrue);
expect(attributedLabel.attributes[0].range, const TextRange(start:0, end: 5));
final AttributedString attributedValue = tester.getSemantics(find.byKey(key)).attributedValue;
expect(attributedValue.string, 'value');
expect(attributedValue.attributes.length, 1);
expect(attributedValue.attributes[0] is LocaleStringAttribute, isTrue);
final LocaleStringAttribute valueLocale = attributedValue.attributes[0] as LocaleStringAttribute;
expect(valueLocale.range, const TextRange(start:0, end: 5));
expect(valueLocale.locale, const Locale('en', 'MX'));
final AttributedString attributedHint = tester.getSemantics(find.byKey(key)).attributedHint;
expect(attributedHint.string, 'hint');
expect(attributedHint.attributes.length, 1);
expect(attributedHint.attributes[0] is SpellOutStringAttribute, isTrue);
expect(attributedHint.attributes[0].range, const TextRange(start:1, end: 2));
});
testWidgets('Semantics can merge attributed strings', (WidgetTester tester) async {
final UniqueKey key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Semantics(
key: key,
attributedLabel: AttributedString(
'label',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)),
],
),
attributedHint: AttributedString(
'hint',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)),
],
),
child: Semantics(
attributedLabel: AttributedString(
'label',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)),
],
),
attributedHint: AttributedString(
'hint',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)),
],
),
child: const Placeholder(),
)
)
),
)
);
final AttributedString attributedLabel = tester.getSemantics(find.byKey(key)).attributedLabel;
expect(attributedLabel.string, 'label\nlabel');
expect(attributedLabel.attributes.length, 2);
expect(attributedLabel.attributes[0] is SpellOutStringAttribute, isTrue);
expect(attributedLabel.attributes[0].range, const TextRange(start:0, end: 5));
expect(attributedLabel.attributes[1] is SpellOutStringAttribute, isTrue);
expect(attributedLabel.attributes[1].range, const TextRange(start:6, end: 11));
final AttributedString attributedHint = tester.getSemantics(find.byKey(key)).attributedHint;
expect(attributedHint.string, 'hint\nhint');
expect(attributedHint.attributes.length, 2);
expect(attributedHint.attributes[0] is SpellOutStringAttribute, isTrue);
expect(attributedHint.attributes[0].range, const TextRange(start:1, end: 2));
expect(attributedHint.attributes[1] is SpellOutStringAttribute, isTrue);
expect(attributedHint.attributes[1].range, const TextRange(start:6, end: 7));
});
testWidgets('Semantics can merge attributed strings with non attributed string', (WidgetTester tester) async {
final UniqueKey key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Semantics(
key: key,
attributedLabel: AttributedString(
'label1',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)),
],
),
child: Semantics(
label: 'label2',
child: Semantics(
attributedLabel: AttributedString(
'label3',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 1, end: 3)),
],
),
child: const Placeholder(),
),
)
)
),
)
);
final AttributedString attributedLabel = tester.getSemantics(find.byKey(key)).attributedLabel;
expect(attributedLabel.string, 'label1\nlabel2\nlabel3');
expect(attributedLabel.attributes.length, 2);
expect(attributedLabel.attributes[0] is SpellOutStringAttribute, isTrue);
expect(attributedLabel.attributes[0].range, const TextRange(start:0, end: 5));
expect(attributedLabel.attributes[1] is SpellOutStringAttribute, isTrue);
expect(attributedLabel.attributes[1].range, const TextRange(start:15, end: 17));
});
});
group('Row', () { group('Row', () {
testWidgets('multiple baseline aligned children', (WidgetTester tester) async { testWidgets('multiple baseline aligned children', (WidgetTester tester) async {
final UniqueKey key1 = UniqueKey(); final UniqueKey key1 = UniqueKey();
......
...@@ -546,11 +546,11 @@ void main() { ...@@ -546,11 +546,11 @@ void main() {
final SemanticsData data = SemanticsData( final SemanticsData data = SemanticsData(
flags: flags, flags: flags,
actions: actions, actions: actions,
label: 'a', attributedLabel: AttributedString('a'),
increasedValue: 'b', attributedIncreasedValue: AttributedString('b'),
value: 'c', attributedValue: AttributedString('c'),
decreasedValue: 'd', attributedDecreasedValue: AttributedString('d'),
hint: 'e', attributedHint: AttributedString('e'),
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0), rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
elevation: 3.0, elevation: 3.0,
......
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