blob: 5ae975aa4fd1fd5495825ff3440a03d5e1540a8d [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;
/// Adds increment/decrement event handling to a semantics object.
///
/// The implementation uses a hidden `<input type="range">` element with ARIA
/// attributes to cause the browser to render increment/decrement controls to
/// the assistive technology.
///
/// The input element is disabled whenever the gesture mode switches to pointer
/// events. This is to prevent the browser from taking over drag gestures. Drag
/// gestures must be interpreted by the Flutter framework.
class Incrementable extends RoleManager {
/// The HTML element used to render semantics to the browser.
final html.InputElement _element = html.InputElement();
/// The value used by the input element.
///
/// Flutter values are strings, and are not necessarily numbers. In order to
/// convey to the browser what the available "range" of values is we
/// substitute the framework value with a generated `int` surrogate.
/// "aria-valuetext" attribute is used to cause the browser to announce the
/// framework value to the user.
int _currentSurrogateValue = 1;
/// Disables the input [_element] when the gesture mode switches to
/// [GestureMode.pointerEvents], and enables it when the mode switches back to
/// [GestureMode.browserGestures].
GestureModeCallback _gestureModeListener;
/// Whether we forwarded a semantics action to the framework and awaiting an
/// update.
///
/// This field is used to determine whether the HTML DOM of the semantics
/// tree should be updated.
bool _pendingResync = false;
Incrementable(SemanticsObject semanticsObject)
: super(Role.incrementable, semanticsObject) {
semanticsObject.element.append(_element);
_element.type = 'range';
_element.setAttribute('role', 'slider');
_element.addEventListener('change', (_) {
if (_element.disabled) {
return;
}
_pendingResync = true;
final int newInputValue = int.parse(_element.value);
if (newInputValue > _currentSurrogateValue) {
_currentSurrogateValue += 1;
window.invokeOnSemanticsAction(
semanticsObject.id, ui.SemanticsAction.increase, null);
} else if (newInputValue < _currentSurrogateValue) {
_currentSurrogateValue -= 1;
window.invokeOnSemanticsAction(
semanticsObject.id, ui.SemanticsAction.decrease, null);
}
});
// Store the callback as a closure because Dart does not guarantee that
// tear-offs produce the same function object.
_gestureModeListener = (GestureMode mode) {
update();
};
semanticsObject.owner.addGestureModeListener(_gestureModeListener);
}
@override
void update() {
switch (semanticsObject.owner.gestureMode) {
case GestureMode.browserGestures:
_enableBrowserGestureHandling();
_updateInputValues();
break;
case GestureMode.pointerEvents:
_disableBrowserGestureHandling();
break;
}
}
void _enableBrowserGestureHandling() {
assert(semanticsObject.owner.gestureMode == GestureMode.browserGestures);
if (!_element.disabled) {
return;
}
_element.disabled = false;
}
void _updateInputValues() {
assert(semanticsObject.owner.gestureMode == GestureMode.browserGestures);
final bool updateNeeded = _pendingResync ||
semanticsObject.isValueDirty ||
semanticsObject.isIncreasedValueDirty ||
semanticsObject.isDecreasedValueDirty;
if (!updateNeeded) {
return;
}
_pendingResync = false;
final String surrogateTextValue = '$_currentSurrogateValue';
_element.value = surrogateTextValue;
_element.setAttribute('aria-valuenow', surrogateTextValue);
_element.setAttribute('aria-valuetext', semanticsObject.value);
final bool canIncrease = semanticsObject.increasedValue != null;
final String surrogateMaxTextValue =
canIncrease ? '${_currentSurrogateValue + 1}' : surrogateTextValue;
_element.max = surrogateMaxTextValue;
_element.setAttribute('aria-valuemax', surrogateMaxTextValue);
final bool canDecrease = semanticsObject.decreasedValue != null;
final String surrogateMinTextValue =
canDecrease ? '${_currentSurrogateValue - 1}' : surrogateTextValue;
_element.min = surrogateMinTextValue;
_element.setAttribute('aria-valuemin', surrogateMinTextValue);
}
void _disableBrowserGestureHandling() {
if (_element.disabled) {
return;
}
_element.disabled = true;
}
@override
void dispose() {
assert(_gestureModeListener != null);
semanticsObject.owner.removeGestureModeListener(_gestureModeListener);
_gestureModeListener = null;
_disableBrowserGestureHandling();
_element.remove();
}
}