Unverified Commit 810a29d6 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Semantics framework updates (#18758)

Changes:

- Move the SemanticsConfiguration update from RenderToggleable to each subclass, so that Switch can use toggled.
- Add image, liveRegion, toggled properties to Semantics, SemanticsConfiguration, SemanticsNode
- Added semanticsLabel and excludeFromSemantics to Image (the latter so that we avoid creating a semantics node)
- Added onDismiss semantics action which maps to the modal escape on iOS and dismiss action on Android.
- Added dismiss and liveRegion to snackbar widget
- Updated custom painter semantics to handle image, liveRegion, toggle
- Updated relevant tests to use correct flag/action
parent 687f059a
......@@ -272,6 +272,7 @@ class RecipeCard extends StatelessWidget {
recipe.imagePath,
package: recipe.imagePackage,
fit: BoxFit.contain,
semanticLabel: recipe.name,
),
),
new Expanded(
......
......@@ -241,6 +241,12 @@ class _RenderCheckbox extends RenderToggleable {
super.value = newValue;
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isChecked = value == true;
}
// The square outer bounds of the checkbox at t, with the specified origin.
// At t == 0.0, the outer rect's size is _kEdgeSize (Checkbox.width)
// At t == 0.5, .. is _kEdgeSize - _kStrokeWidth
......
......@@ -236,6 +236,8 @@ class _RenderRadio extends RenderToggleable {
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isInMutuallyExclusiveGroup = true;
config
..isInMutuallyExclusiveGroup = true
..isChecked = value == true;
}
}
......@@ -1713,6 +1713,10 @@ class _PersistentBottomSheetState extends State<_PersistentBottomSheet> {
},
child: new Semantics(
container: true,
onDismiss: () {
close();
widget.onClosing();
},
child: new BottomSheet(
animationController: widget.animationController,
enableDrag: widget.enableDrag,
......
......@@ -224,7 +224,11 @@ class SnackBar extends StatelessWidget {
);
},
child: new Semantics(
liveRegion: true,
container: true,
onDismiss: () {
Scaffold.of(context).removeCurrentSnackBar(reason: SnackBarClosedReason.swipe);
},
child: new Dismissible(
key: const Key('dismissible'),
direction: DismissDirection.down,
......
......@@ -419,6 +419,12 @@ class _RenderSwitch extends RenderToggleable {
markNeedsPaint();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isToggled = value == true;
}
@override
void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas;
......
......@@ -339,7 +339,6 @@ abstract class RenderToggleable extends RenderConstrainedBox {
config.isEnabled = isInteractive;
if (isInteractive)
config.onTap = _handleTap;
config.isChecked = _value == true;
}
@override
......
......@@ -846,6 +846,15 @@ class RenderCustomPaint extends RenderProxyBox {
if (properties.namesRoute != null) {
config.namesRoute = properties.namesRoute;
}
if (properties.liveRegion != null) {
config.liveRegion = properties.liveRegion;
}
if (properties.toggled != null) {
config.isToggled = properties.toggled;
}
if (properties.image != null) {
config.isImage = properties.image;
}
if (properties.label != null) {
config.label = properties.label;
}
......@@ -912,6 +921,9 @@ class RenderCustomPaint extends RenderProxyBox {
if (properties.onDidLoseAccessibilityFocus != null) {
config.onDidLoseAccessibilityFocus = properties.onDidLoseAccessibilityFocus;
}
if (properties.onDismiss != null) {
config.onDismiss = properties.onDismiss;
}
newChild.updateWith(
config: config,
......
......@@ -3168,6 +3168,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
bool explicitChildNodes,
bool enabled,
bool checked,
bool toggled,
bool selected,
bool button,
bool header,
......@@ -3178,6 +3179,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
bool scopesRoute,
bool namesRoute,
bool hidden,
bool image,
bool liveRegion,
bool isSwitch,
String label,
String value,
String increasedValue,
......@@ -3186,6 +3190,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
TextDirection textDirection,
SemanticsSortKey sortKey,
VoidCallback onTap,
VoidCallback onDismiss,
VoidCallback onLongPress,
VoidCallback onScrollLeft,
VoidCallback onScrollRight,
......@@ -3207,6 +3212,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_explicitChildNodes = explicitChildNodes,
_enabled = enabled,
_checked = checked,
_toggled = toggled,
_selected = selected,
_button = button,
_header = header,
......@@ -3216,7 +3222,10 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_obscured = obscured,
_scopesRoute = scopesRoute,
_namesRoute = namesRoute,
_liveRegion = liveRegion,
_hidden = hidden,
_image = image,
_onDismiss = onDismiss,
_label = label,
_value = value,
_increasedValue = increasedValue,
......@@ -3408,6 +3417,38 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate();
}
/// If non-null, sets the [SemanticsNode.isImage] semantic to the given
/// value.
bool get image => _image;
bool _image;
set image(bool value) {
if (_image == value)
return;
_image = value;
}
/// If non-null, sets the [SemanticsNode.isLiveRegion] semantic to the given
/// value.
bool get liveRegion => _liveRegion;
bool _liveRegion;
set liveRegion(bool value) {
if (_liveRegion == value)
return;
_liveRegion = value;
markNeedsSemanticsUpdate();
}
/// If non-null, sets the [SemanticsNode.isToggled] semantic to the given
/// value.
bool get toggled => _toggled;
bool _toggled;
set toggled(bool value) {
if (_toggled == value)
return;
_toggled = value;
markNeedsSemanticsUpdate();
}
/// If non-null, sets the [SemanticsNode.label] semantic to the given value.
///
/// The reading direction is given by [textDirection].
......@@ -3516,6 +3557,24 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate();
}
/// The handler for [SemanticsAction.dismiss].
///
/// This is a request to dismiss the currently focused node.
///
/// TalkBack users on Android can trigger this action in the local context
/// menu, and VoiceOver users on iOS can trigger this action with a standard
/// gesture or menu option.
VoidCallback get onDismiss => _onDismiss;
VoidCallback _onDismiss;
set onDismiss(VoidCallback handler) {
if (_onDismiss == handler)
return;
final bool hadValue = _onDismiss != null;
_onDismiss = handler;
if ((handler != null) == hadValue)
markNeedsSemanticsUpdate();
}
/// The handler for [SemanticsAction.longPress].
///
/// This is the semantic equivalent of a user pressing and holding the screen
......@@ -3848,11 +3907,15 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.explicitChildNodes = explicitChildNodes;
assert((scopesRoute == true && explicitChildNodes == true) || scopesRoute != true,
'explicitChildNodes must be set to true if scopes route is true');
assert(!(toggled == true && checked == true),
'A semantics node cannot be toggled and checked at the same time');
if (enabled != null)
config.isEnabled = enabled;
if (checked != null)
config.isChecked = checked;
if (toggled != null)
config.isToggled = toggled;
if (selected != null)
config.isSelected = selected;
if (button != null)
......@@ -3869,6 +3932,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.isObscured = obscured;
if (hidden != null)
config.isHidden = hidden;
if (image != null)
config.isImage = image;
if (label != null)
config.label = label;
if (value != null)
......@@ -3883,6 +3948,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.scopesRoute = scopesRoute;
if (namesRoute != null)
config.namesRoute = namesRoute;
if (liveRegion != null)
config.liveRegion = liveRegion;
if (textDirection != null)
config.textDirection = textDirection;
if (sortKey != null)
......@@ -3894,6 +3961,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.onTap = _performTap;
if (onLongPress != null)
config.onLongPress = _performLongPress;
if (onDismiss != null)
config.onDismiss = _performDismiss;
if (onScrollLeft != null)
config.onScrollLeft = _performScrollLeft;
if (onScrollRight != null)
......@@ -3936,6 +4005,11 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
onLongPress();
}
void _performDismiss() {
if (onDismiss != null)
onDismiss();
}
void _performScrollLeft() {
if (onScrollLeft != null)
onScrollLeft();
......
......@@ -419,6 +419,7 @@ class SemanticsProperties extends DiagnosticableTree {
this.enabled,
this.checked,
this.selected,
this.toggled,
this.button,
this.header,
this.textField,
......@@ -428,6 +429,8 @@ class SemanticsProperties extends DiagnosticableTree {
this.obscured,
this.scopesRoute,
this.namesRoute,
this.image,
this.liveRegion,
this.label,
this.value,
this.increasedValue,
......@@ -451,6 +454,7 @@ class SemanticsProperties extends DiagnosticableTree {
this.onSetSelection,
this.onDidGainAccessibilityFocus,
this.onDidLoseAccessibilityFocus,
this.onDismiss,
this.customSemanticsActions,
});
......@@ -465,8 +469,17 @@ class SemanticsProperties extends DiagnosticableTree {
/// If non-null, indicates that this subtree represents a checkbox
/// or similar widget with a "checked" state, and what its current
/// state is.
///
/// This is mutually exclusive with [toggled].
final bool checked;
/// If non-null, indicates that this subtree represents a toggle switch
/// or similar widget with an "on" state, and what its current
/// state is.
///
/// This is mutually exclusive with [checked].
final bool toggled;
/// If non-null indicates that this subtree represents something that can be
/// in a selected or unselected state, and what its current state is.
///
......@@ -553,6 +566,36 @@ class SemanticsProperties extends DiagnosticableTree {
/// * [SemanticsFlag.namesRoute] for a description of how the name is used.
final bool namesRoute;
/// If non-null, whether the node represents an image.
///
/// See also:
///
/// * [SemanticsFlag.image], for the flag this setting controls.
final bool image;
/// If non-null, whether the node should be considered a live region.
///
/// On Android, when a live region semantics node is first created TalkBack
/// will make a polite announcement of the current label. This announcement
/// occurs even if the node is not focused. Subsequent polite announcements
/// can be made by sending a [UpdateLiveRegionEvent] semantics event. The
/// announcement will only be made if the node's label has changed since the
/// last update.
///
/// On iOS, no announcements are made but the node is marked as
/// `UIAccessibilityTraitUpdatesFrequently`.
///
/// An example of a live region is the [Snackbar] widget. When it appears
/// on the screen it may be difficult to focus to read the label. A live
/// region causes an initial polite announcement to be generated
/// automatically.
///
/// See also:
/// * [SemanticsFlag.liveRegion], the semantics flag this setting controls.
/// * [SemanticsConfiguration.liveRegion], for a full description of a live region.
/// * [UpdateLiveRegionEvent], to trigger a polite announcement of a live region.
final bool liveRegion;
/// Provides a textual description of the widget.
///
/// If a label is provided, there must either by an ambient [Directionality]
......@@ -815,6 +858,15 @@ class SemanticsProperties extends DiagnosticableTree {
/// * [FocusNode], [FocusScope], [FocusManager], which manage the input focus
final VoidCallback onDidLoseAccessibilityFocus;
/// The handler for [SemanticsAction.dismiss].
///
/// This is a request to dismiss the currently focused node.
///
/// TalkBack users on Android can trigger this action in the local context
/// menu, and VoiceOver users on iOS can trigger this action with a standard
/// gesture or menu option.
final VoidCallback onDismiss;
/// A map from each supported [CustomSemanticsAction] to a provided handler.
///
/// The handler associated with each custom action is called whenever a
......@@ -2346,6 +2398,20 @@ class SemanticsConfiguration {
_onScrollLeft = value;
}
/// The handler for [SemanticsAction.dismiss].
///
/// This is a request to dismiss the currently focused node.
///
/// TalkBack users on Android can trigger this action in the local context
/// menu, and VoiceOver users on iOS can trigger this action with a standard
/// gesture or menu option.
VoidCallback get onDismiss => _onDismiss;
VoidCallback _onDismiss;
set onDismiss(VoidCallback value) {
_addArgumentlessAction(SemanticsAction.dismiss, value);
_onDismiss = value;
}
/// The handler for [SemanticsAction.scrollRight].
///
/// This is the semantic equivalent of a user moving their finger across the
......@@ -2771,6 +2837,34 @@ class SemanticsConfiguration {
_setFlag(SemanticsFlag.namesRoute, value);
}
/// Whether the semantics node represents an image.
bool get isImage => _hasFlag(SemanticsFlag.isImage);
set isImage(bool value) {
_setFlag(SemanticsFlag.isImage, value);
}
/// Whether the semantics node is a live region.
///
/// On Android, when a live region semantics node is first created TalkBack
/// will make a polite announcement of the current label. This announcement
/// occurs even if the node is not focused. Subsequent polite announcements
/// can be made by sending a [UpdateLiveRegionEvent] semantics event. The
/// announcement will only be made if the node's label has changed since the
/// last update.
///
/// An example of a live region is the [Snackbar] widget. When it appears
/// on the screen it may be difficult to focus to read the label. A live
/// region causes an initial polite announcement to be generated
/// automatically.
///
/// See also:
///
/// * [SemanticsFlag.isLiveRegion], the semantics flag that this setting controls.
bool get liveRegion => _hasFlag(SemanticsFlag.isLiveRegion);
set liveRegion(bool value) {
_setFlag(SemanticsFlag.isLiveRegion, value);
}
/// The reading direction for the text in [label], [value], [hint],
/// [increasedValue], and [decreasedValue].
TextDirection get textDirection => _textDirection;
......@@ -2804,7 +2898,8 @@ class SemanticsConfiguration {
}
/// If this node has Boolean state that can be controlled by the user, whether
/// that state is on or off, corresponding to true and false, respectively.
/// that state is checked or unchecked, corresponding to true and false,
/// respectively.
///
/// Do not call the setter for this field if the owning [RenderObject] doesn't
/// have checked/unchecked state that can be controlled by the user.
......@@ -2817,6 +2912,20 @@ class SemanticsConfiguration {
_setFlag(SemanticsFlag.isChecked, value);
}
/// If this node has Boolean state that can be controlled by the user, whether
/// that state is on or off, corresponding to true and false, respectively.
///
/// Do not call the setter for this field if the owning [RenderObject] doesn't
/// have on/off state that can be controlled by the user.
///
/// The getter returns null if the owning [RenderObject] does not have
/// on/off state.
bool get isToggled => _hasFlag(SemanticsFlag.hasToggledState) ? _hasFlag(SemanticsFlag.isToggled) : null;
set isToggled(bool value) {
_setFlag(SemanticsFlag.hasToggledState, true);
_setFlag(SemanticsFlag.isToggled, value);
}
/// Whether the owning RenderObject corresponds to UI that allows the user to
/// pick one of several mutually exclusive options.
///
......
......@@ -133,3 +133,21 @@ class TapSemanticEvent extends SemanticsEvent {
@override
Map<String, dynamic> getDataMap() => const <String, dynamic>{};
}
/// An event which triggers a polite announcement of a live region.
///
/// This requires that the semantics node has already been marked as a live
/// region. On Android, TalkBack will make a verbal announcement, as long as
/// the label of the semantics node has changed since the last live region
/// update. iOS does not currently support this event.
///
/// See also:
///
/// * [SemanticsFlag.liveRegion], for a description of live regions.
class UpdateLiveRegionEvent extends SemanticsEvent {
/// Creates a new [UpdateLiveRegionEvent].
const UpdateLiveRegionEvent() : super('updateLiveRegion');
@override
Map<String, dynamic> getDataMap() => const <String, dynamic>{};
}
......@@ -5080,6 +5080,7 @@ class Semantics extends SingleChildRenderObjectWidget {
bool enabled,
bool checked,
bool selected,
bool toggled,
bool button,
bool header,
bool textField,
......@@ -5089,6 +5090,8 @@ class Semantics extends SingleChildRenderObjectWidget {
bool scopesRoute,
bool namesRoute,
bool hidden,
bool image,
bool liveRegion,
String label,
String value,
String increasedValue,
......@@ -5107,6 +5110,7 @@ class Semantics extends SingleChildRenderObjectWidget {
VoidCallback onCopy,
VoidCallback onCut,
VoidCallback onPaste,
VoidCallback onDismiss,
MoveCursorHandler onMoveCursorForwardByCharacter,
MoveCursorHandler onMoveCursorBackwardByCharacter,
SetSelectionHandler onSetSelection,
......@@ -5121,6 +5125,7 @@ class Semantics extends SingleChildRenderObjectWidget {
properties: new SemanticsProperties(
enabled: enabled,
checked: checked,
toggled: toggled,
selected: selected,
button: button,
header: header,
......@@ -5131,6 +5136,8 @@ class Semantics extends SingleChildRenderObjectWidget {
scopesRoute: scopesRoute,
namesRoute: namesRoute,
hidden: hidden,
image: image,
liveRegion: liveRegion,
label: label,
value: value,
increasedValue: increasedValue,
......@@ -5153,8 +5160,10 @@ class Semantics extends SingleChildRenderObjectWidget {
onMoveCursorBackwardByCharacter: onMoveCursorBackwardByCharacter,
onDidGainAccessibilityFocus: onDidGainAccessibilityFocus,
onDidLoseAccessibilityFocus: onDidLoseAccessibilityFocus,
onDismiss: onDismiss,
onSetSelection: onSetSelection,
customSemanticsActions: customSemanticsActions,
onSetSelection: onSetSelection,),
),
);
/// Creates a semantic annotation using [SemanticsProperties].
......@@ -5208,16 +5217,19 @@ class Semantics extends SingleChildRenderObjectWidget {
explicitChildNodes: explicitChildNodes,
enabled: properties.enabled,
checked: properties.checked,
toggled: properties.toggled,
selected: properties.selected,
button: properties.button,
header: properties.header,
textField: properties.textField,
focused: properties.focused,
liveRegion: properties.liveRegion,
inMutuallyExclusiveGroup: properties.inMutuallyExclusiveGroup,
obscured: properties.obscured,
scopesRoute: properties.scopesRoute,
namesRoute: properties.namesRoute,
hidden: properties.hidden,
image: properties.image,
label: properties.label,
value: properties.value,
increasedValue: properties.increasedValue,
......@@ -5234,6 +5246,7 @@ class Semantics extends SingleChildRenderObjectWidget {
onIncrease: properties.onIncrease,
onDecrease: properties.onDecrease,
onCopy: properties.onCopy,
onDismiss: properties.onDismiss,
onCut: properties.onCut,
onPaste: properties.onPaste,
onMoveCursorForwardByCharacter: properties.onMoveCursorForwardByCharacter,
......@@ -5261,10 +5274,11 @@ class Semantics extends SingleChildRenderObjectWidget {
void updateRenderObject(BuildContext context, RenderSemanticsAnnotations renderObject) {
renderObject
..container = container
..scopesRoute = properties.scopesRoute
..explicitChildNodes = explicitChildNodes
..scopesRoute = properties.scopesRoute
..enabled = properties.enabled
..checked = properties.checked
..toggled = properties.toggled
..selected = properties.selected
..button = properties.button
..header = properties.header
......@@ -5273,6 +5287,8 @@ class Semantics extends SingleChildRenderObjectWidget {
..inMutuallyExclusiveGroup = properties.inMutuallyExclusiveGroup
..obscured = properties.obscured
..hidden = properties.hidden
..image = properties.image
..liveRegion = properties.liveRegion
..label = properties.label
..value = properties.value
..increasedValue = properties.increasedValue
......@@ -5288,6 +5304,7 @@ class Semantics extends SingleChildRenderObjectWidget {
..onScrollUp = properties.onScrollUp
..onScrollDown = properties.onScrollDown
..onIncrease = properties.onIncrease
..onDismiss = properties.onDismiss
..onDecrease = properties.onDecrease
..onCopy = properties.onCopy
..onCut = properties.onCut
......
......@@ -120,9 +120,13 @@ class Image extends StatefulWidget {
/// widget should be placed in a context that sets tight layout constraints.
/// Otherwise, the image dimensions will change as the image is loaded, which
/// will result in ugly layout changes.
///
/// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored.
const Image({
Key key,
@required this.image,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width,
this.height,
this.color,
......@@ -152,9 +156,13 @@ class Image extends StatefulWidget {
///
/// An optional [headers] argument can be used to send custom HTTP headers
/// with the image request.
///
/// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored.
Image.network(String src, {
Key key,
double scale = 1.0,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width,
this.height,
this.color,
......@@ -183,9 +191,13 @@ class Image extends StatefulWidget {
///
/// On Android, this may require the
/// `android.permission.READ_EXTERNAL_STORAGE` permission.
///
/// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored.
Image.file(File file, {
Key key,
double scale = 1.0,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width,
this.height,
this.color,
......@@ -217,6 +229,8 @@ class Image extends StatefulWidget {
///
/// * If the `scale` argument is provided and is not null, then the exact
/// asset specified will be used.
///
/// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored.
//
// TODO(ianh): Implement the following (see ../services/image_resolution.dart):
// ///
......@@ -319,6 +333,8 @@ class Image extends StatefulWidget {
Image.asset(String name, {
Key key,
AssetBundle bundle,
this.semanticLabel,
this.excludeFromSemantics = false,
double scale,
this.width,
this.height,
......@@ -347,9 +363,13 @@ class Image extends StatefulWidget {
/// widget should be placed in a context that sets tight layout constraints.
/// Otherwise, the image dimensions will change as the image is loaded, which
/// will result in ugly layout changes.
///
/// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored.
Image.memory(Uint8List bytes, {
Key key,
double scale = 1.0,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width,
this.height,
this.color,
......@@ -472,6 +492,18 @@ class Image extends StatefulWidget {
/// (false), when the image provider changes.
final bool gaplessPlayback;
/// A Semantic description of the image.
///
/// Used to provide a description of the image to TalkBack on Andoid, and
/// VoiceOver on iOS.
final String semanticLabel;
/// Whether to exclude this image from semantics.
///
/// Useful for images which do not contribute meaningful information to an
/// application.
final bool excludeFromSemantics;
@override
_ImageState createState() => new _ImageState();
......@@ -488,6 +520,8 @@ class Image extends StatefulWidget {
properties.add(new EnumProperty<ImageRepeat>('repeat', repeat, defaultValue: ImageRepeat.noRepeat));
properties.add(new DiagnosticsProperty<Rect>('centerSlice', centerSlice, defaultValue: null));
properties.add(new FlagProperty('matchTextDirection', value: matchTextDirection, ifTrue: 'match text direction'));
properties.add(new StringProperty('semanticLabel', semanticLabel, defaultValue: null));
properties.add(new DiagnosticsProperty<bool>('this.excludeFromSemantics', excludeFromSemantics));
}
}
......@@ -578,7 +612,7 @@ class _ImageState extends State<Image> {
@override
Widget build(BuildContext context) {
return new RawImage(
final RawImage image = RawImage(
image: _imageInfo?.image,
width: widget.width,
height: widget.height,
......@@ -591,6 +625,14 @@ class _ImageState extends State<Image> {
centerSlice: widget.centerSlice,
matchTextDirection: widget.matchTextDirection,
);
if (widget.excludeFromSemantics)
return image;
return new Semantics(
container: widget.semanticLabel != null,
image: true,
label: widget.semanticLabel == null ? '' : widget.semanticLabel,
child: image,
);
}
@override
......
......@@ -101,8 +101,8 @@ void main() {
rect: new Rect.fromLTWH(0.0, 0.0, 800.0, 56.0),
transform: null,
flags: <SemanticsFlag>[
SemanticsFlag.hasCheckedState,
SemanticsFlag.isChecked,
SemanticsFlag.hasToggledState,
SemanticsFlag.isToggled,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled
],
......
......@@ -327,6 +327,7 @@ void _defineTests() {
key: const ValueKey<int>(1),
rect: new Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
properties: new SemanticsProperties(
onDismiss: () => performedActions.add(SemanticsAction.dismiss),
onTap: () => performedActions.add(SemanticsAction.tap),
onLongPress: () => performedActions.add(SemanticsAction.longPress),
onScrollLeft: () => performedActions.add(SemanticsAction.scrollLeft),
......@@ -349,8 +350,7 @@ void _defineTests() {
));
final Set<SemanticsAction> allActions = SemanticsAction.values.values.toSet()
..remove(SemanticsAction.customAction) // customAction is not user-exposed.
..remove(SemanticsAction.showOnScreen) // showOnScreen is not user-exposed
..remove(SemanticsAction.dismiss); // TODO(jonahwilliams): update when dismiss is exposed.
..remove(SemanticsAction.showOnScreen); // showOnScreen is not user-exposed
const int expectedId = 2;
final TestSemantics expectedSemantics = new TestSemantics.root(
......@@ -397,7 +397,7 @@ void _defineTests() {
testWidgets('Supports all flags', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
// checked state and toggled state are mutually exclusive.
await tester.pumpWidget(new CustomPaint(
painter: new _PainterWithSemantics(
semantics: new CustomPainterSemantics(
......@@ -416,18 +416,17 @@ void _defineTests() {
obscured: true,
scopesRoute: true,
namesRoute: true,
image: true,
liveRegion: true,
),
),
),
));
// TODO(jonahwilliams): update when the following semantics flags are added.
final List<SemanticsFlag> flags = SemanticsFlag.values.values.toList();
List<SemanticsFlag> flags = SemanticsFlag.values.values.toList();
flags
..remove(SemanticsFlag.isImage)
..remove(SemanticsFlag.hasToggledState)
..remove(SemanticsFlag.isToggled)
..remove(SemanticsFlag.isLiveRegion);
final TestSemantics expectedSemantics = new TestSemantics.root(
..remove(SemanticsFlag.isToggled);
TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
......@@ -443,6 +442,50 @@ void _defineTests() {
);
expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true));
await tester.pumpWidget(new CustomPaint(
painter: new _PainterWithSemantics(
semantics: new CustomPainterSemantics(
key: const ValueKey<int>(1),
rect: new Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
properties: const SemanticsProperties(
enabled: true,
toggled: true,
selected: true,
hidden: true,
button: true,
textField: true,
focused: true,
inMutuallyExclusiveGroup: true,
header: true,
obscured: true,
scopesRoute: true,
namesRoute: true,
image: true,
liveRegion: true,
),
),
),
));
flags = SemanticsFlag.values.values.toList();
flags
..remove(SemanticsFlag.hasCheckedState)
..remove(SemanticsFlag.isChecked);
expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 2,
rect: TestSemantics.fullScreen,
flags: flags,
),
]
),
],
);
expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true));
semantics.dispose();
});
......
......@@ -133,10 +133,12 @@ Widget buildImageAtRatio(String image, Key key, double ratio, bool inferSize, [A
child: inferSize ?
new Image(
key: key,
excludeFromSemantics: true,
image: new TestAssetImage(image)
) :
new Image(
key: key,
excludeFromSemantics: true,
image: new TestAssetImage(image),
height: imageSize,
width: imageSize,
......
......@@ -12,6 +12,7 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../painting/image_data.dart';
import 'semantics_tester.dart';
void main() {
testWidgets('Verify Image resets its RenderImage when changing providers', (WidgetTester tester) async {
......@@ -21,7 +22,8 @@ void main() {
new Container(
key: key,
child: new Image(
image: imageProvider1
image: imageProvider1,
excludeFromSemantics: true,
)
),
null,
......@@ -42,7 +44,8 @@ void main() {
new Container(
key: key,
child: new Image(
image: imageProvider2
image: imageProvider2,
excludeFromSemantics: true,
)
),
null,
......@@ -61,7 +64,8 @@ void main() {
key: key,
child: new Image(
gaplessPlayback: true,
image: imageProvider1
image: imageProvider1,
excludeFromSemantics: true,
)
),
null,
......@@ -83,7 +87,8 @@ void main() {
key: key,
child: new Image(
gaplessPlayback: true,
image: imageProvider2
image: imageProvider2,
excludeFromSemantics: true,
)
),
null,
......@@ -100,7 +105,8 @@ void main() {
await tester.pumpWidget(
new Image(
key: key,
image: imageProvider1
image: imageProvider1,
excludeFromSemantics: true,
),
null,
EnginePhase.layout
......@@ -119,7 +125,8 @@ void main() {
await tester.pumpWidget(
new Image(
key: key,
image: imageProvider2
image: imageProvider2,
excludeFromSemantics: true,
),
null,
EnginePhase.layout
......@@ -136,7 +143,8 @@ void main() {
new Image(
key: key,
gaplessPlayback: true,
image: imageProvider1
image: imageProvider1,
excludeFromSemantics: true,
),
null,
EnginePhase.layout
......@@ -156,6 +164,7 @@ void main() {
new Image(
key: key,
gaplessPlayback: true,
excludeFromSemantics: true,
image: imageProvider2
),
null,
......@@ -188,6 +197,7 @@ void main() {
padding: EdgeInsets.zero,
),
child: new Image(
excludeFromSemantics: true,
key: imageKey,
image: imageProvider
),
......@@ -214,6 +224,7 @@ void main() {
padding: EdgeInsets.zero,
),
child: new Image(
excludeFromSemantics: true,
key: imageKey,
image: imageProvider
),
......@@ -243,6 +254,7 @@ void main() {
padding: EdgeInsets.zero,
),
child: new Image(
excludeFromSemantics: true,
key: imageKey,
image: imageProvider
)
......@@ -280,6 +292,7 @@ void main() {
padding: EdgeInsets.zero,
),
child: new Image(
excludeFromSemantics: true,
key: imageKey,
image: imageProvider
)
......@@ -293,7 +306,7 @@ void main() {
testWidgets('Verify Image stops listening to ImageStream', (WidgetTester tester) async {
final TestImageProvider imageProvider = new TestImageProvider();
await tester.pumpWidget(new Image(image: imageProvider));
await tester.pumpWidget(new Image(image: imageProvider, excludeFromSemantics: true));
final State<Image> image = tester.state/*State<Image>*/(find.byType(Image));
expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, unresolved, 2 listeners), pixels: null)'));
imageProvider.complete();
......@@ -304,12 +317,13 @@ void main() {
});
testWidgets('Image.memory control test', (WidgetTester tester) async {
await tester.pumpWidget(new Image.memory(new Uint8List.fromList(kTransparentImage)));
await tester.pumpWidget(new Image.memory(new Uint8List.fromList(kTransparentImage), excludeFromSemantics: true,));
});
testWidgets('Image color and colorBlend parameters', (WidgetTester tester) async {
await tester.pumpWidget(
new Image(
excludeFromSemantics: true,
image: new TestImageProvider(),
color: const Color(0xFF00FF00),
colorBlendMode: BlendMode.clear
......@@ -345,6 +359,7 @@ void main() {
testWidgets('TickerMode controls stream registration', (WidgetTester tester) async {
final TestImageStreamCompleter imageStreamCompleter = new TestImageStreamCompleter();
final Image image = new Image(
excludeFromSemantics: true,
image: new TestImageProvider(streamCompleter: imageStreamCompleter),
);
await tester.pumpWidget(
......@@ -373,6 +388,7 @@ void main() {
new Container(
key: key,
child: new Image(
excludeFromSemantics: true,
image: imageProvider1
)
),
......@@ -396,7 +412,8 @@ void main() {
new Container(
key: key,
child: new Image(
image: imageProvider2
excludeFromSemantics: true,
image: imageProvider2
)
),
null,
......@@ -409,8 +426,8 @@ void main() {
});
testWidgets('Image State can be reconfigured to use another image', (WidgetTester tester) async {
final Image image1 = new Image(image: new TestImageProvider()..complete(), width: 10.0);
final Image image2 = new Image(image: new TestImageProvider()..complete(), width: 20.0);
final Image image1 = new Image(image: new TestImageProvider()..complete(), width: 10.0, excludeFromSemantics: true);
final Image image2 = new Image(image: new TestImageProvider()..complete(), width: 20.0, excludeFromSemantics: true);
final Column column = new Column(children: <Widget>[image1, image2]);
await tester.pumpWidget(column, null, EnginePhase.layout);
......@@ -425,6 +442,58 @@ void main() {
expect(renderObjects[1].image, isNotNull);
expect(renderObjects[1].width, 10.0);
});
testWidgets('Image contributes semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Row(
children: <Widget>[
new Image(
image: new TestImageProvider(),
width: 100.0,
height: 100.0,
semanticLabel: 'test',
),
],
),
),
);
expect(semantics, hasSemantics(new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
label: 'test',
rect: new Rect.fromLTWH(0.0, 0.0, 100.0, 100.0),
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[SemanticsFlag.isImage],
)
]
), ignoreTransform: true));
semantics.dispose();
});
testWidgets('Image can exclude semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Image(
image: new TestImageProvider(),
width: 100.0,
height: 100.0,
excludeFromSemantics: true,
),
),
);
expect(semantics, hasSemantics(new TestSemantics.root(
children: <TestSemantics>[]
)));
semantics.dispose();
});
}
class TestImageProvider extends ImageProvider<TestImageProvider> {
......
......@@ -21,7 +21,7 @@ Future<Null> main() async {
final GlobalKey imageKey = new GlobalKey();
await tester.pumpWidget(
new MaterialApp(
home: new Image(image: fakeImageProvider, key: imageKey),
home: new Image(image: fakeImageProvider, excludeFromSemantics: true, key: imageKey),
routes: <String, WidgetBuilder> {
'/page': (BuildContext context) => new Container()
}
......
......@@ -395,6 +395,7 @@ void main() {
await tester.pumpWidget(
new Semantics(
container: true,
onDismiss: () => performedActions.add(SemanticsAction.dismiss),
onTap: () => performedActions.add(SemanticsAction.tap),
onLongPress: () => performedActions.add(SemanticsAction.longPress),
onScrollLeft: () => performedActions.add(SemanticsAction.scrollLeft),
......@@ -416,8 +417,7 @@ void main() {
final Set<SemanticsAction> allActions = SemanticsAction.values.values.toSet()
..remove(SemanticsAction.customAction) // customAction is not user-exposed.
..remove(SemanticsAction.showOnScreen) // showOnScreen is not user-exposed
..remove(SemanticsAction.dismiss); // TODO(jonahwilliams): remove when dismiss action is exposed.
..remove(SemanticsAction.showOnScreen); // showOnScreen is not user-exposed
const int expectedId = 1;
final TestSemantics expectedSemantics = new TestSemantics.root(
......@@ -459,9 +459,10 @@ void main() {
testWidgets('Semantics widget supports all flags', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
// Note: checked state and toggled state are mutually exclusive.
await tester.pumpWidget(
new Semantics(
key: const Key('a'),
container: true,
explicitChildNodes: true,
// flags
......@@ -477,15 +478,14 @@ void main() {
obscured: true,
scopesRoute: true,
namesRoute: true,
image: true,
liveRegion: true,
)
);
// TODO(jonahwilliams): update when the following semantics flags are added.
final List<SemanticsFlag> flags = SemanticsFlag.values.values.toList();
flags
..remove(SemanticsFlag.isImage)
..remove(SemanticsFlag.hasToggledState)
..remove(SemanticsFlag.isToggled)
..remove(SemanticsFlag.isLiveRegion);
..remove(SemanticsFlag.isToggled);
TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
......@@ -498,7 +498,9 @@ void main() {
expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
await tester.pumpWidget(new Semantics(
key: const Key('b'),
container: true,
scopesRoute: false,
));
expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
......@@ -510,6 +512,26 @@ void main() {
);
expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
await tester.pumpWidget(
new Semantics(
key: const Key('c'),
toggled: true,
),
);
expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
rect: TestSemantics.fullScreen,
flags: <SemanticsFlag>[
SemanticsFlag.hasToggledState,
SemanticsFlag.isToggled,
],
),
],
);
expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
semantics.dispose();
});
......
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