Unverified Commit 1ec44a0c authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Incorporating Link Semantics (#41327)

parent 14c1c211
c635d70c726699cc79c0f3d900e0d0a3ea65efe2 a29385d9fe81761f2cedba7d9d4aa4709db04d83
...@@ -831,6 +831,9 @@ class RenderCustomPaint extends RenderProxyBox { ...@@ -831,6 +831,9 @@ class RenderCustomPaint extends RenderProxyBox {
if (properties.button != null) { if (properties.button != null) {
config.isButton = properties.button; config.isButton = properties.button;
} }
if (properties.link != null) {
config.isLink = properties.link;
}
if (properties.textField != null) { if (properties.textField != null) {
config.isTextField = properties.textField; config.isTextField = properties.textField;
} }
......
...@@ -887,6 +887,7 @@ class RenderParagraph extends RenderBox ...@@ -887,6 +887,7 @@ class RenderParagraph extends RenderBox
if (info.recognizer is TapGestureRecognizer) { if (info.recognizer is TapGestureRecognizer) {
final TapGestureRecognizer recognizer = info.recognizer; final TapGestureRecognizer recognizer = info.recognizer;
configuration.onTap = recognizer.onTap; configuration.onTap = recognizer.onTap;
configuration.isLink = true;
} else if (info.recognizer is LongPressGestureRecognizer) { } else if (info.recognizer is LongPressGestureRecognizer) {
final LongPressGestureRecognizer recognizer = info.recognizer; final LongPressGestureRecognizer recognizer = info.recognizer;
configuration.onLongPress = recognizer.onLongPress; configuration.onLongPress = recognizer.onLongPress;
......
...@@ -3485,6 +3485,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3485,6 +3485,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
bool toggled, bool toggled,
bool selected, bool selected,
bool button, bool button,
bool link,
bool header, bool header,
bool textField, bool textField,
bool readOnly, bool readOnly,
...@@ -3536,6 +3537,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3536,6 +3537,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_toggled = toggled, _toggled = toggled,
_selected = selected, _selected = selected,
_button = button, _button = button,
_link = link,
_header = header, _header = header,
_textField = textField, _textField = textField,
_readOnly = readOnly, _readOnly = readOnly,
...@@ -3678,6 +3680,16 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3678,6 +3680,16 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
/// If non-null, sets the [SemanticsNode.isLink] semantic to the given value.
bool get link => _link;
bool _link;
set link(bool value) {
if (link == value)
return;
_link = value;
markNeedsSemanticsUpdate();
}
/// If non-null, sets the [SemanticsNode.isHeader] semantic to the given value. /// If non-null, sets the [SemanticsNode.isHeader] semantic to the given value.
bool get header => _header; bool get header => _header;
bool _header; bool _header;
...@@ -4360,6 +4372,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -4360,6 +4372,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.isSelected = selected; config.isSelected = selected;
if (button != null) if (button != null)
config.isButton = button; config.isButton = button;
if (link != null)
config.isLink = link;
if (header != null) if (header != null)
config.isHeader = header; config.isHeader = header;
if (textField != null) if (textField != null)
......
...@@ -591,6 +591,7 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -591,6 +591,7 @@ class SemanticsProperties extends DiagnosticableTree {
this.selected, this.selected,
this.toggled, this.toggled,
this.button, this.button,
this.link,
this.header, this.header,
this.textField, this.textField,
this.readOnly, this.readOnly,
...@@ -670,6 +671,13 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -670,6 +671,13 @@ class SemanticsProperties extends DiagnosticableTree {
/// is focused. /// is focused.
final bool button; final bool button;
/// If non-null, indicates that this subtree represents a link.
///
/// iOS's VoiceOver provides users with a unique hint when a link is focused.
/// Android's Talkback will announce a link hint the same way it does a
/// button.
final bool link;
/// If non-null, indicates that this subtree represents a header. /// If non-null, indicates that this subtree represents a header.
/// ///
/// A header divides into sections. For example, an address book application /// A header divides into sections. For example, an address book application
...@@ -3615,6 +3623,12 @@ class SemanticsConfiguration { ...@@ -3615,6 +3623,12 @@ class SemanticsConfiguration {
_setFlag(SemanticsFlag.isButton, value); _setFlag(SemanticsFlag.isButton, value);
} }
/// Whether the owning [RenderObject] is a link (true) or not (false).
bool get isLink => _hasFlag(SemanticsFlag.isLink);
set isLink(bool value) {
_setFlag(SemanticsFlag.isLink, value);
}
/// Whether the owning [RenderObject] is a header (true) or not (false). /// Whether the owning [RenderObject] is a header (true) or not (false).
bool get isHeader => _hasFlag(SemanticsFlag.isHeader); bool get isHeader => _hasFlag(SemanticsFlag.isHeader);
set isHeader(bool value) { set isHeader(bool value) {
......
...@@ -6184,6 +6184,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -6184,6 +6184,7 @@ class Semantics extends SingleChildRenderObjectWidget {
bool selected, bool selected,
bool toggled, bool toggled,
bool button, bool button,
bool link,
bool header, bool header,
bool textField, bool textField,
bool readOnly, bool readOnly,
...@@ -6237,6 +6238,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -6237,6 +6238,7 @@ class Semantics extends SingleChildRenderObjectWidget {
toggled: toggled, toggled: toggled,
selected: selected, selected: selected,
button: button, button: button,
link: link,
header: header, header: header,
textField: textField, textField: textField,
readOnly: readOnly, readOnly: readOnly,
...@@ -6349,6 +6351,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -6349,6 +6351,7 @@ class Semantics extends SingleChildRenderObjectWidget {
toggled: properties.toggled, toggled: properties.toggled,
selected: properties.selected, selected: properties.selected,
button: properties.button, button: properties.button,
link: properties.link,
header: properties.header, header: properties.header,
textField: properties.textField, textField: properties.textField,
readOnly: properties.readOnly, readOnly: properties.readOnly,
...@@ -6418,6 +6421,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -6418,6 +6421,7 @@ class Semantics extends SingleChildRenderObjectWidget {
..toggled = properties.toggled ..toggled = properties.toggled
..selected = properties.selected ..selected = properties.selected
..button = properties.button ..button = properties.button
..link = properties.link
..header = properties.header ..header = properties.header
..textField = properties.textField ..textField = properties.textField
..readOnly = properties.readOnly ..readOnly = properties.readOnly
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import '../flutter_test_alternative.dart'; import '../flutter_test_alternative.dart';
void main() { void main() {
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:ui' as ui show TextBox; import 'dart:ui' as ui show TextBox;
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
......
...@@ -573,6 +573,7 @@ void main() { ...@@ -573,6 +573,7 @@ void main() {
expect(config.isSemanticBoundary, isFalse); expect(config.isSemanticBoundary, isFalse);
expect(config.isButton, isFalse); expect(config.isButton, isFalse);
expect(config.isLink, isFalse);
expect(config.isMergingSemanticsOfDescendants, isFalse); expect(config.isMergingSemanticsOfDescendants, isFalse);
expect(config.isEnabled, null); expect(config.isEnabled, null);
expect(config.isChecked, null); expect(config.isChecked, null);
...@@ -596,6 +597,7 @@ void main() { ...@@ -596,6 +597,7 @@ void main() {
config.isSemanticBoundary = true; config.isSemanticBoundary = true;
config.isButton = true; config.isButton = true;
config.isLink = true;
config.isMergingSemanticsOfDescendants = true; config.isMergingSemanticsOfDescendants = true;
config.isEnabled = true; config.isEnabled = true;
config.isChecked = true; config.isChecked = true;
...@@ -632,6 +634,7 @@ void main() { ...@@ -632,6 +634,7 @@ void main() {
expect(config.isSemanticBoundary, isTrue); expect(config.isSemanticBoundary, isTrue);
expect(config.isButton, isTrue); expect(config.isButton, isTrue);
expect(config.isLink, isTrue);
expect(config.isMergingSemanticsOfDescendants, isTrue); expect(config.isMergingSemanticsOfDescendants, isTrue);
expect(config.isEnabled, isTrue); expect(config.isEnabled, isTrue);
expect(config.isChecked, isTrue); expect(config.isChecked, isTrue);
......
...@@ -413,6 +413,7 @@ void _defineTests() { ...@@ -413,6 +413,7 @@ void _defineTests() {
selected: true, selected: true,
hidden: true, hidden: true,
button: true, button: true,
link: true,
textField: true, textField: true,
readOnly: true, readOnly: true,
focused: true, focused: true,
...@@ -461,6 +462,7 @@ void _defineTests() { ...@@ -461,6 +462,7 @@ void _defineTests() {
selected: true, selected: true,
hidden: true, hidden: true,
button: true, button: true,
link: true,
textField: true, textField: true,
readOnly: true, readOnly: true,
focused: true, focused: true,
......
...@@ -473,6 +473,7 @@ void main() { ...@@ -473,6 +473,7 @@ void main() {
checked: true, checked: true,
selected: true, selected: true,
button: true, button: true,
link: true,
textField: true, textField: true,
readOnly: true, readOnly: true,
focused: true, focused: true,
......
...@@ -232,6 +232,7 @@ void main() { ...@@ -232,6 +232,7 @@ void main() {
TestSemantics( TestSemantics(
label: 'Clickable', label: 'Clickable',
actions: <SemanticsAction>[SemanticsAction.tap], actions: <SemanticsAction>[SemanticsAction.tap],
flags: <SemanticsFlag>[SemanticsFlag.isLink],
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
), ),
], ],
...@@ -281,9 +282,8 @@ void main() { ...@@ -281,9 +282,8 @@ void main() {
TestSemantics( TestSemantics(
label: 'world', label: 'world',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
actions: <SemanticsAction>[ actions: <SemanticsAction>[SemanticsAction.tap],
SemanticsAction.tap, flags: <SemanticsFlag>[SemanticsFlag.isLink],
],
), ),
TestSemantics( TestSemantics(
label: ' this is a cat-astrophe', label: ' this is a cat-astrophe',
...@@ -338,6 +338,7 @@ void main() { ...@@ -338,6 +338,7 @@ void main() {
label: 'world', label: 'world',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
actions: <SemanticsAction>[SemanticsAction.tap], actions: <SemanticsAction>[SemanticsAction.tap],
flags: <SemanticsFlag>[SemanticsFlag.isLink],
), ),
], ],
), ),
...@@ -389,9 +390,8 @@ void main() { ...@@ -389,9 +390,8 @@ void main() {
TestSemantics( TestSemantics(
label: 'world', label: 'world',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
actions: <SemanticsAction>[ actions: <SemanticsAction>[SemanticsAction.tap],
SemanticsAction.tap, flags: <SemanticsFlag>[SemanticsFlag.isLink],
],
), ),
TestSemantics( TestSemantics(
label: ' this is a regrettable event', label: ' this is a regrettable event',
...@@ -454,9 +454,8 @@ void main() { ...@@ -454,9 +454,8 @@ void main() {
rect: const Rect.fromLTRB(150.0, -4.0, 200.0, 18.0), rect: const Rect.fromLTRB(150.0, -4.0, 200.0, 18.0),
label: 'RIS', label: 'RIS',
textDirection: TextDirection.rtl, // in the last string we switched to RTL using RLE. textDirection: TextDirection.rtl, // in the last string we switched to RTL using RLE.
actions: <SemanticsAction>[ actions: <SemanticsAction>[SemanticsAction.tap],
SemanticsAction.tap, flags: <SemanticsFlag>[SemanticsFlag.isLink],
],
), ),
TestSemantics( TestSemantics(
rect: const Rect.fromLTRB(192.0, -4.0, 424.0, 18.0), rect: const Rect.fromLTRB(192.0, -4.0, 424.0, 18.0),
...@@ -491,6 +490,46 @@ void main() { ...@@ -491,6 +490,46 @@ void main() {
semantics.dispose(); semantics.dispose();
}, skip: true); // TODO(jonahwilliams): correct once https://github.com/flutter/flutter/issues/20891 is resolved. }, skip: true); // TODO(jonahwilliams): correct once https://github.com/flutter/flutter/issues/20891 is resolved.
testWidgets('TapGesture recognizers contribute link semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const TextStyle textStyle = TextStyle(fontFamily: 'Ahem');
await tester.pumpWidget(
Text.rich(
TextSpan(
children: <TextSpan>[
TextSpan(
text: 'click me',
recognizer: TapGestureRecognizer()..onTap = () { },
),
],
style: textStyle,
),
textDirection: TextDirection.ltr,
),
);
final TestSemantics expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
children: <TestSemantics>[
TestSemantics(
label: 'click me',
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[SemanticsAction.tap],
flags: <SemanticsFlag>[SemanticsFlag.isLink]
),
],
),
],
);
expect(semantics, hasSemantics(
expectedSemantics,
ignoreTransform: true,
ignoreId: true,
ignoreRect: true,
));
semantics.dispose();
});
testWidgets('inline widgets generate semantic nodes', (WidgetTester tester) async { testWidgets('inline widgets generate semantic nodes', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
const TextStyle textStyle = TextStyle(fontFamily: 'Ahem'); const TextStyle textStyle = TextStyle(fontFamily: 'Ahem');
...@@ -534,9 +573,8 @@ void main() { ...@@ -534,9 +573,8 @@ void main() {
TestSemantics( TestSemantics(
label: 'pebble', label: 'pebble',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
actions: <SemanticsAction>[ actions: <SemanticsAction>[SemanticsAction.tap],
SemanticsAction.tap, flags: <SemanticsFlag>[SemanticsFlag.isLink],
],
), ),
TestSemantics( TestSemantics(
label: ' in the ', label: ' in the ',
...@@ -612,9 +650,8 @@ void main() { ...@@ -612,9 +650,8 @@ void main() {
TestSemantics( TestSemantics(
label: 'pebble', label: 'pebble',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
actions: <SemanticsAction>[ actions: <SemanticsAction>[SemanticsAction.tap],
SemanticsAction.tap, flags: <SemanticsFlag>[SemanticsFlag.isLink],
],
rect: const Rect.fromLTRB(52.0, 48.0, 228.0, 84.0), rect: const Rect.fromLTRB(52.0, 48.0, 228.0, 84.0),
), ),
TestSemantics( TestSemantics(
......
...@@ -440,6 +440,7 @@ Matcher matchesSemantics({ ...@@ -440,6 +440,7 @@ Matcher matchesSemantics({
bool isChecked = false, bool isChecked = false,
bool isSelected = false, bool isSelected = false,
bool isButton = false, bool isButton = false,
bool isLink = false,
bool isFocused = false, bool isFocused = false,
bool isTextField = false, bool isTextField = false,
bool isReadOnly = false, bool isReadOnly = false,
...@@ -489,6 +490,7 @@ Matcher matchesSemantics({ ...@@ -489,6 +490,7 @@ Matcher matchesSemantics({
if (isChecked) SemanticsFlag.isChecked, if (isChecked) SemanticsFlag.isChecked,
if (isSelected) SemanticsFlag.isSelected, if (isSelected) SemanticsFlag.isSelected,
if (isButton) SemanticsFlag.isButton, if (isButton) SemanticsFlag.isButton,
if (isLink) SemanticsFlag.isLink,
if (isTextField) SemanticsFlag.isTextField, if (isTextField) SemanticsFlag.isTextField,
if (isReadOnly) SemanticsFlag.isReadOnly, if (isReadOnly) SemanticsFlag.isReadOnly,
if (isFocused) SemanticsFlag.isFocused, if (isFocused) SemanticsFlag.isFocused,
......
...@@ -412,6 +412,7 @@ void main() { ...@@ -412,6 +412,7 @@ void main() {
namesRoute: true, namesRoute: true,
header: true, header: true,
button: true, button: true,
link: true,
onTap: () { }, onTap: () { },
onLongPress: () { }, onLongPress: () { },
label: 'foo', label: 'foo',
...@@ -439,6 +440,7 @@ void main() { ...@@ -439,6 +440,7 @@ void main() {
hasTapAction: true, hasTapAction: true,
hasLongPressAction: true, hasLongPressAction: true,
isButton: true, isButton: true,
isLink: true,
isHeader: true, isHeader: true,
namesRoute: true, namesRoute: true,
onTapHint: 'scan', onTapHint: 'scan',
...@@ -460,6 +462,7 @@ void main() { ...@@ -460,6 +462,7 @@ void main() {
hasTapAction: true, hasTapAction: true,
hasLongPressAction: true, hasLongPressAction: true,
isButton: true, isButton: true,
isLink: true,
isHeader: true, isHeader: true,
namesRoute: true, namesRoute: true,
onTapHint: 'scan', onTapHint: 'scan',
...@@ -481,6 +484,7 @@ void main() { ...@@ -481,6 +484,7 @@ void main() {
hasTapAction: true, hasTapAction: true,
hasLongPressAction: true, hasLongPressAction: true,
isButton: true, isButton: true,
isLink: true,
isHeader: true, isHeader: true,
namesRoute: true, namesRoute: true,
onTapHint: 'scans', onTapHint: 'scans',
...@@ -544,6 +548,7 @@ void main() { ...@@ -544,6 +548,7 @@ void main() {
isChecked: true, isChecked: true,
isSelected: true, isSelected: true,
isButton: true, isButton: true,
isLink: true,
isTextField: true, isTextField: true,
isReadOnly: true, isReadOnly: true,
hasEnabledState: true, hasEnabledState: true,
......
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