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