• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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/// Manages semantics objects that represent editable text fields.
8///
9/// This role is implemented via a content-editable HTML element. This role does
10/// not proactively switch modes depending on the current
11/// [EngineSemanticsOwner.gestureMode]. However, in Chrome on Android it ignores
12/// browser gestures when in pointer mode. In Safari on iOS touch events are
13/// used to detect text box invocation. This is because Safari issues touch
14/// events even when Voiceover is enabled.
15class TextField extends RoleManager {
16  TextField(SemanticsObject semanticsObject)
17      : super(Role.textField, semanticsObject) {
18    final html.HtmlElement editableDomElement =
19        semanticsObject.hasFlag(ui.SemanticsFlag.isMultiline)
20            ? html.TextAreaElement()
21            : html.InputElement();
22    persistentTextEditingElement = PersistentTextEditingElement(
23      editableDomElement,
24      onDomElementSwap: _setupDomElement,
25    );
26    _setupDomElement();
27  }
28
29  PersistentTextEditingElement persistentTextEditingElement;
30  html.Element get _textFieldElement => persistentTextEditingElement.domElement;
31
32  void _setupDomElement() {
33    // On iOS, even though the semantic text field is transparent, the cursor
34    // and text highlighting are still visible. The cursor and text selection
35    // are made invisible by CSS in [DomRenderer.reset].
36    // But there's one more case where iOS highlights text. That's when there's
37    // and autocorrect suggestion. To disable that, we have to do the following:
38    _textFieldElement
39      ..spellcheck = false
40      ..setAttribute('spellcheck', 'false')
41      ..setAttribute('autocorrect', 'off')
42      ..setAttribute('autocomplete', 'off')
43      ..setAttribute('data-semantics-role', 'text-field');
44
45    _textFieldElement.style
46      ..position = 'absolute'
47      // `top` and `left` are intentionally set to zero here.
48      //
49      // The text field would live inside a `<flt-semantics>` which should
50      // already be positioned using semantics.rect.
51      //
52      // See also:
53      //
54      // * [SemanticsObject.recomputePositionAndSize], which sets the position
55      //   and size of the parent `<flt-semantics>` element.
56      ..top = '0'
57      ..left = '0'
58      ..width = '${semanticsObject.rect.width}px'
59      ..height = '${semanticsObject.rect.height}px';
60    semanticsObject.element.append(_textFieldElement);
61
62    switch (browserEngine) {
63      case BrowserEngine.blink:
64      case BrowserEngine.unknown:
65        _initializeForBlink();
66        break;
67      case BrowserEngine.webkit:
68        _initializeForWebkit();
69        break;
70    }
71  }
72
73  /// Chrome on Android reports text field activation as a "click" event.
74  ///
75  /// When in browser gesture mode, the focus is forwarded to the framework as
76  /// a tap to initialize editing.
77  void _initializeForBlink() {
78    _textFieldElement.addEventListener('focus', (html.Event event) {
79      if (semanticsObject.owner.gestureMode != GestureMode.browserGestures) {
80        return;
81      }
82
83      textEditing.useCustomEditableElement(persistentTextEditingElement);
84      ui.window
85          .onSemanticsAction(semanticsObject.id, ui.SemanticsAction.tap, null);
86    });
87  }
88
89  /// Safari on iOS reports text field activation via touch events.
90  ///
91  /// This emulates a tap recognizer to detect the activation. Because touch
92  /// events are present regardless of whether accessibility is enabled or not,
93  /// this mode is always enabled.
94  void _initializeForWebkit() {
95    num lastTouchStartOffsetX;
96    num lastTouchStartOffsetY;
97
98    _textFieldElement.addEventListener('touchstart', (html.Event event) {
99      textEditing.useCustomEditableElement(persistentTextEditingElement);
100      final html.TouchEvent touchEvent = event;
101      lastTouchStartOffsetX = touchEvent.changedTouches.last.client.x;
102      lastTouchStartOffsetY = touchEvent.changedTouches.last.client.y;
103    }, true);
104
105    _textFieldElement.addEventListener('touchend', (html.Event event) {
106      final html.TouchEvent touchEvent = event;
107
108      if (lastTouchStartOffsetX != null) {
109        assert(lastTouchStartOffsetY != null);
110        final num offsetX = touchEvent.changedTouches.last.client.x;
111        final num offsetY = touchEvent.changedTouches.last.client.y;
112
113        // This should match the similar constant define in:
114        //
115        // lib/src/gestures/constants.dart
116        //
117        // The value is pre-squared so we have to do less math at runtime.
118        const double kTouchSlop = 18.0 * 18.0; // Logical pixels squared
119
120        if (offsetX * offsetX + offsetY * offsetY < kTouchSlop) {
121          // Recognize it as a tap that requires a keyboard.
122          ui.window.onSemanticsAction(
123              semanticsObject.id, ui.SemanticsAction.tap, null);
124        }
125      } else {
126        assert(lastTouchStartOffsetY == null);
127      }
128
129      lastTouchStartOffsetX = null;
130      lastTouchStartOffsetY = null;
131    }, true);
132  }
133
134  @override
135  void update() {
136    // The user is editing the semantic text field directly, so there's no need
137    // to do any update here.
138  }
139
140  @override
141  void dispose() {
142    _textFieldElement.remove();
143    textEditing.stopUsingCustomEditableElement();
144  }
145}
146