Skip to content

Commit

Permalink
[UIAsyncTextInput] Keyboard sometimes autocapitalizes incorrectly whi…
Browse files Browse the repository at this point in the history
…le typing an inline completion

https://bugs.webkit.org/show_bug.cgi?id=265764
rdar://119084774

Reviewed by Richard Robinson.

After adopting `-invalidateTextEntryContext` in place of `-[UIKeyboardImpl layoutHasChanged]` when
the async text input codepath is enabled, the keyboard sometimes unexpectedly autocapitalizes when
typing while showing inline predictions in Mail compose.

This is because the replacement API (`-invalidateTextEntryContext`) now additionally updates text
context in addition to updating the layout and positioning of input method UI (i.e. when using
Chinese or Japanese input). This is meant to only be done only once, when initially starting IME
composition, in order to give certain web apps (e.g. Google Docs) a chance to reposition any
offscreen, hidden editable containers such that the input method UI and candidates bar shows up in
the right place — to achieve this, we set a `_candidateViewNeedsUpdate` flag upon setting any marked
text where the marked text range is previously empty, and clear out the flag after the next
selection change.

In iOS 17+, inline predictions also exercise this same marked text codepath; however, instead of
maintaining a persistent marked text string, inline predictions continuously clear out and reinsert
marked text as the prediction is regenerated while typing, which causes the
`_candidateViewNeedsUpdate` flag to be continuously set (and therefore, causes us to continuously
invoke `-invalidateTextEntryContext` while typing).

The combination of the above causes the keyboard to occasionally lose context while typing, which
results in the keyboard becoming erroneously autoshifted after typing a space. To fix this, we make
this logic more nuanced, such that we only `-invalidateTextEntryContext` if there's an active IME
session (excluding marked text, set by inline predictions).

Test: editing/input/ios/typing-with-inline-predictions.html

* LayoutTests/editing/input/ios/typing-with-inline-predictions-expected.txt: Added.
* LayoutTests/editing/input/ios/typing-with-inline-predictions.html: Added.

Add a layout test to exercise the change.

* Source/WebKit/UIProcess/ios/WKContentViewInteraction.h:

Introduce a new flag, `_isDeferringKeyEventsToInputMethod`, to track whether or not we have an
active IME session.

* Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm:
(-[WKContentView cleanUpInteraction]):
(-[WKContentView canPerformActionForWebView:withSender:]):

Drive-by fix: replace some internal calls to `-hasContent` with an internal version instead,
`-_hasContent`, to avoid self-induced release assertions after 271417@main.

(-[WKContentView _setMarkedText:underlines:highlights:selectedRange:]):
(-[WKContentView unmarkText]):

Unset the new flag when committing marked text.

(-[WKContentView _internalHandleKeyWebEvent:withCompletionHandler:]):

Set the new flag, `_isDeferringKeyEventsToInputMethod`, if we've actually deferred key event
handling to system IME.

(-[WKContentView hasContent]):
(-[WKContentView _hasContent]):

See drive-by fix above.

Canonical link: https://commits.webkit.org/271482@main
  • Loading branch information
whsieh committed Dec 4, 2023
1 parent 1838f3e commit 01a600b
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
This test verifies that typing with inline predictions does not cause the keyboard to incorrectly autocapitalize words. This test requires WebKitTestRunner.

On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".


PASS isAutoShifted is false
PASS input.value is "To whom it may concern"
PASS successfullyParsed is true

TEST COMPLETE

53 changes: 53 additions & 0 deletions LayoutTests/editing/input/ios/typing-with-inline-predictions.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true allowsInlinePredictions=true ] -->
<html>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8">
<head>
<script src="../../../resources/js-test.js"></script>
<script src="../../../resources/ui-helper.js"></script>
<style>
body {
margin: 0;
font-family: system-ui;
line-height: 150%;
}

input {
border: 1px solid tomato;
box-sizing: border-box;
outline: none;
font-size: 18px;
width: 300px;
}
</style>
<script>
jsTestIsAsync = true;

