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)
......
...@@ -6,7 +6,7 @@ import 'dart:math' as math; ...@@ -6,7 +6,7 @@ import 'dart:math' as math;
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'dart:ui' show Offset, Rect, SemanticsAction, SemanticsFlag, import 'dart:ui' show Offset, Rect, SemanticsAction, SemanticsFlag,
TextDirection; TextDirection, StringAttribute;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart' show MatrixUtils, TransformProperty; import 'package:flutter/painting.dart' show MatrixUtils, TransformProperty;
...@@ -16,7 +16,7 @@ import 'package:vector_math/vector_math_64.dart'; ...@@ -16,7 +16,7 @@ import 'package:vector_math/vector_math_64.dart';
import 'binding.dart' show SemanticsBinding; import 'binding.dart' show SemanticsBinding;
import 'semantics_event.dart'; import 'semantics_event.dart';
export 'dart:ui' show SemanticsAction; export 'dart:ui' show SemanticsAction, StringAttribute, SpellOutStringAttribute, LocaleStringAttribute;
export 'semantics_event.dart'; export 'semantics_event.dart';
/// Signature for a function that is called for each [SemanticsNode]. /// Signature for a function that is called for each [SemanticsNode].
...@@ -170,6 +170,91 @@ class CustomSemanticsAction { ...@@ -170,6 +170,91 @@ class CustomSemanticsAction {
} }
} }
/// A string that carries a list of [StringAttribute]s.
@immutable
class AttributedString {
/// Creates a attributed string.
///
/// The [TextRange] in the [attributes] must be inside the length of the
/// [string].
///
/// The [attributes] must not be changed after the attributed string is
/// created.
AttributedString(
this.string, {
this.attributes = const <StringAttribute>[],
}) : assert(string.isNotEmpty || attributes.isEmpty),
assert(() {
for (final StringAttribute attribute in attributes) {
assert(
string.length >= attribute.range.start &&
string.length >= attribute.range.end,
'The range in $attribute is outside of the string $string',
);
}
return true;
}());
/// The plain string stored in the attributed string.
final String string;
/// The attributes this string carries.
///
/// The list must not be modified after this string is created.
final List<StringAttribute> attributes;
/// Returns a new [AttributedString] by concatenate the operands
///
/// The string attribute list of the returned [AttributedString] will contains
/// the string attributes from both operands with updated text ranges.
AttributedString operator +(AttributedString other) {
if (string.isEmpty) {
return other;
}
if (other.string.isEmpty) {
return this;
}
// None of the strings is empty.
final String newString = string + other.string;
final List<StringAttribute> newAttributes = List<StringAttribute>.from(attributes);
if (other.attributes.isNotEmpty) {
final int offset = string.length;
for (final StringAttribute attribute in other.attributes) {
final TextRange newRange = TextRange(
start: attribute.range.start + offset,
end: attribute.range.end + offset,
);
final StringAttribute adjustedAttribute = attribute.copy(range: newRange);
newAttributes.add(adjustedAttribute);
}
}
return AttributedString(newString, attributes: newAttributes);
}
/// Two [AttributedString]s are equal if their string and attributes are.
@override
bool operator ==(Object other) {
return other.runtimeType == runtimeType
&& other is AttributedString
&& other.string == string
&& listEquals<StringAttribute>(other.attributes, attributes);
}
@override
int get hashCode {
return ui.hashValues(
string,
attributes,
);
}
@override
String toString() {
return "${objectRuntimeType(this, 'AttributedString')}('$string', attributes: $attributes)";
}
}
/// Summary information about a [SemanticsNode] object. /// Summary information about a [SemanticsNode] object.
/// ///
/// A semantics node might [SemanticsNode.mergeAllDescendantsIntoThisNode], /// A semantics node might [SemanticsNode.mergeAllDescendantsIntoThisNode],
...@@ -185,14 +270,14 @@ class SemanticsData with Diagnosticable { ...@@ -185,14 +270,14 @@ class SemanticsData with Diagnosticable {
/// The [flags], [actions], [label], and [Rect] arguments must not be null. /// The [flags], [actions], [label], and [Rect] arguments must not be null.
/// ///
/// If [label] is not empty, then [textDirection] must also not be null. /// If [label] is not empty, then [textDirection] must also not be null.
const SemanticsData({ SemanticsData({
required this.flags, required this.flags,
required this.actions, required this.actions,
required this.label, required this.attributedLabel,
required this.increasedValue, required this.attributedValue,
required this.value, required this.attributedIncreasedValue,
required this.decreasedValue, required this.attributedDecreasedValue,
required this.hint, required this.attributedHint,
required this.textDirection, required this.textDirection,
required this.rect, required this.rect,
required this.elevation, required this.elevation,
...@@ -211,16 +296,16 @@ class SemanticsData with Diagnosticable { ...@@ -211,16 +296,16 @@ class SemanticsData with Diagnosticable {
this.customSemanticsActionIds, this.customSemanticsActionIds,
}) : assert(flags != null), }) : assert(flags != null),
assert(actions != null), assert(actions != null),
assert(label != null), assert(attributedLabel != null),
assert(value != null), assert(attributedValue != null),
assert(decreasedValue != null), assert(attributedDecreasedValue != null),
assert(increasedValue != null), assert(attributedIncreasedValue != null),
assert(hint != null), assert(attributedHint != null),
assert(label == '' || textDirection != null, 'A SemanticsData object with label "$label" had a null textDirection.'), assert(attributedLabel.string == '' || textDirection != null, 'A SemanticsData object with label "${attributedLabel.string}" had a null textDirection.'),
assert(value == '' || textDirection != null, 'A SemanticsData object with value "$value" had a null textDirection.'), assert(attributedValue.string == '' || textDirection != null, 'A SemanticsData object with value "${attributedValue.string}" had a null textDirection.'),
assert(hint == '' || textDirection != null, 'A SemanticsData object with hint "$hint" had a null textDirection.'), assert(attributedHint.string == '' || textDirection != null, 'A SemanticsData object with hint "${attributedHint.string}" had a null textDirection.'),
assert(decreasedValue == '' || textDirection != null, 'A SemanticsData object with decreasedValue "$decreasedValue" had a null textDirection.'), assert(attributedDecreasedValue.string == '' || textDirection != null, 'A SemanticsData object with decreasedValue "${attributedDecreasedValue.string}" had a null textDirection.'),
assert(increasedValue == '' || textDirection != null, 'A SemanticsData object with increasedValue "$increasedValue" had a null textDirection.'), assert(attributedIncreasedValue.string == '' || textDirection != null, 'A SemanticsData object with increasedValue "${attributedIncreasedValue.string}" had a null textDirection.'),
assert(rect != null); assert(rect != null);
/// A bit field of [SemanticsFlag]s that apply to this node. /// A bit field of [SemanticsFlag]s that apply to this node.
...@@ -229,32 +314,62 @@ class SemanticsData with Diagnosticable { ...@@ -229,32 +314,62 @@ class SemanticsData with Diagnosticable {
/// A bit field of [SemanticsAction]s that apply to this node. /// A bit field of [SemanticsAction]s that apply to this node.
final int actions; final int actions;
/// A textual description of this node. /// A textual description for the current label of the node.
///
/// The reading direction is given by [textDirection].
String get label => attributedLabel.string;
/// A textual description for the current label of the node in
/// [AttributedString] format.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
final String label; final AttributedString attributedLabel;
/// A textual description for the current value of the node. /// A textual description for the current value of the node.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
final String value; String get value => attributedValue.string;
/// A textual description for the current value of the node in
/// [AttributedString] format.
///
/// The reading direction is given by [textDirection].
final AttributedString attributedValue;
/// The value that [value] will become after performing a /// The value that [value] will become after performing a
/// [SemanticsAction.increase] action. /// [SemanticsAction.increase] action.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
final String increasedValue; String get increasedValue => attributedIncreasedValue.string;
/// The value that [value] will become after performing a
/// [SemanticsAction.increase] action in [AttributedString] format.
///
/// The reading direction is given by [textDirection].
final AttributedString attributedIncreasedValue;
/// The value that [value] will become after performing a /// The value that [value] will become after performing a
/// [SemanticsAction.decrease] action. /// [SemanticsAction.decrease] action.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
final String decreasedValue; String get decreasedValue => attributedDecreasedValue.string;
/// The value that [value] will become after performing a
/// [SemanticsAction.decrease] action in [AttributedString] format.
///
/// The reading direction is given by [textDirection].
final AttributedString attributedDecreasedValue;
/// A brief description of the result of performing an action on this node. /// A brief description of the result of performing an action on this node.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
final String hint; String get hint => attributedHint.string;
/// A brief description of the result of performing an action on this node
/// in [AttributedString] format.
///
/// The reading direction is given by [textDirection].
final AttributedString attributedHint;
/// The reading direction for the text in [label], [value], [hint], /// The reading direction for the text in [label], [value], [hint],
/// [increasedValue], and [decreasedValue]. /// [increasedValue], and [decreasedValue].
...@@ -409,11 +524,11 @@ class SemanticsData with Diagnosticable { ...@@ -409,11 +524,11 @@ class SemanticsData with Diagnosticable {
describeEnum(flag), describeEnum(flag),
]; ];
properties.add(IterableProperty<String>('flags', flagSummary, ifEmpty: null)); properties.add(IterableProperty<String>('flags', flagSummary, ifEmpty: null));
properties.add(StringProperty('label', label, defaultValue: '')); properties.add(StringProperty('label', attributedLabel.attributes.isEmpty ? label : attributedLabel.toString(), defaultValue: ''));
properties.add(StringProperty('value', value, defaultValue: '')); properties.add(StringProperty('value', attributedValue.attributes.isEmpty? value :attributedValue.toString(), defaultValue: ''));
properties.add(StringProperty('increasedValue', increasedValue, defaultValue: '')); properties.add(StringProperty('increasedValue', attributedIncreasedValue.attributes.isEmpty? increasedValue :attributedIncreasedValue.toString(), defaultValue: ''));
properties.add(StringProperty('decreasedValue', decreasedValue, defaultValue: '')); properties.add(StringProperty('decreasedValue', attributedDecreasedValue.attributes.isEmpty? decreasedValue :attributedDecreasedValue.toString(), defaultValue: ''));
properties.add(StringProperty('hint', hint, defaultValue: '')); properties.add(StringProperty('hint', attributedHint.attributes.isEmpty? hint :attributedHint.toString(), defaultValue: ''));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null)); properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
if (textSelection?.isValid == true) if (textSelection?.isValid == true)
properties.add(MessageProperty('textSelection', '[${textSelection!.start}, ${textSelection!.end}]')); properties.add(MessageProperty('textSelection', '[${textSelection!.start}, ${textSelection!.end}]'));
...@@ -432,11 +547,11 @@ class SemanticsData with Diagnosticable { ...@@ -432,11 +547,11 @@ class SemanticsData with Diagnosticable {
return other is SemanticsData return other is SemanticsData
&& other.flags == flags && other.flags == flags
&& other.actions == actions && other.actions == actions
&& other.label == label && other.attributedLabel == attributedLabel
&& other.value == value && other.attributedValue == attributedValue
&& other.increasedValue == increasedValue && other.attributedIncreasedValue == attributedIncreasedValue
&& other.decreasedValue == decreasedValue && other.attributedDecreasedValue == attributedDecreasedValue
&& other.hint == hint && other.attributedHint == attributedHint
&& other.textDirection == textDirection && other.textDirection == textDirection
&& other.rect == rect && other.rect == rect
&& setEquals(other.tags, tags) && setEquals(other.tags, tags)
...@@ -461,11 +576,11 @@ class SemanticsData with Diagnosticable { ...@@ -461,11 +576,11 @@ class SemanticsData with Diagnosticable {
ui.hashValues( ui.hashValues(
flags, flags,
actions, actions,
label, attributedLabel,
value, attributedValue,
increasedValue, attributedIncreasedValue,
decreasedValue, attributedDecreasedValue,
hint, attributedHint,
textDirection, textDirection,
rect, rect,
tags, tags,
...@@ -610,10 +725,15 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -610,10 +725,15 @@ class SemanticsProperties extends DiagnosticableTree {
this.maxValueLength, this.maxValueLength,
this.currentValueLength, this.currentValueLength,
this.label, this.label,
this.attributedLabel,
this.value, this.value,
this.attributedValue,
this.increasedValue, this.increasedValue,
this.attributedIncreasedValue,
this.decreasedValue, this.decreasedValue,
this.attributedDecreasedValue,
this.hint, this.hint,
this.attributedHint,
this.hintOverrides, this.hintOverrides,
this.textDirection, this.textDirection,
this.sortKey, this.sortKey,
...@@ -639,7 +759,11 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -639,7 +759,11 @@ class SemanticsProperties extends DiagnosticableTree {
this.onDidLoseAccessibilityFocus, this.onDidLoseAccessibilityFocus,
this.onDismiss, this.onDismiss,
this.customSemanticsActions, this.customSemanticsActions,
}); }) : assert(label == null || attributedLabel == null, 'Only one of label or attributedLabel should be provided'),
assert(value == null || attributedValue == null, 'Only one of value or attributedValue should be provided'),
assert(increasedValue == null || attributedIncreasedValue == null, 'Only one of increasedValue or attributedIncreasedValue should be provided'),
assert(decreasedValue == null || attributedDecreasedValue == null, 'Only one of decreasedValue or attributedDecreasedValue should be provided'),
assert(hint == null || attributedHint == null, 'Only one of hint or attributedHint should be provided');
/// If non-null, indicates that this subtree represents something that can be /// If non-null, indicates that this subtree represents something that can be
/// in an enabled or disabled state. /// in an enabled or disabled state.
...@@ -846,61 +970,164 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -846,61 +970,164 @@ class SemanticsProperties extends DiagnosticableTree {
/// If a label is provided, there must either by an ambient [Directionality] /// If a label is provided, there must either by an ambient [Directionality]
/// or an explicit [textDirection] should be provided. /// or an explicit [textDirection] should be provided.
/// ///
/// Callers must not provide both [label] and [attributedLabel]. One or both
/// must be null.
///
/// See also: /// See also:
/// ///
/// * [SemanticsConfiguration.label] for a description of how this is exposed /// * [SemanticsConfiguration.label] for a description of how this is exposed
/// in TalkBack and VoiceOver. /// in TalkBack and VoiceOver.
/// * [attributedLabel] for a [AttributedString] version of this property.
final String? label; final String? label;
/// Provides a [AttributedString] version of textual description of the widget.
///
/// If a [attributedLabel] is provided, there must either by an ambient
/// [Directionality] or an explicit [textDirection] should be provided.
///
/// Callers must not provide both [label] and [attributedLabel]. One or both
/// must be null.
///
/// See also:
///
/// * [SemanticsConfiguration.attributedLabel] for a description of how this
/// is exposed in TalkBack and VoiceOver.
/// * [label] for a plain string version of this property.
final AttributedString? attributedLabel;
/// Provides a textual description of the value of the widget. /// Provides a textual description of the value of the widget.
/// ///
/// If a value is provided, there must either by an ambient [Directionality] /// If a value is provided, there must either by an ambient [Directionality]
/// or an explicit [textDirection] should be provided. /// or an explicit [textDirection] should be provided.
/// ///
/// Callers must not provide both [value] and [attributedValue], One or both
/// must be null.
///
/// See also: /// See also:
/// ///
/// * [SemanticsConfiguration.value] for a description of how this is exposed /// * [SemanticsConfiguration.value] for a description of how this is exposed
/// in TalkBack and VoiceOver. /// in TalkBack and VoiceOver.
/// * [attributedLabel] for a [AttributedString] version of this property.
final String? value; final String? value;
/// The value that [value] will become after a [SemanticsAction.increase] /// Provides a [AttributedString] version of textual description of the value
/// action has been performed on this widget. /// of the widget.
///
/// If a [attributedValue] is provided, there must either by an ambient
/// [Directionality] or an explicit [textDirection] should be provided.
///
/// Callers must not provide both [value] and [attributedValue], One or both
/// must be null.
///
/// See also:
///
/// * [SemanticsConfiguration.attributedValue] for a description of how this
/// is exposed in TalkBack and VoiceOver.
/// * [value] for a plain string version of this property.
final AttributedString? attributedValue;
/// The value that [value] or [attributedValue] will become after a
/// [SemanticsAction.increase] action has been performed on this widget.
/// ///
/// If a value is provided, [onIncrease] must also be set and there must /// If a value is provided, [onIncrease] must also be set and there must
/// either be an ambient [Directionality] or an explicit [textDirection] /// either be an ambient [Directionality] or an explicit [textDirection]
/// must be provided. /// must be provided.
/// ///
/// Callers must not provide both [increasedValue] and
/// [attributedIncreasedValue], One or both must be null.
///
/// See also: /// See also:
/// ///
/// * [SemanticsConfiguration.increasedValue] for a description of how this /// * [SemanticsConfiguration.increasedValue] for a description of how this
/// is exposed in TalkBack and VoiceOver. /// is exposed in TalkBack and VoiceOver.
/// * [attributedIncreasedValue] for a [AttributedString] version of this
/// property.
final String? increasedValue; final String? increasedValue;
/// The value that [value] will become after a [SemanticsAction.decrease] /// The [AttributedString] that [value] or [attributedValue] will become after
/// action has been performed on this widget. /// a [SemanticsAction.increase] action has been performed on this widget.
///
/// If a [attributedIncreasedValue] is provided, [onIncrease] must also be set
/// and there must either be an ambient [Directionality] or an explicit
/// [textDirection] must be provided.
///
/// Callers must not provide both [increasedValue] and
/// [attributedIncreasedValue], One or both must be null.
///
/// See also:
///
/// * [SemanticsConfiguration.attributedIncreasedValue] for a description of
/// how this is exposed in TalkBack and VoiceOver.
/// * [increasedValue] for a plain string version of this property.
final AttributedString? attributedIncreasedValue;
/// The value that [value] or [attributedValue] will become after a
/// [SemanticsAction.decrease] action has been performed on this widget.
/// ///
/// If a value is provided, [onDecrease] must also be set and there must /// If a value is provided, [onDecrease] must also be set and there must
/// either be an ambient [Directionality] or an explicit [textDirection] /// either be an ambient [Directionality] or an explicit [textDirection]
/// must be provided. /// must be provided.
/// ///
/// Callers must not provide both [decreasedValue] and
/// [attributedDecreasedValue], One or both must be null.
///
/// See also: /// See also:
/// ///
/// * [SemanticsConfiguration.decreasedValue] for a description of how this /// * [SemanticsConfiguration.decreasedValue] for a description of how this
/// is exposed in TalkBack and VoiceOver. /// is exposed in TalkBack and VoiceOver.
/// * [attributedDecreasedValue] for a [AttributedString] version of this
/// property.
final String? decreasedValue; final String? decreasedValue;
/// The [AttributedString] that [value] or [attributedValue] will become after
/// a [SemanticsAction.decrease] action has been performed on this widget.
///
/// If a [attributedDecreasedValue] is provided, [onDecrease] must also be set
/// and there must either be an ambient [Directionality] or an explicit
/// [textDirection] must be provided.
///
/// Callers must not provide both [decreasedValue] and
/// [attributedDecreasedValue], One or both must be null/// provided.
///
/// See also:
///
/// * [SemanticsConfiguration.attributedDecreasedValue] for a description of
/// how this is exposed in TalkBack and VoiceOver.
/// * [decreasedValue] for a plain string version of this property.
final AttributedString? attributedDecreasedValue;
/// Provides a brief textual description of the result of an action performed /// Provides a brief textual description of the result of an action performed
/// on the widget. /// on the widget.
/// ///
/// If a hint is provided, there must either be an ambient [Directionality] /// If a hint is provided, there must either be an ambient [Directionality]
/// or an explicit [textDirection] should be provided. /// or an explicit [textDirection] should be provided.
/// ///
/// Callers must not provide both [hint] and [attributedHint], One or both
/// must be null.
///
/// See also: /// See also:
/// ///
/// * [SemanticsConfiguration.hint] for a description of how this is exposed /// * [SemanticsConfiguration.hint] for a description of how this is exposed
/// in TalkBack and VoiceOver. /// in TalkBack and VoiceOver.
/// * [attributedHint] for a [AttributedString] version of this property.
final String? hint; final String? hint;
/// Provides a [AttributedString] version of brief textual description of the
/// result of an action performed on the widget.
///
/// If a [attributedHint] is provided, there must either by an ambient
/// [Directionality] or an explicit [textDirection] should be provided.
///
/// Callers must not provide both [hint] and [attributedHint], One or both
/// must be null.
///
/// See also:
///
/// * [SemanticsConfiguration.attributedHint] for a description of how this
/// is exposed in TalkBack and VoiceOver.
/// * [hint] for a plain string version of this property.
final AttributedString? attributedHint;
/// Provides hint values which override the default hints on supported /// Provides hint values which override the default hints on supported
/// platforms. /// platforms.
/// ///
...@@ -1183,8 +1410,11 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -1183,8 +1410,11 @@ class SemanticsProperties extends DiagnosticableTree {
properties.add(DiagnosticsProperty<bool>('checked', checked, defaultValue: null)); properties.add(DiagnosticsProperty<bool>('checked', checked, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('selected', selected, defaultValue: null)); properties.add(DiagnosticsProperty<bool>('selected', selected, defaultValue: null));
properties.add(StringProperty('label', label, defaultValue: '')); properties.add(StringProperty('label', label, defaultValue: ''));
properties.add(StringProperty('attributedLabel', attributedLabel.toString(), defaultValue: ''));
properties.add(StringProperty('value', value)); properties.add(StringProperty('value', value));
properties.add(StringProperty('attributedValue', attributedValue.toString(), defaultValue: ''));
properties.add(StringProperty('hint', hint)); properties.add(StringProperty('hint', hint));
properties.add(StringProperty('attributedHint', attributedHint.toString(), defaultValue: ''));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null)); properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null)); properties.add(DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null));
properties.add(DiagnosticsProperty<SemanticsHintOverrides>('hintOverrides', hintOverrides)); properties.add(DiagnosticsProperty<SemanticsHintOverrides>('hintOverrides', hintOverrides));
...@@ -1607,13 +1837,13 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1607,13 +1837,13 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
} }
bool _isDifferentFromCurrentSemanticAnnotation(SemanticsConfiguration config) { bool _isDifferentFromCurrentSemanticAnnotation(SemanticsConfiguration config) {
return _label != config.label || return _attributedLabel != config.attributedLabel ||
_hint != config.hint || _attributedHint != config.attributedHint ||
_elevation != config.elevation || _elevation != config.elevation ||
_thickness != config.thickness || _thickness != config.thickness ||
_decreasedValue != config.decreasedValue || _attributedValue != config.attributedValue ||
_value != config.value || _attributedIncreasedValue != config.attributedIncreasedValue ||
_increasedValue != config.increasedValue || _attributedDecreasedValue != config.attributedDecreasedValue ||
_flags != config._flags || _flags != config._flags ||
_textDirection != config.textDirection || _textDirection != config.textDirection ||
_sortKey != config._sortKey || _sortKey != config._sortKey ||
...@@ -1653,14 +1883,25 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1653,14 +1883,25 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
/// A textual description of this node. /// A textual description of this node.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
String get label => _label; String get label => _attributedLabel.string;
String _label = _kEmptyConfig.label;
/// A textual description of this node in [AttributedString] format.
///
/// The reading direction is given by [textDirection].
AttributedString get attributedLabel => _attributedLabel;
AttributedString _attributedLabel = _kEmptyConfig.attributedLabel;
/// A textual description for the current value of the node. /// A textual description for the current value of the node.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
String get value => _value; String get value => _attributedValue.string;
String _value = _kEmptyConfig.value;
/// A textual description for the current value of the node in
/// [AttributedString] format.
///
/// The reading direction is given by [textDirection].
AttributedString get attributedValue => _attributedValue;
AttributedString _attributedValue = _kEmptyConfig.attributedValue;
/// The value that [value] will have after a [SemanticsAction.decrease] action /// The value that [value] will have after a [SemanticsAction.decrease] action
/// has been performed. /// has been performed.
...@@ -1669,8 +1910,17 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1669,8 +1910,17 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
/// available on this node. /// available on this node.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
String get decreasedValue => _decreasedValue; String get decreasedValue => _attributedDecreasedValue.string;
String _decreasedValue = _kEmptyConfig.decreasedValue;
/// The value in [AttributedString] format that [value] or [attributedValue]
/// will have after a [SemanticsAction.decrease] action has been performed.
///
/// This property is only valid if the [SemanticsAction.decrease] action is
/// available on this node.
///
/// The reading direction is given by [textDirection].
AttributedString get attributedDecreasedValue => _attributedDecreasedValue;
AttributedString _attributedDecreasedValue = _kEmptyConfig.attributedDecreasedValue;
/// The value that [value] will have after a [SemanticsAction.increase] action /// The value that [value] will have after a [SemanticsAction.increase] action
/// has been performed. /// has been performed.
...@@ -1679,14 +1929,30 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1679,14 +1929,30 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
/// available on this node. /// available on this node.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
String get increasedValue => _increasedValue; String get increasedValue => _attributedIncreasedValue.string;
String _increasedValue = _kEmptyConfig.increasedValue;
/// The value in [AttributedString] format that [value] or [attributedValue]
/// will have after a [SemanticsAction.increase] action has been performed.
///
/// This property is only valid if the [SemanticsAction.increase] action is
/// available on this node.
///
/// The reading direction is given by [textDirection].
AttributedString get attributedIncreasedValue => _attributedIncreasedValue;
AttributedString _attributedIncreasedValue = _kEmptyConfig.attributedIncreasedValue;
/// A brief description of the result of performing an action on this node. /// A brief description of the result of performing an action on this node.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
String get hint => _hint; String get hint => _attributedHint.string;
String _hint = _kEmptyConfig.hint;
/// A brief description of the result of performing an action on this node
/// in [AttributedString] format.
///
/// The reading direction is given by [textDirection].
AttributedString get attributedHint => _attributedHint;
AttributedString _attributedHint = _kEmptyConfig.attributedHint;
/// The elevation along the z-axis at which the [rect] of this [SemanticsNode] /// The elevation along the z-axis at which the [rect] of this [SemanticsNode]
/// is located above its parent. /// is located above its parent.
...@@ -1891,11 +2157,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1891,11 +2157,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
'SemanticsNodes with children must not specify a platformViewId.', 'SemanticsNodes with children must not specify a platformViewId.',
); );
_label = config.label; _attributedLabel = config.attributedLabel;
_decreasedValue = config.decreasedValue; _attributedValue = config.attributedValue;
_value = config.value; _attributedIncreasedValue = config.attributedIncreasedValue;
_increasedValue = config.increasedValue; _attributedDecreasedValue = config.attributedDecreasedValue;
_hint = config.hint; _attributedHint = config.attributedHint;
_hintOverrides = config.hintOverrides; _hintOverrides = config.hintOverrides;
_elevation = config.elevation; _elevation = config.elevation;
_thickness = config.thickness; _thickness = config.thickness;
...@@ -1920,11 +2186,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1920,11 +2186,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]); _replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
assert( assert(
!_canPerformAction(SemanticsAction.increase) || (_value == '') == (_increasedValue == ''), !_canPerformAction(SemanticsAction.increase) || (value == '') == (increasedValue == ''),
'A SemanticsNode with action "increase" needs to be annotated with either both "value" and "increasedValue" or neither', 'A SemanticsNode with action "increase" needs to be annotated with either both "value" and "increasedValue" or neither',
); );
assert( assert(
!_canPerformAction(SemanticsAction.decrease) || (_value == '') == (_decreasedValue == ''), !_canPerformAction(SemanticsAction.decrease) || (value == '') == (decreasedValue == ''),
'A SemanticsNode with action "increase" needs to be annotated with either both "value" and "decreasedValue" or neither', 'A SemanticsNode with action "increase" needs to be annotated with either both "value" and "decreasedValue" or neither',
); );
} }
...@@ -1938,11 +2204,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1938,11 +2204,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
SemanticsData getSemanticsData() { SemanticsData getSemanticsData() {
int flags = _flags; int flags = _flags;
int actions = _actionsAsBits; int actions = _actionsAsBits;
String label = _label; AttributedString attributedLabel = _attributedLabel;
String hint = _hint; AttributedString attributedValue = _attributedValue;
String value = _value; AttributedString attributedIncreasedValue = _attributedIncreasedValue;
String increasedValue = _increasedValue; AttributedString attributedDecreasedValue = _attributedDecreasedValue;
String decreasedValue = _decreasedValue; AttributedString attributedHint = _attributedHint;
TextDirection? textDirection = _textDirection; TextDirection? textDirection = _textDirection;
Set<SemanticsTag>? mergedTags = tags == null ? null : Set<SemanticsTag>.from(tags!); Set<SemanticsTag>? mergedTags = tags == null ? null : Set<SemanticsTag>.from(tags!);
TextSelection? textSelection = _textSelection; TextSelection? textSelection = _textSelection;
...@@ -1991,12 +2257,12 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -1991,12 +2257,12 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
platformViewId ??= node._platformViewId; platformViewId ??= node._platformViewId;
maxValueLength ??= node._maxValueLength; maxValueLength ??= node._maxValueLength;
currentValueLength ??= node._currentValueLength; currentValueLength ??= node._currentValueLength;
if (value == '' || value == null) if (attributedValue == null || attributedValue.string == '')
value = node._value; attributedValue = node._attributedValue;
if (increasedValue == '' || increasedValue == null) if (attributedIncreasedValue == null || attributedIncreasedValue.string == '')
increasedValue = node._increasedValue; attributedIncreasedValue = node._attributedIncreasedValue;
if (decreasedValue == '' || decreasedValue == null) if (attributedDecreasedValue == null || attributedDecreasedValue.string == '')
decreasedValue = node._decreasedValue; attributedDecreasedValue = node._attributedDecreasedValue;
if (node.tags != null) { if (node.tags != null) {
mergedTags ??= <SemanticsTag>{}; mergedTags ??= <SemanticsTag>{};
mergedTags!.addAll(node.tags!); mergedTags!.addAll(node.tags!);
...@@ -2019,16 +2285,16 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -2019,16 +2285,16 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action)); customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action));
} }
} }
label = _concatStrings( attributedLabel = _concatAttributedString(
thisString: label, thisAttributedString: attributedLabel,
thisTextDirection: textDirection, thisTextDirection: textDirection,
otherString: node._label, otherAttributedString: node._attributedLabel,
otherTextDirection: node._textDirection, otherTextDirection: node._textDirection,
); );
hint = _concatStrings( attributedHint = _concatAttributedString(
thisString: hint, thisAttributedString: attributedHint,
thisTextDirection: textDirection, thisTextDirection: textDirection,
otherString: node._hint, otherAttributedString: node._attributedHint,
otherTextDirection: node._textDirection, otherTextDirection: node._textDirection,
); );
...@@ -2041,11 +2307,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -2041,11 +2307,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
return SemanticsData( return SemanticsData(
flags: flags, flags: flags,
actions: actions, actions: actions,
label: label, attributedLabel: attributedLabel,
value: value, attributedValue: attributedValue,
increasedValue: increasedValue, attributedIncreasedValue: attributedIncreasedValue,
decreasedValue: decreasedValue, attributedDecreasedValue: attributedDecreasedValue,
hint: hint, attributedHint: attributedHint,
textDirection: textDirection, textDirection: textDirection,
rect: rect, rect: rect,
transform: transform, transform: transform,
...@@ -2108,11 +2374,16 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -2108,11 +2374,16 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
flags: data.flags, flags: data.flags,
actions: data.actions, actions: data.actions,
rect: data.rect, rect: data.rect,
label: data.label, label: data.attributedLabel.string,
value: data.value, labelAttributes: data.attributedLabel.attributes,
decreasedValue: data.decreasedValue, value: data.attributedValue.string,
increasedValue: data.increasedValue, valueAttributes: data.attributedValue.attributes,
hint: data.hint, increasedValue: data.attributedIncreasedValue.string,
increasedValueAttributes: data.attributedIncreasedValue.attributes,
decreasedValue: data.attributedDecreasedValue.string,
decreasedValueAttributes: data.attributedDecreasedValue.attributes,
hint: data.attributedHint.string,
hintAttributes: data.attributedHint.attributes,
textDirection: data.textDirection, textDirection: data.textDirection,
textSelectionBase: data.textSelection != null ? data.textSelection!.baseOffset : -1, textSelectionBase: data.textSelection != null ? data.textSelection!.baseOffset : -1,
textSelectionExtent: data.textSelection != null ? data.textSelection!.extentOffset : -1, textSelectionExtent: data.textSelection != null ? data.textSelection!.extentOffset : -1,
...@@ -2246,11 +2517,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -2246,11 +2517,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
properties.add(IterableProperty<String>('flags', flags, ifEmpty: null)); properties.add(IterableProperty<String>('flags', flags, ifEmpty: null));
properties.add(FlagProperty('isInvisible', value: isInvisible, ifTrue: 'invisible')); properties.add(FlagProperty('isInvisible', value: isInvisible, ifTrue: 'invisible'));
properties.add(FlagProperty('isHidden', value: hasFlag(SemanticsFlag.isHidden), ifTrue: 'HIDDEN')); properties.add(FlagProperty('isHidden', value: hasFlag(SemanticsFlag.isHidden), ifTrue: 'HIDDEN'));
properties.add(StringProperty('label', _label, defaultValue: '')); properties.add(StringProperty('label', _attributedLabel.attributes.isEmpty ? _attributedLabel.string : _attributedLabel.toString(), defaultValue: ''));
properties.add(StringProperty('value', _value, defaultValue: '')); properties.add(StringProperty('value', _attributedValue.attributes.isEmpty ? _attributedValue.string : _attributedValue.toString(), defaultValue: ''));
properties.add(StringProperty('increasedValue', _increasedValue, defaultValue: '')); properties.add(StringProperty('increasedValue', _attributedIncreasedValue.attributes.isEmpty ? _attributedIncreasedValue.string : _attributedIncreasedValue.toString(), defaultValue: ''));
properties.add(StringProperty('decreasedValue', _decreasedValue, defaultValue: '')); properties.add(StringProperty('decreasedValue', _attributedDecreasedValue.attributes.isEmpty ? _attributedDecreasedValue.string : _attributedDecreasedValue.toString(), defaultValue: ''));
properties.add(StringProperty('hint', _hint, defaultValue: '')); properties.add(StringProperty('hint', _attributedHint.attributes.isEmpty ? _attributedHint.string : _attributedHint.toString(), defaultValue: ''));
properties.add(EnumProperty<TextDirection>('textDirection', _textDirection, defaultValue: null)); properties.add(EnumProperty<TextDirection>('textDirection', _textDirection, defaultValue: null));
properties.add(DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null)); properties.add(DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null));
if (_textSelection?.isValid == true) if (_textSelection?.isValid == true)
...@@ -3414,86 +3685,174 @@ class SemanticsConfiguration { ...@@ -3414,86 +3685,174 @@ class SemanticsConfiguration {
/// A textual description of the owning [RenderObject]. /// A textual description of the owning [RenderObject].
/// ///
/// On iOS this is used for the `accessibilityLabel` property defined in the /// Setting this attribute will override the [attributedLabel].
/// `UIAccessibility` Protocol. On Android it is concatenated together with
/// [value] and [hint] in the following order: [value], [label], [hint].
/// The concatenated value is then used as the `Text` description.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
String get label => _label; ///
String _label = ''; /// See also:
/// * [attributedLabel]: which is the [AttributedString] of this property.
String get label => _attributedLabel.string;
set label(String label) { set label(String label) {
assert(label != null); assert(label != null);
_label = label; _attributedLabel = AttributedString(label);
_hasBeenAnnotated = true;
}
/// A textual description of the owning [RenderObject] in [AttributedString]
/// format.
///
/// On iOS this is used for the `accessibilityAttributedLabel` property
/// defined in the `UIAccessibility` Protocol. On Android it is concatenated
/// together with [attributedValue] and [attributedHint] in the following
/// order: [attributedValue], [attributedLabel], [attributedHint]. The
/// concatenated value is then used as the `Text` description.
///
/// The reading direction is given by [textDirection].
AttributedString get attributedLabel => _attributedLabel;
AttributedString _attributedLabel = AttributedString('');
set attributedLabel(AttributedString attributedLabel) {
_attributedLabel = attributedLabel;
_hasBeenAnnotated = true; _hasBeenAnnotated = true;
} }
/// A textual description for the current value of the owning [RenderObject]. /// A textual description for the current value of the owning [RenderObject].
/// ///
/// On iOS this is used for the `accessibilityValue` property defined in the /// Setting this attribute will override the [attributedValue].
/// `UIAccessibility` Protocol. On Android it is concatenated together with
/// [label] and [hint] in the following order: [value], [label], [hint].
/// The concatenated value is then used as the `Text` description.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
/// ///
/// See also: /// See also:
/// ///
/// * [decreasedValue], describes what [value] will be after performing /// * [attributedValue], which is the [AttributedString] of this property.
/// [SemanticsAction.decrease]. /// * [decreasedValue] and [attributedDecreasedValue], describes what
/// * [increasedValue], describes what [value] will be after performing /// [value] will be after performing [SemanticsAction.decrease].
/// [SemanticsAction.increase]. /// * [increasedValue] and [attributedIncreasedValue], describes what
String get value => _value; /// [value] will be after performing [SemanticsAction.increase].
String _value = ''; String get value => _attributedValue.string;
set value(String value) { set value(String value) {
assert(value != null); assert(value != null);
_value = value; _attributedValue = AttributedString(value);
_hasBeenAnnotated = true;
}
/// A textual description for the current value of the owning [RenderObject]
/// in [AttributedString] format.
///
/// On iOS this is used for the `accessibilityAttributedValue` property
/// defined in the `UIAccessibility` Protocol. On Android it is concatenated
/// together with [attributedLabel] and [attributedHint] in the following
/// order: [attributedValue], [attributedLabel], [attributedHint]. The
/// concatenated value is then used as the `Text` description.
///
/// The reading direction is given by [textDirection].
///
/// See also:
///
/// * [attributedDecreasedValue], describes what [value] will be after
/// performing [SemanticsAction.decrease].
/// * [attributedIncreasedValue], describes what [value] will be after
/// performing [SemanticsAction.increase].
AttributedString get attributedValue => _attributedValue;
AttributedString _attributedValue = AttributedString('');
set attributedValue(AttributedString attributedValue) {
_attributedValue = attributedValue;
_hasBeenAnnotated = true; _hasBeenAnnotated = true;
} }
/// The value that [value] will have after performing a /// The value that [value] will have after performing a
/// [SemanticsAction.decrease] action. /// [SemanticsAction.decrease] action.
/// ///
/// This must be set if a handler for [SemanticsAction.decrease] is provided /// Setting this attribute will override the [attributedDecreasedValue].
/// and [value] is set. ///
/// One of the [attributedDecreasedValue] or [decreasedValue] must be set if
/// a handler for [SemanticsAction.decrease] is provided and one of the
/// [value] or [attributedValue] is set.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
String get decreasedValue => _decreasedValue; String get decreasedValue => _attributedDecreasedValue.string;
String _decreasedValue = '';
set decreasedValue(String decreasedValue) { set decreasedValue(String decreasedValue) {
assert(decreasedValue != null); assert(decreasedValue != null);
_decreasedValue = decreasedValue; _attributedDecreasedValue = AttributedString(decreasedValue);
_hasBeenAnnotated = true;
}
/// The value that [value] will have after performing a
/// [SemanticsAction.decrease] action in [AttributedString] format.
///
/// One of the [attributedDecreasedValue] or [decreasedValue] must be set if
/// a handler for [SemanticsAction.decrease] is provided and one of the
/// [value] or [attributedValue] is set.
///
/// The reading direction is given by [textDirection].
AttributedString get attributedDecreasedValue => _attributedDecreasedValue;
AttributedString _attributedDecreasedValue = AttributedString('');
set attributedDecreasedValue(AttributedString attributedDecreasedValue) {
_attributedDecreasedValue = attributedDecreasedValue;
_hasBeenAnnotated = true; _hasBeenAnnotated = true;
} }
/// The value that [value] will have after performing a /// The value that [value] will have after performing a
/// [SemanticsAction.increase] action. /// [SemanticsAction.increase] action.
/// ///
/// This must be set if a handler for [SemanticsAction.increase] is provided /// Setting this attribute will override the [attributedIncreasedValue].
/// and [value] is set. ///
/// One of the [attributedIncreasedValue] or [increasedValue] must be set if
/// a handler for [SemanticsAction.increase] is provided and one of the
/// [value] or [attributedValue] is set.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
String get increasedValue => _increasedValue; String get increasedValue => _attributedIncreasedValue.string;
String _increasedValue = '';
set increasedValue(String increasedValue) { set increasedValue(String increasedValue) {
assert(increasedValue != null); assert(increasedValue != null);
_increasedValue = increasedValue; _attributedIncreasedValue = AttributedString(increasedValue);
_hasBeenAnnotated = true;
}
/// The value that [value] will have after performing a
/// [SemanticsAction.increase] action in [AttributedString] format.
///
/// One of the [attributedIncreasedValue] or [increasedValue] must be set if
/// a handler for [SemanticsAction.increase] is provided and one of the
/// [value] or [attributedValue] is set.
///
/// The reading direction is given by [textDirection].
AttributedString get attributedIncreasedValue => _attributedIncreasedValue;
AttributedString _attributedIncreasedValue = AttributedString('');
set attributedIncreasedValue(AttributedString attributedIncreasedValue) {
_attributedIncreasedValue = attributedIncreasedValue;
_hasBeenAnnotated = true; _hasBeenAnnotated = true;
} }
/// A brief description of the result of performing an action on this node. /// A brief description of the result of performing an action on this node.
/// ///
/// On iOS this is used for the `accessibilityHint` property defined in the /// Setting this attribute will override the [attributedHint].
/// `UIAccessibility` Protocol. On Android it is concatenated together with
/// [label] and [value] in the following order: [value], [label], [hint].
/// The concatenated value is then used as the `Text` description.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
String get hint => _hint; ///
String _hint = ''; /// See also:
/// * [attributedHint]: which is the [AttributedString] of this property.
String get hint => _attributedHint.string;
set hint(String hint) { set hint(String hint) {
assert(hint != null); assert(hint != null);
_hint = hint; _attributedHint = AttributedString(hint);
_hasBeenAnnotated = true;
}
/// A brief description of the result of performing an action on this node in
/// [AttributedString] format.
///
/// On iOS this is used for the `accessibilityAttributedHint` property
/// defined in the `UIAccessibility` Protocol. On Android it is concatenated
/// together with [attributedLabel] and [attributedValue] in the following
/// order: [attributedValue], [attributedLabel], [attributedHint]. The
/// concatenated value is then used as the `Text` description.
///
/// The reading direction is given by [textDirection].
AttributedString get attributedHint => _attributedHint;
AttributedString _attributedHint = AttributedString('');
set attributedHint(AttributedString attributedHint) {
_attributedHint = attributedHint;
_hasBeenAnnotated = true; _hasBeenAnnotated = true;
} }
...@@ -3903,7 +4262,7 @@ class SemanticsConfiguration { ...@@ -3903,7 +4262,7 @@ class SemanticsConfiguration {
if (_currentValueLength != null && other._currentValueLength != null) { if (_currentValueLength != null && other._currentValueLength != null) {
return false; return false;
} }
if (_value != null && _value.isNotEmpty && other._value != null && other._value.isNotEmpty) if (_attributedValue != null && _attributedValue.string.isNotEmpty && other._attributedValue != null && other._attributedValue.string.isNotEmpty)
return false; return false;
return true; return true;
} }
...@@ -3943,22 +4302,22 @@ class SemanticsConfiguration { ...@@ -3943,22 +4302,22 @@ class SemanticsConfiguration {
textDirection ??= child.textDirection; textDirection ??= child.textDirection;
_sortKey ??= child._sortKey; _sortKey ??= child._sortKey;
_label = _concatStrings( _attributedLabel = _concatAttributedString(
thisString: _label, thisAttributedString: _attributedLabel,
thisTextDirection: textDirection, thisTextDirection: textDirection,
otherString: child._label, otherAttributedString: child._attributedLabel,
otherTextDirection: child.textDirection, otherTextDirection: child.textDirection,
); );
if (_decreasedValue == '' || _decreasedValue == null) if (_attributedValue == null || _attributedValue.string == '')
_decreasedValue = child._decreasedValue; _attributedValue = child._attributedValue;
if (_value == '' || _value == null) if (_attributedIncreasedValue == null || _attributedIncreasedValue.string == '')
_value = child._value; _attributedIncreasedValue = child._attributedIncreasedValue;
if (_increasedValue == '' || _increasedValue == null) if (_attributedDecreasedValue == null || _attributedDecreasedValue.string == '')
_increasedValue = child._increasedValue; _attributedDecreasedValue = child._attributedDecreasedValue;
_hint = _concatStrings( _attributedHint = _concatAttributedString(
thisString: _hint, thisAttributedString: _attributedHint,
thisTextDirection: textDirection, thisTextDirection: textDirection,
otherString: child._hint, otherAttributedString: child._attributedHint,
otherTextDirection: child.textDirection, otherTextDirection: child.textDirection,
); );
...@@ -3977,11 +4336,11 @@ class SemanticsConfiguration { ...@@ -3977,11 +4336,11 @@ class SemanticsConfiguration {
.._isMergingSemanticsOfDescendants = _isMergingSemanticsOfDescendants .._isMergingSemanticsOfDescendants = _isMergingSemanticsOfDescendants
.._textDirection = _textDirection .._textDirection = _textDirection
.._sortKey = _sortKey .._sortKey = _sortKey
.._label = _label .._attributedLabel = _attributedLabel
.._increasedValue = _increasedValue .._attributedIncreasedValue = _attributedIncreasedValue
.._value = _value .._attributedValue = _attributedValue
.._decreasedValue = _decreasedValue .._attributedDecreasedValue = _attributedDecreasedValue
.._hint = _hint .._attributedHint = _attributedHint
.._hintOverrides = _hintOverrides .._hintOverrides = _hintOverrides
.._elevation = _elevation .._elevation = _elevation
.._thickness = _thickness .._thickness = _thickness
...@@ -4020,28 +4379,28 @@ enum DebugSemanticsDumpOrder { ...@@ -4020,28 +4379,28 @@ enum DebugSemanticsDumpOrder {
traversalOrder, traversalOrder,
} }
String _concatStrings({ AttributedString _concatAttributedString({
required String thisString, required AttributedString thisAttributedString,
required String otherString, required AttributedString otherAttributedString,
required TextDirection? thisTextDirection, required TextDirection? thisTextDirection,
required TextDirection? otherTextDirection, required TextDirection? otherTextDirection,
}) { }) {
if (otherString.isEmpty) if (otherAttributedString.string.isEmpty)
return thisString; return thisAttributedString;
String nestedLabel = otherString;
if (thisTextDirection != otherTextDirection && otherTextDirection != null) { if (thisTextDirection != otherTextDirection && otherTextDirection != null) {
switch (otherTextDirection) { switch (otherTextDirection) {
case TextDirection.rtl: case TextDirection.rtl:
nestedLabel = '${Unicode.RLE}$nestedLabel${Unicode.PDF}'; otherAttributedString = AttributedString(Unicode.RLE) + otherAttributedString + AttributedString(Unicode.PDF);
break; break;
case TextDirection.ltr: case TextDirection.ltr:
nestedLabel = '${Unicode.LRE}$nestedLabel${Unicode.PDF}'; otherAttributedString = AttributedString(Unicode.LRE) + otherAttributedString + AttributedString(Unicode.PDF);
break; break;
} }
} }
if (thisString.isEmpty) if (thisAttributedString.string.isEmpty)
return nestedLabel; return otherAttributedString;
return '$thisString\n$nestedLabel';
return thisAttributedString + AttributedString('\n') + otherAttributedString;
} }
/// Base class for all sort keys for [SemanticsProperties.sortKey] accessibility /// Base class for all sort keys for [SemanticsProperties.sortKey] accessibility
......
...@@ -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