blob: de78e08ee6d4b9796b969951873874c0709938bb [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.6
part of engine;
/// Renders [_label] and [_value] to the semantics DOM.
///
/// The rendering method is browser-dependent. There is no explicit ARIA
/// attribute to express "value". Instead, you are expected to render the
/// value as text content of HTML.
///
/// VoiceOver only supports "aria-label" for certain ARIA roles. For plain
/// text it expects that the label is part of the text content of the element.
/// The strategy for VoiceOver is to combine [_label] and [_value] and stamp
/// out a single child element that contains the value.
///
/// TalkBack supports the "aria-label" attribute. However, when present,
/// TalkBack ignores the text content. Therefore, we cannot split [_label]
/// and [_value] between "aria-label" and text content. The strategy for
/// TalkBack is to combine [_label] and [_value] into a single "aria-label".
///
/// The [_value] is not always rendered. Some semantics nodes correspond to
/// interactive controls, such as an `<input>` element. In such case the value
/// is reported via that element's `value` attribute rather than rendering it
/// separately.
///
/// Aria role image is not managed by this role manager. Img role and label
/// describes the visual are added in [ImageRoleManager].
class LabelAndValue extends RoleManager {
LabelAndValue(SemanticsObject semanticsObject)
: super(Role.labelAndValue, semanticsObject);
/// Supplements the "aria-label" that renders the combination of [_label] and
/// [_value] to semantics as text content.
///
/// This extra element is needed for the following reasons:
///
/// - VoiceOver on iOS Safari does not recognize standalone "aria-label". It
/// only works for specific roles.
/// - TalkBack does support "aria-label". However, if an element has children
/// its label is not reachable via accessibility focus. This happens, for
/// example in popup dialogs, such as the alert dialog. The text of the
/// alert is supplied as a label on the parent node.
html.Element _auxiliaryValueElement;
@override
void update() {
final bool hasValue = semanticsObject.hasValue;
final bool hasLabel = semanticsObject.hasLabel;
// If the node is incrementable or a text field the value is reported to the
// browser via the respective role managers. We do not need to also render
// it again here.
final bool shouldDisplayValue = hasValue &&
!semanticsObject.isIncrementable &&
!semanticsObject.isTextField;
if (!hasLabel && !shouldDisplayValue) {
_cleanUpDom();
return;
}
final StringBuffer combinedValue = StringBuffer();
if (hasLabel) {
combinedValue.write(semanticsObject.label);
if (shouldDisplayValue) {
combinedValue.write(' ');
}
}
if (shouldDisplayValue) {
combinedValue.write(semanticsObject.value);
}
semanticsObject.element
.setAttribute('aria-label', combinedValue.toString());
if (semanticsObject.hasFlag(ui.SemanticsFlag.isHeader)) {
semanticsObject.setAriaRole('heading', true);
}
if (_auxiliaryValueElement == null) {
_auxiliaryValueElement = html.Element.tag('flt-semantics-value');
// Absolute positioning and sizing of leaf text elements confuses
// VoiceOver. So we let the browser size the value node. The node will
// still have a bigger tap area. However, if the node is a parent to other
// nodes, then VoiceOver behaves as expected with absolute positioning and
// sizing.
if (semanticsObject.hasChildren) {
_auxiliaryValueElement.style
..position = 'absolute'
..top = '0'
..left = '0'
..width = '${semanticsObject.rect.width}px'
..height = '${semanticsObject.rect.height}px';
}
_auxiliaryValueElement.style.fontSize = '6px';
semanticsObject.element.append(_auxiliaryValueElement);
}
_auxiliaryValueElement.text = combinedValue.toString();
}
void _cleanUpDom() {
if (_auxiliaryValueElement != null) {
_auxiliaryValueElement.remove();
_auxiliaryValueElement = null;
}
semanticsObject.element.attributes.remove('aria-label');
semanticsObject.setAriaRole('heading', false);
}
@override
void dispose() {
_cleanUpDom();
}
}