addEventListener("load", async () => {
description("This test verifies that typing with inline predictions does not cause the keyboard to incorrectly autocapitalize words. This test requires WebKitTestRunner.");

input = document.querySelector("input");
await UIHelper.activateElementAndWaitForInputSession(input);

let characterCountBeforeLongPauseForInlinePredictions = 10;
for (let character of [..."to whom it may concern"]) {
await UIHelper.typeCharacter(character);
await UIHelper.ensurePresentationUpdate();
if (!--characterCountBeforeLongPauseForInlinePredictions)
await UIHelper.delayFor(500);
}

isAutoShifted = await UIHelper.keyboardIsAutomaticallyShifted();
shouldBeFalse("isAutoShifted");
shouldBeEqualToString("input.value", "To whom it may concern");

input.blur();
await UIHelper.waitForKeyboardToHide();
finishJSTest();
});
</script>
</head>
<body>
<input />
</body>
</html>
1 change: 1 addition & 0 deletions Source/WebKit/UIProcess/ios/WKContentViewInteraction.h
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ struct ImageAnalysisContextMenuActionData {
BOOL _isPresentingEditMenu;
BOOL _isHandlingActiveKeyEvent;
BOOL _isHandlingActivePressesEvent;
BOOL _isDeferringKeyEventsToInputMethod;

BOOL _focusRequiresStrongPasswordAssistance;
BOOL _waitingForEditDragSnapshot;
Expand Down
16 changes: 13 additions & 3 deletions Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1467,6 +1467,7 @@ - (void)cleanUpInteraction
_treatAsContentEditableUntilNextEditorStateUpdate = NO;
_isHandlingActiveKeyEvent = NO;
_isHandlingActivePressesEvent = NO;
_isDeferringKeyEventsToInputMethod = NO;

if (_interactionViewsContainerView) {
[self.layer removeObserver:self forKeyPath:@"transform" context:WKContentViewKVOTransformContext];
Expand Down Expand Up @@ -4557,7 +4558,7 @@ - (BOOL)canPerformActionForWebView:(SEL)action withSender:(id)sender

if (action == @selector(select:)) {
// Disable select in password fields so that you can't see word boundaries.
return !editorState.isInPasswordField && !editorState.selectionIsRange && self.hasContent;
return !editorState.isInPasswordField && !editorState.selectionIsRange && self._hasContent;
}

auto isPreparingEditMenu = [&] {
Expand All @@ -4567,7 +4568,7 @@ - (BOOL)canPerformActionForWebView:(SEL)action withSender:(id)sender
if (action == @selector(selectAll:)) {
if (isPreparingEditMenu()) {
// By platform convention we don't show Select All in the edit menu for a range selection.
return !editorState.selectionIsRange && self.hasContent;
return !editorState.selectionIsRange && self._hasContent;
}
return YES;
}
Expand Down Expand Up @@ -6305,13 +6306,14 @@ - (void)setMarkedText:(NSString *)markedText selectedRange:(NSRange)selectedRang
- (void)_setMarkedText:(NSString *)markedText underlines:(const Vector<WebCore::CompositionUnderline>&)underlines highlights:(const Vector<WebCore::CompositionHighlight>&)highlights selectedRange:(NSRange)selectedRange
{
_autocorrectionContextNeedsUpdate = YES;
_candidateViewNeedsUpdate = !self.hasMarkedText;
_candidateViewNeedsUpdate = !self.hasMarkedText && _isDeferringKeyEventsToInputMethod;
_markedText = markedText;
_page->setCompositionAsync(markedText, underlines, highlights, { }, selectedRange, { });
}

- (void)unmarkText
{
_isDeferringKeyEventsToInputMethod = NO;
_markedText = nil;
_page->confirmCompositionAsync();
}
Expand Down Expand Up @@ -7150,6 +7152,7 @@ - (void)_internalHandleKeyWebEvent:(::WebEvent *)event withCompletionHandler:(vo
using HandledByInputMethod = WebKit::NativeWebKeyboardEvent::HandledByInputMethod;
if ([self _deferKeyEventToInputMethodEditing:event]) {
completionHandler(event, YES);
_isDeferringKeyEventsToInputMethod = YES;
_page->handleKeyboardEvent(WebKit::NativeWebKeyboardEvent(event, HandledByInputMethod::Yes));
return;
}
Expand Down Expand Up @@ -7528,6 +7531,11 @@ - (BOOL)hasContent
{
RELEASE_ASSERT_ASYNC_TEXT_INTERACTIONS_DISABLED();

return self._hasContent;
}

- (BOOL)_hasContent
{
auto& editorState = _page->editorState();
return !editorState.selectionIsNone && editorState.postLayoutData && editorState.postLayoutData->hasContent;
}
Expand Down Expand Up @@ -8691,6 +8699,8 @@ - (void)_updateChangedSelection:(BOOL)force
// FIXME: We need to figure out what to do if the selection is changed by Javascript.
if (_textInteractionWrapper) {
_markedText = editorState.hasComposition ? postLayoutData.markedText : String { };
if (![_markedText length])
_isDeferringKeyEventsToInputMethod = NO;
[_textInteractionWrapper selectionChanged];
}

Expand Down

0 comments on commit 01a600b

Please sign in to comment.