• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5part of engine;
6
7/// Renders [_label] and [_value] to the semantics DOM.
8///
9/// The rendering method is browser-dependent. There is no explicit ARIA
10/// attribute to express "value". Instead, you are expected to render the
11/// value as text content of HTML.
12///
13/// VoiceOver only supports "aria-label" for certain ARIA roles. For plain
14/// text it expects that the label is part of the text content of the element.
15/// The strategy for VoiceOver is to combine [_label] and [_value] and stamp
16/// out a single child element that contains the value.
17///
18/// TalkBack supports the "aria-label" attribute. However, when present,
19/// TalkBack ignores the text content. Therefore, we cannot split [_label]
20/// and [_value] between "aria-label" and text content. The strategy for
21/// TalkBack is to combine [_label] and [_value] into a single "aria-label".
22///
23/// The [_value] is not always rendered. Some semantics nodes correspond to
24/// interactive controls, such as an `<input>` element. In such case the value
25/// is reported via that element's `value` attribute rather than rendering it
26/// separately.
27///
28/// Aria role image is not managed by this role manager. Img role and label
29/// describes the visual are added in [ImageRoleManager].
30class LabelAndValue extends RoleManager {
31  LabelAndValue(SemanticsObject semanticsObject)
32      : super(Role.labelAndValue, semanticsObject);
33
34  /// Supplements the "aria-label" that renders the combination of [_label] and
35  /// [_value] to semantics as text content.
36  ///
37  /// This extra element is needed for the following reasons:
38  ///
39  /// - VoiceOver on iOS Safari does not recognize standalone "aria-label". It
40  ///   only works for specific roles.
41  /// - TalkBack does support "aria-label". However, if an element has children
42  ///   its label is not reachable via accessibility focus. This happens, for
43  ///   example in popup dialogs, such as the alert dialog. The text of the
44  ///   alert is supplied as a label on the parent node.
45  html.Element _auxiliaryValueElement;
46
47  @override
48  void update() {
49    final bool hasValue = semanticsObject.hasValue;
50    final bool hasLabel = semanticsObject.hasLabel;
51
52    // If the node is incrementable or a text field the value is reported to the
53    // browser via the respective role managers. We do not need to also render
54    // it again here.
55    final bool shouldDisplayValue = hasValue &&
56        !semanticsObject.isIncrementable &&
57        !semanticsObject.isTextField;
58
59    if (!hasLabel && !shouldDisplayValue) {
60      _cleanUpDom();
61      return;
62    }
63
64    final StringBuffer combinedValue = StringBuffer();
65    if (hasLabel) {
66      combinedValue.write(semanticsObject.label);
67      if (shouldDisplayValue) {
68        combinedValue.write(' ');
69      }
70    }
71
72    if (shouldDisplayValue) {
73      combinedValue.write(semanticsObject.value);
74    }
75
76    semanticsObject.element
77        .setAttribute('aria-label', combinedValue.toString());
78
79    if (semanticsObject.hasFlag(ui.SemanticsFlag.isHeader)) {
80      semanticsObject.setAriaRole('heading', true);
81    }
82
83    if (_auxiliaryValueElement == null) {
84      _auxiliaryValueElement = html.Element.tag('flt-semantics-value');
85      // Absolute positioning and sizing of leaf text elements confuses
86      // VoiceOver. So we let the browser size the value node. The node will
87      // still have a bigger tap area. However, if the node is a parent to other
88      // nodes, then VoiceOver behaves as expected with absolute positioning and
89      // sizing.
90      if (semanticsObject.hasChildren) {
91        _auxiliaryValueElement.style
92          ..position = 'absolute'
93          ..top = '0'
94          ..left = '0'
95          ..width = '${semanticsObject.rect.width}px'
96          ..height = '${semanticsObject.rect.height}px';
97      }
98      _auxiliaryValueElement.style.fontSize = '6px';
99      semanticsObject.element.append(_auxiliaryValueElement);
100    }
101    _auxiliaryValueElement.text = combinedValue.toString();
102  }
103
104  void _cleanUpDom() {
105    if (_auxiliaryValueElement != null) {
106      _auxiliaryValueElement.remove();
107      _auxiliaryValueElement = null;
108    }
109    semanticsObject.element.attributes.remove('aria-label');
110    semanticsObject.setAriaRole('heading', false);
111  }
112
113  @override
114  void dispose() {
115    _cleanUpDom();
116  }
117}
118