diff --git a/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml b/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml index 7ee965c3cf14b..876215ea6ba28 100644 --- a/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml +++ b/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml @@ -8326,6 +8326,21 @@ WriteRichTextDataWhenCopyingOrDragging: WebCore: default: true +WritingSuggestionsAttributeEnabled: + type: bool + status: stable + category: dom + humanReadableName: "Writing Suggestions" + humanReadableDescription: "Enable the writingsuggestions attribute" + condition: ENABLE(WRITING_SUGGESTIONS) + defaultValue: + WebKitLegacy: + default: true + WebKit: + default: true + WebCore: + default: true + ZoomOnDoubleTapWhenRoot: type: bool status: internal diff --git a/Source/WTF/wtf/PlatformEnable.h b/Source/WTF/wtf/PlatformEnable.h index 88f2ed08021f9..f2db0b5dc3935 100644 --- a/Source/WTF/wtf/PlatformEnable.h +++ b/Source/WTF/wtf/PlatformEnable.h @@ -995,3 +995,8 @@ && USE(LINEARMEDIAKIT) #define ENABLE_LINEAR_MEDIA_PLAYER 0 #endif + +#if !defined(ENABLE_WRITING_SUGGESTIONS) \ + && (PLATFORM(COCOA) && HAVE(INLINE_PREDICTIONS)) +#define ENABLE_WRITING_SUGGESTIONS 1 +#endif diff --git a/Source/WebCore/dom/Element.cpp b/Source/WebCore/dom/Element.cpp index 29ab5763cac38..eb6c5649df0e2 100644 --- a/Source/WebCore/dom/Element.cpp +++ b/Source/WebCore/dom/Element.cpp @@ -80,8 +80,10 @@ #include "HTMLScriptElement.h" #include "HTMLSelectElement.h" #include "HTMLTemplateElement.h" +#include "HTMLTextAreaElement.h" #include "IdChangeInvalidation.h" #include "IdTargetObserverRegistry.h" +#include "InputType.h" #include "InspectorInstrumentation.h" #include "JSDOMPromiseDeferred.h" #include "JSLazyEventListener.h" @@ -4752,6 +4754,53 @@ bool Element::isSpellCheckingEnabled() const return true; } +bool Element::isWritingSuggestionsEnabled() const +{ + // If none of the following conditions are true, then return `false`. + + // `element` is an `input` element whose `type` attribute is in either the + // `Text`, `Search`, `URL`, `Email` state and is `mutable`. + auto isEligibleInputElement = [&] { + RefPtr input = dynamicDowncast(*this); + if (!input) + return false; + + return !input->isDisabledFormControl() && input->supportsWritingSuggestions(); + }; + + // `element` is a `textarea` element that is `mutable`. + auto isEligibleTextArea = [&] { + RefPtr textArea = dynamicDowncast(*this); + if (!textArea) + return false; + + return !textArea->isDisabledFormControl(); + }; + + // `element` is an `editing host` or is `editable`. + + if (!isEligibleInputElement() && !isEligibleTextArea() && !hasEditableStyle()) + return false; + + // If `element` has an 'inclusive ancestor' with a `writingsuggestions` content attribute that's + // not in the `default` state and the nearest such ancestor's `writingsuggestions` content attribute + // is in the `false` state, then return `false`. + + for (auto* ancestor = this; ancestor; ancestor = ancestor->parentElementInComposedTree()) { + auto& value = ancestor->attributeWithoutSynchronization(HTMLNames::writingsuggestionsAttr); + + if (value.isNull()) + continue; + if (value.isEmpty() || equalLettersIgnoringASCIICase(value, "true"_s)) + return true; + if (equalLettersIgnoringASCIICase(value, "false"_s)) + return false; + } + + // Otherwise, return `true`. + return true; +} + #if ASSERT_ENABLED bool Element::fastAttributeLookupAllowed(const QualifiedName& name) const { diff --git a/Source/WebCore/dom/Element.h b/Source/WebCore/dom/Element.h index a510ddb99f81e..1d74cdd3923c9 100644 --- a/Source/WebCore/dom/Element.h +++ b/Source/WebCore/dom/Element.h @@ -629,6 +629,7 @@ class Element : public ContainerNode { #endif bool isSpellCheckingEnabled() const; + WEBCORE_EXPORT bool isWritingSuggestionsEnabled() const; inline bool hasID() const; inline bool hasClass() const; diff --git a/Source/WebCore/editing/VisibleSelection.cpp b/Source/WebCore/editing/VisibleSelection.cpp index 69a8dd3bc327d..43d8f6216acf2 100644 --- a/Source/WebCore/editing/VisibleSelection.cpp +++ b/Source/WebCore/editing/VisibleSelection.cpp @@ -687,6 +687,21 @@ bool VisibleSelection::isInPasswordField() const return textControl && textControl->isPasswordField(); } +bool VisibleSelection::canEnableWritingSuggestions() const +{ + RefPtr containerNode = start().containerNode(); + if (!containerNode) + return false; + + if (RefPtr element = dynamicDowncast(containerNode.get())) + return element->isWritingSuggestionsEnabled(); + + if (RefPtr element = containerNode->parentElement()) + return element->isWritingSuggestionsEnabled(); + + return false; +} + bool VisibleSelection::isInAutoFilledAndViewableField() const { if (RefPtr input = dynamicDowncast(enclosingTextFormControl(start()))) diff --git a/Source/WebCore/editing/VisibleSelection.h b/Source/WebCore/editing/VisibleSelection.h index 60c9998df33fe..0717b381a1441 100644 --- a/Source/WebCore/editing/VisibleSelection.h +++ b/Source/WebCore/editing/VisibleSelection.h @@ -118,6 +118,8 @@ class VisibleSelection { WEBCORE_EXPORT bool isInPasswordField() const; WEBCORE_EXPORT bool isInAutoFilledAndViewableField() const; + WEBCORE_EXPORT bool canEnableWritingSuggestions() const; + WEBCORE_EXPORT static Position adjustPositionForEnd(const Position& currentPosition, Node* startContainerNode); WEBCORE_EXPORT static Position adjustPositionForStart(const Position& currentPosition, Node* startContainerNode); diff --git a/Source/WebCore/html/HTMLAttributeNames.in b/Source/WebCore/html/HTMLAttributeNames.in index dc81afe2f24bf..b53f68f2216c3 100644 --- a/Source/WebCore/html/HTMLAttributeNames.in +++ b/Source/WebCore/html/HTMLAttributeNames.in @@ -431,6 +431,7 @@ webkitdirectory webkitdropzone width wrap +writingsuggestions x-apple-data-detectors x-apple-data-detectors-result x-apple-data-detectors-type diff --git a/Source/WebCore/html/HTMLElement.cpp b/Source/WebCore/html/HTMLElement.cpp index 27e8cad0ea533..e46ec7fd3876e 100644 --- a/Source/WebCore/html/HTMLElement.cpp +++ b/Source/WebCore/html/HTMLElement.cpp @@ -731,6 +731,16 @@ void HTMLElement::setSpellcheck(bool enable) setAttributeWithoutSynchronization(spellcheckAttr, enable ? trueAtom() : falseAtom()); } +bool HTMLElement::writingsuggestions() const +{ + return isWritingSuggestionsEnabled(); +} + +void HTMLElement::setWritingsuggestions(bool enable) +{ + setAttributeWithoutSynchronization(writingsuggestionsAttr, enable ? trueAtom() : falseAtom()); +} + void HTMLElement::effectiveSpellcheckAttributeChanged(bool newValue) { for (auto it = descendantsOfType(*this).begin(); it;) { diff --git a/Source/WebCore/html/HTMLElement.h b/Source/WebCore/html/HTMLElement.h index 1b2aff9aee73b..01ee460c27754 100644 --- a/Source/WebCore/html/HTMLElement.h +++ b/Source/WebCore/html/HTMLElement.h @@ -84,6 +84,9 @@ class HTMLElement : public StyledElement { WEBCORE_EXPORT bool spellcheck() const; WEBCORE_EXPORT void setSpellcheck(bool); + WEBCORE_EXPORT bool writingsuggestions() const; + WEBCORE_EXPORT void setWritingsuggestions(bool); + WEBCORE_EXPORT bool translate() const; WEBCORE_EXPORT void setTranslate(bool); diff --git a/Source/WebCore/html/HTMLElement.idl b/Source/WebCore/html/HTMLElement.idl index 755368a5ab84a..d6aaf324d5b59 100644 --- a/Source/WebCore/html/HTMLElement.idl +++ b/Source/WebCore/html/HTMLElement.idl @@ -69,6 +69,8 @@ // Non-standard: We are the only browser to support this now that Blink dropped it (http://crbug.com/688943). [CEReactions=Needed, Reflect] attribute DOMString webkitdropzone; + + [Conditional=WRITING_SUGGESTIONS, CEReactions=Needed] attribute boolean writingsuggestions; }; HTMLElement includes GlobalEventHandlers; diff --git a/Source/WebCore/html/HTMLInputElement.cpp b/Source/WebCore/html/HTMLInputElement.cpp index 426db6c96a71c..5109ff1cc0879 100644 --- a/Source/WebCore/html/HTMLInputElement.cpp +++ b/Source/WebCore/html/HTMLInputElement.cpp @@ -1039,6 +1039,18 @@ bool HTMLInputElement::isTextType() const return m_inputType->isTextType(); } +bool HTMLInputElement::supportsWritingSuggestions() const +{ + static constexpr OptionSet allowedTypes = { + InputType::Type::Text, + InputType::Type::Search, + InputType::Type::URL, + InputType::Type::Email, + }; + + return allowedTypes.contains(m_inputType->type()); +} + void HTMLInputElement::setDefaultCheckedState(bool isDefaultChecked) { if (m_isDefaultChecked == isDefaultChecked) diff --git a/Source/WebCore/html/HTMLInputElement.h b/Source/WebCore/html/HTMLInputElement.h index f511366a778ae..4352fab7d2432 100644 --- a/Source/WebCore/html/HTMLInputElement.h +++ b/Source/WebCore/html/HTMLInputElement.h @@ -157,6 +157,7 @@ class HTMLInputElement : public HTMLTextFormControlElement { // isTextField && !isPasswordField. WEBCORE_EXPORT bool isText() const; bool isTextType() const; + bool supportsWritingSuggestions() const; WEBCORE_EXPORT bool isEmailField() const; WEBCORE_EXPORT bool isFileUpload() const; bool isImageButton() const; diff --git a/Source/WebKit/Shared/EditorState.cpp b/Source/WebKit/Shared/EditorState.cpp index ecb5844f9aa90..0e7ab2e8c0c4b 100644 --- a/Source/WebKit/Shared/EditorState.cpp +++ b/Source/WebKit/Shared/EditorState.cpp @@ -75,6 +75,8 @@ TextStream& operator<<(TextStream& ts, const EditorState& editorState) ts.dumpProperty("enclosingListType", enumToUnderlyingType(editorState.postLayoutData->enclosingListType)); if (editorState.postLayoutData->baseWritingDirection != WebCore::WritingDirection::Natural) ts.dumpProperty("baseWritingDirection", static_cast(editorState.postLayoutData->baseWritingDirection)); + if (editorState.postLayoutData->canEnableWritingSuggestions) + ts.dumpProperty("canEnableWritingSuggestions", editorState.postLayoutData->canEnableWritingSuggestions); #endif // PLATFORM(COCOA) #if PLATFORM(IOS_FAMILY) if (editorState.postLayoutData->markedText.length()) diff --git a/Source/WebKit/Shared/EditorState.h b/Source/WebKit/Shared/EditorState.h index 907d8d53b88e4..7f8104aded87e 100644 --- a/Source/WebKit/Shared/EditorState.h +++ b/Source/WebKit/Shared/EditorState.h @@ -95,6 +95,7 @@ struct EditorState { ListType enclosingListType { ListType::None }; WebCore::WritingDirection baseWritingDirection { WebCore::WritingDirection::Natural }; bool editableRootIsTransparentOrFullyClipped { false }; + bool canEnableWritingSuggestions { false }; #endif #if PLATFORM(IOS_FAMILY) String markedText; diff --git a/Source/WebKit/Shared/EditorState.serialization.in b/Source/WebKit/Shared/EditorState.serialization.in index d2f2a49055783..24bf05b03dec6 100644 --- a/Source/WebKit/Shared/EditorState.serialization.in +++ b/Source/WebKit/Shared/EditorState.serialization.in @@ -70,6 +70,7 @@ struct WebKit::EditorState { WebKit::ListType enclosingListType; WebCore::WritingDirection baseWritingDirection; bool editableRootIsTransparentOrFullyClipped; + bool canEnableWritingSuggestions; #endif #if PLATFORM(IOS_FAMILY) String markedText; diff --git a/Source/WebKit/Shared/FocusedElementInformation.h b/Source/WebKit/Shared/FocusedElementInformation.h index 010cc2ed9e03d..c70cb6fa6bb59 100644 --- a/Source/WebKit/Shared/FocusedElementInformation.h +++ b/Source/WebKit/Shared/FocusedElementInformation.h @@ -134,6 +134,7 @@ struct FocusedElementInformation { bool hasEverBeenPasswordField { false }; bool shouldSynthesizeKeyEventsForEditing { false }; bool isSpellCheckingEnabled { true }; + bool isWritingSuggestionsEnabled { false }; bool shouldAvoidResizingWhenInputViewBoundsChange { false }; bool shouldAvoidScrollingWhenFocusedContentIsVisible { false }; bool shouldUseLegacySelectPopoverDismissalBehaviorInDataActivation { false }; diff --git a/Source/WebKit/Shared/FocusedElementInformation.serialization.in b/Source/WebKit/Shared/FocusedElementInformation.serialization.in index 5e899fd6fa7d3..f8cd77fb21e9d 100644 --- a/Source/WebKit/Shared/FocusedElementInformation.serialization.in +++ b/Source/WebKit/Shared/FocusedElementInformation.serialization.in @@ -104,6 +104,7 @@ enum class WebKit::InputType : uint8_t { bool hasEverBeenPasswordField; bool shouldSynthesizeKeyEventsForEditing; bool isSpellCheckingEnabled; + bool isWritingSuggestionsEnabled; bool shouldAvoidResizingWhenInputViewBoundsChange; bool shouldAvoidScrollingWhenFocusedContentIsVisible; bool shouldUseLegacySelectPopoverDismissalBehaviorInDataActivation; diff --git a/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm b/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm index 2045dcb3cff72..4e78e32743107 100644 --- a/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm +++ b/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm @@ -6915,7 +6915,7 @@ - (void)_updateTextInputTraits:(id)traits privateTraits.shortcutConversionType = _focusedElementInformation.elementType == WebKit::InputType::Password ? UITextShortcutConversionTypeNo : UITextShortcutConversionTypeDefault; #if HAVE(INLINE_PREDICTIONS) - traits.inlinePredictionType = (self.webView.configuration.allowsInlinePredictions || _page->preferences().inlinePredictionsInAllEditableElementsEnabled()) ? UITextInlinePredictionTypeDefault : UITextInlinePredictionTypeNo; + traits.inlinePredictionType = (self.webView.configuration.allowsInlinePredictions || _page->preferences().inlinePredictionsInAllEditableElementsEnabled() || _focusedElementInformation.isWritingSuggestionsEnabled) ? UITextInlinePredictionTypeDefault : UITextInlinePredictionTypeNo; #endif [self _updateTextInputTraitsForInteractionTintColor]; diff --git a/Source/WebKit/UIProcess/mac/WebViewImpl.mm b/Source/WebKit/UIProcess/mac/WebViewImpl.mm index cd8f553e536a8..3a9bb887bb59d 100644 --- a/Source/WebKit/UIProcess/mac/WebViewImpl.mm +++ b/Source/WebKit/UIProcess/mac/WebViewImpl.mm @@ -3210,8 +3210,6 @@ static String commandNameForSelector(SEL selector) std::optional WebViewImpl::postLayoutDataForContentEditable() { const EditorState& editorState = m_page->editorState(); - if (!editorState.isContentEditable) - return std::nullopt; // FIXME: It's pretty lame that we have to depend on the most recent EditorState having post layout data, // and that we just bail if it is missing. @@ -5195,6 +5193,14 @@ static BOOL shouldUseHighlightsForMarkedText(NSAttributedString *string) #if HAVE(INLINE_PREDICTIONS) bool WebViewImpl::allowsInlinePredictions() const { + const EditorState& editorState = m_page->editorState(); + + if (editorState.hasPostLayoutData() && editorState.postLayoutData->canEnableWritingSuggestions) + return NSSpellChecker.isAutomaticInlineCompletionEnabled; + + if (!editorState.isContentEditable) + return false; + if (!inlinePredictionsEnabled() && !m_page->preferences().inlinePredictionsInAllEditableElementsEnabled()) return false; diff --git a/Source/WebKit/WebProcess/WebPage/Cocoa/WebPageCocoa.mm b/Source/WebKit/WebProcess/WebPage/Cocoa/WebPageCocoa.mm index ae43f1a4698bf..cefe2e2345ab8 100644 --- a/Source/WebKit/WebProcess/WebPage/Cocoa/WebPageCocoa.mm +++ b/Source/WebKit/WebProcess/WebPage/Cocoa/WebPageCocoa.mm @@ -584,6 +584,8 @@ } postLayoutData.baseWritingDirection = frame.editor().baseWritingDirectionForSelectionStart(); + + postLayoutData.canEnableWritingSuggestions = selection.canEnableWritingSuggestions(); } if (RefPtr editableRootOrFormControl = enclosingTextFormControl(selection.start()) ?: selection.rootEditableElement()) { diff --git a/Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm b/Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm index a7fd723127c0d..104410c82c8c3 100644 --- a/Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm +++ b/Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm @@ -3550,6 +3550,8 @@ static void handleAnimationActions(Element& element, uint32_t action) if (htmlElement) information.isSpellCheckingEnabled = htmlElement->spellcheck(); + information.isWritingSuggestionsEnabled = focusedElement->isWritingSuggestionsEnabled(); + if (RefPtr formControlElement = dynamicDowncast(focusedElement)) information.isFocusingWithValidationMessage = formControlElement->isFocusingWithValidationMessage(); diff --git a/Tools/TestWebKitAPI/SourcesCocoa.txt b/Tools/TestWebKitAPI/SourcesCocoa.txt index d4dd36e81732a..cdcced1ae8cc8 100644 --- a/Tools/TestWebKitAPI/SourcesCocoa.txt +++ b/Tools/TestWebKitAPI/SourcesCocoa.txt @@ -353,6 +353,7 @@ Tests/WebKitCocoa/WebSQLBasics.mm Tests/WebKitCocoa/WebSocket.mm Tests/WebKitCocoa/WebsiteDataStoreCustomPaths.mm Tests/WebKitCocoa/WebsitePolicies.mm +Tests/WebKitCocoa/WritingSuggestions.mm Tests/WebKitCocoa/YoutubeReplacementPlugin.mm Tests/WebKitCocoa/_WKInputDelegate.mm Tests/WebKitCocoa/_WKWebAuthenticationPanel.mm diff --git a/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj b/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj index 7f328039f3c18..2f26853d986d1 100644 --- a/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj +++ b/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj @@ -2013,6 +2013,7 @@ 0711DF51226A95FB003DD2F7 /* AVFoundationSoftLinkTest.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AVFoundationSoftLinkTest.mm; sourceTree = ""; }; 07137049265320E500CA2C9A /* AudioBufferSize.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AudioBufferSize.mm; sourceTree = ""; }; 0721D4582838295400A95853 /* start-offset.ts */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.typescript; path = "start-offset.ts"; sourceTree = ""; }; + 07338E042B7433A400F949EB /* WritingSuggestions.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = WritingSuggestions.mm; sourceTree = ""; }; 0738012E275EADAB000FA77C /* GetDisplayMediaWindowAndScreen.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = GetDisplayMediaWindowAndScreen.mm; sourceTree = ""; }; 0746645722FF62D000E3451A /* AccessibilityTestSupportProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AccessibilityTestSupportProtocol.h; sourceTree = ""; }; 0746645822FF630500E3451A /* AccessibilityTestPlugin.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AccessibilityTestPlugin.mm; sourceTree = ""; }; @@ -4337,6 +4338,7 @@ 9984FACA1CFFAEEE008D198C /* WKWebViewTextInput.mm */, 95A524942581A10D00461FE9 /* WKWebViewThemeColor.mm */, 953ABB3425C0D681004C8B73 /* WKWebViewUnderPageBackgroundColor.mm */, + 07338E042B7433A400F949EB /* WritingSuggestions.mm */, 465A08FD280096F80028FD0E /* YoutubeReplacementPlugin.mm */, ); name = "WebKit Cocoa"; diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/WritingSuggestions.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/WritingSuggestions.mm new file mode 100644 index 0000000000000..19ccf0c74a6bb --- /dev/null +++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/WritingSuggestions.mm @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2017 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "config.h" + +// FIXME: Get these tests to run on other platforms. +#if PLATFORM(IOS_FAMILY) && ENABLE(WRITING_SUGGESTIONS) + +#import "PlatformUtilities.h" +#import "Test.h" +#import "TestInputDelegate.h" +#import "TestNavigationDelegate.h" +#import "TestProtocol.h" +#import "TestResourceLoadDelegate.h" +#import "TestWKWebView.h" +#import "UIKitSPIForTesting.h" +#import "WKWebViewConfigurationExtras.h" +#import +#import + +@interface WritingSuggestionsWebAPIWKWebView : TestWKWebView + +- (void)focusElementAndEnsureEditorStateUpdate:(NSString *)element; + +@end + +@implementation WritingSuggestionsWebAPIWKWebView { + RetainPtr _inputDelegate; +} + +- (instancetype)initWithHTMLString:(NSString *)string +{ + auto configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES]; + + if (!(self = [super initWithFrame:CGRectMake(0, 0, 320, 568) configuration:configuration])) + return nil; + + _inputDelegate = adoptNS([[TestInputDelegate alloc] init]); + [_inputDelegate setFocusStartsInputSessionPolicyHandler:[&] (WKWebView *, id<_WKFocusedElementInfo>) -> _WKFocusStartsInputSessionPolicy { + return _WKFocusStartsInputSessionPolicyAllow; + }]; + [self _setInputDelegate:_inputDelegate.get()]; + + [self synchronouslyLoadHTMLString:string]; + + return self; +} + +- (void)focusElementAndEnsureEditorStateUpdate:(NSString *)element +{ + NSString *script = [NSString stringWithFormat:@"%@.focus()", element]; + [self evaluateJavaScriptAndWaitForInputSessionToChange:script]; + [self waitForNextPresentationUpdate]; +} + +@end + +TEST(WritingSuggestionsWebAPI, DefaultState) +{ + auto webView = adoptNS([[WritingSuggestionsWebAPIWKWebView alloc] initWithHTMLString:@"
This is some text.
"]); + [webView focusElementAndEnsureEditorStateUpdate:@"document.getElementById('div')"]; + + EXPECT_EQ(UITextInlinePredictionTypeDefault, [webView effectiveTextInputTraits].inlinePredictionType); +} + +TEST(WritingSuggestionsWebAPI, DefaultStateWithInput) +{ + auto webView = adoptNS([[WritingSuggestionsWebAPIWKWebView alloc] initWithHTMLString:@""]); + [webView focusElementAndEnsureEditorStateUpdate:@"document.getElementById('input')"]; + + EXPECT_EQ(UITextInlinePredictionTypeDefault, [webView effectiveTextInputTraits].inlinePredictionType); +} + +TEST(WritingSuggestionsWebAPI, ExplicitlyEnabled) +{ + auto webView = adoptNS([[WritingSuggestionsWebAPIWKWebView alloc] initWithHTMLString:@"
This is some text.
"]); + [webView focusElementAndEnsureEditorStateUpdate:@"document.getElementById('div')"]; + + EXPECT_EQ(UITextInlinePredictionTypeDefault, [webView effectiveTextInputTraits].inlinePredictionType); +} + +TEST(WritingSuggestionsWebAPI, ExplicitlyDisabled) +{ + auto webView = adoptNS([[WritingSuggestionsWebAPIWKWebView alloc] initWithHTMLString:@"
This is some text.
"]); + [webView focusElementAndEnsureEditorStateUpdate:@"document.getElementById('div')"]; + + EXPECT_EQ(UITextInlinePredictionTypeNo, [webView effectiveTextInputTraits].inlinePredictionType); +} + +TEST(WritingSuggestionsWebAPI, ExplicitlyDisabledOnParent) +{ + auto webView = adoptNS([[WritingSuggestionsWebAPIWKWebView alloc] initWithHTMLString:@"
This is some text.
"]); + [webView focusElementAndEnsureEditorStateUpdate:@"document.getElementById('div')"]; + + EXPECT_EQ(UITextInlinePredictionTypeNo, [webView effectiveTextInputTraits].inlinePredictionType); +} + +#endif