blob: 83ad32fd6a005b4180f1ad716b14eac44d314651 [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.
import 'package:ui/ui.dart' as ui;
import '../configuration.dart';
import '../dom.dart';
import 'semantics.dart';
/// 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.
DomElement? _auxiliaryValueElement;
@override
void update() {
final bool hasValue = semanticsObject.hasValue;
final bool hasLabel = semanticsObject.hasLabel;
final bool hasTooltip = semanticsObject.hasTooltip;
// If the node is incrementable the value is reported to the browser via
// the respective role manager. We do not need to also render it again here.
final bool shouldDisplayValue = hasValue && !semanticsObject.isIncrementable;
if (!hasLabel && !shouldDisplayValue && !hasTooltip) {
_cleanUpDom();
return;
}
final StringBuffer combinedValue = StringBuffer();
if (hasTooltip) {
combinedValue.write(semanticsObject.tooltip);
if (hasLabel || shouldDisplayValue) {
combinedValue.write('\n');
}
}
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 = domDocument.createElement('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';
}
// Normally use a small font size so that text doesn't leave the scope
// of the semantics node. When debugging semantics, use a font size
// that's reasonably visible.
_auxiliaryValueElement!.style.fontSize = configuration.debugShowSemanticsNodes ? '12px' : '6px';
semanticsObject.element.append(_auxiliaryValueElement!);
}
_auxiliaryValueElement!.text = combinedValue.toString();
}
void _cleanUpDom() {
if (_auxiliaryValueElement != null) {
_auxiliaryValueElement!.remove();
_auxiliaryValueElement = null;
}
semanticsObject.element.removeAttribute('aria-label');
semanticsObject.setAriaRole('heading', false);
}
@override
void dispose() {
_cleanUpDom();
}
}