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/// Adds increment/decrement event handling to a semantics object. 8/// 9/// The implementation uses a hidden `<input type="range">` element with ARIA 10/// attributes to cause the browser to render increment/decrement controls to 11/// the assistive technology. 12/// 13/// The input element is disabled whenever the gesture mode switches to pointer 14/// events. This is to prevent the browser from taking over drag gestures. Drag 15/// gestures must be interpreted by the Flutter framework. 16class Incrementable extends RoleManager { 17 /// The HTML element used to render semantics to the browser. 18 final html.InputElement _element = html.InputElement(); 19 20 /// The value used by the input element. 21 /// 22 /// Flutter values are strings, and are not necessarily numbers. In order to 23 /// convey to the browser what the available "range" of values is we 24 /// substitute the framework value with a generated `int` surrogate. 25 /// "aria-valuetext" attribute is used to cause the browser to announce the 26 /// framework value to the user. 27 int _currentSurrogateValue = 1; 28 29 /// Disables the input [_element] when the gesture mode switches to 30 /// [GestureMode.pointerEvents], and enables it when the mode switches back to 31 /// [GestureMode.browserGestures]. 32 GestureModeCallback _gestureModeListener; 33 34 /// Whether we forwarded a semantics action to the framework and awaiting an 35 /// update. 36 /// 37 /// This field is used to determine whether the HTML DOM of the semantics 38 /// tree should be updated. 39 bool _pendingResync = false; 40 41 Incrementable(SemanticsObject semanticsObject) 42 : super(Role.incrementable, semanticsObject) { 43 semanticsObject.element.append(_element); 44 _element.type = 'range'; 45 _element.setAttribute('role', 'slider'); 46 47 _element.addEventListener('change', (_) { 48 if (_element.disabled) { 49 return; 50 } 51 _pendingResync = true; 52 final int newInputValue = int.parse(_element.value); 53 if (newInputValue > _currentSurrogateValue) { 54 _currentSurrogateValue += 1; 55 ui.window.onSemanticsAction( 56 semanticsObject.id, ui.SemanticsAction.increase, null); 57 } else if (newInputValue < _currentSurrogateValue) { 58 _currentSurrogateValue -= 1; 59 ui.window.onSemanticsAction( 60 semanticsObject.id, ui.SemanticsAction.decrease, null); 61 } 62 }); 63 64 // Store the callback as a closure because Dart does not guarantee that 65 // tear-offs produce the same function object. 66 _gestureModeListener = (GestureMode mode) { 67 update(); 68 }; 69 semanticsObject.owner.addGestureModeListener(_gestureModeListener); 70 } 71 72 @override 73 void update() { 74 switch (semanticsObject.owner.gestureMode) { 75 case GestureMode.browserGestures: 76 _enableBrowserGestureHandling(); 77 _updateInputValues(); 78 break; 79 case GestureMode.pointerEvents: 80 _disableBrowserGestureHandling(); 81 break; 82 } 83 } 84 85 void _enableBrowserGestureHandling() { 86 assert(semanticsObject.owner.gestureMode == GestureMode.browserGestures); 87 if (!_element.disabled) { 88 return; 89 } 90 _element.disabled = false; 91 } 92 93 void _updateInputValues() { 94 assert(semanticsObject.owner.gestureMode == GestureMode.browserGestures); 95 96 final bool updateNeeded = _pendingResync || 97 semanticsObject.isValueDirty || 98 semanticsObject.isIncreasedValueDirty || 99 semanticsObject.isDecreasedValueDirty; 100 101 if (!updateNeeded) { 102 return; 103 } 104 105 _pendingResync = false; 106 107 final String surrogateTextValue = '$_currentSurrogateValue'; 108 _element.value = surrogateTextValue; 109 _element.setAttribute('aria-valuenow', surrogateTextValue); 110 _element.setAttribute('aria-valuetext', semanticsObject.value); 111 112 final bool canIncrease = semanticsObject.increasedValue != null; 113 final String surrogateMaxTextValue = 114 canIncrease ? '${_currentSurrogateValue + 1}' : surrogateTextValue; 115 _element.max = surrogateMaxTextValue; 116 _element.setAttribute('aria-valuemax', surrogateMaxTextValue); 117 118 final bool canDecrease = semanticsObject.decreasedValue != null; 119 final String surrogateMinTextValue = 120 canDecrease ? '${_currentSurrogateValue - 1}' : surrogateTextValue; 121 _element.min = surrogateMinTextValue; 122 _element.setAttribute('aria-valuemin', surrogateMinTextValue); 123 } 124 125 void _disableBrowserGestureHandling() { 126 if (_element.disabled) { 127 return; 128 } 129 _element.disabled = true; 130 } 131 132 @override 133 void dispose() { 134 assert(_gestureModeListener != null); 135 semanticsObject.owner.removeGestureModeListener(_gestureModeListener); 136 _gestureModeListener = null; 137 _disableBrowserGestureHandling(); 138 _element.remove(); 139 } 140} 141