From 3cd3b9488e79d8ab5c49361ba688e94ae1067f0f Mon Sep 17 00:00:00 2001 From: Yunus Ahmed Choudhury Date: Fri, 28 Aug 2026 16:03:42 +0530 Subject: [PATCH 1/5] Add native find navigator to editor --- ios/Sources/GutenbergKit/Sources/EditorViewController.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift index da4c1fefe..80d9474b7 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift @@ -245,6 +245,7 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro config.applicationNameForUserAgent = "GutenbergKit/\(GutenbergKitVersion.version)" self.webView = GBWebView(frame: .zero, configuration: config) + self.webView.isFindInteractionEnabled = true self.webView.scrollView.keyboardDismissMode = .interactive self.isWarmupMode = isWarmupMode @@ -557,6 +558,11 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro evaluate("editor.redo();") } + public func presentFindNavigator() { + webView.findInteraction?.presentFindNavigator(showingReplace: false) + } + + /// Dismisses the topmost modal dialog or menu in the editor public func dismissTopModal() { guard isReady else { return } From 601cae518c070aa6d2c81bfe030ebf3dcd09831d Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Wed, 16 Sep 2026 10:03:29 -0400 Subject: [PATCH 2/5] style: remove extra blank line after presentFindNavigator Resolves a SwiftLint `vertical_whitespace` violation. Co-Authored-By: Claude Opus 5 (1M context) --- ios/Sources/GutenbergKit/Sources/EditorViewController.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift index 80d9474b7..389c99cea 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift @@ -562,7 +562,6 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro webView.findInteraction?.presentFindNavigator(showingReplace: false) } - /// Dismisses the topmost modal dialog or menu in the editor public func dismissTopModal() { guard isReady else { return } From b28db6214118a953f6e0716bfa411ebcd5420b93 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Wed, 16 Sep 2026 10:05:33 -0400 Subject: [PATCH 3/5] feat(demo): add find in post to the editor more menu Gives the iOS demo app an entry point for `presentFindNavigator()`, so the find navigator can be exercised through the same path the host apps will use rather than only via a hardware keyboard shortcut. Co-Authored-By: Claude Opus 5 (1M context) --- ios/Demo-iOS/Sources/Views/EditorView.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ios/Demo-iOS/Sources/Views/EditorView.swift b/ios/Demo-iOS/Sources/Views/EditorView.swift index 0f9b56ca4..fac8fbb43 100644 --- a/ios/Demo-iOS/Sources/Views/EditorView.swift +++ b/ios/Demo-iOS/Sources/Views/EditorView.swift @@ -92,6 +92,12 @@ struct EditorView: View { private var moreMenu: some View { Menu { + Button(action: { + viewModel.perform(.find) + }, label: { + Label("Find in Post", systemImage: "magnifyingglass") + }) + Button(action: { viewModel.isCodeEditorEnabled.toggle() }, label: { @@ -144,6 +150,7 @@ private struct _EditorView: UIViewControllerRepresentable { switch $0 { case .redo: viewController?.redo() case .undo: viewController?.undo() + case .find: viewController?.presentFindNavigator() } } @@ -377,6 +384,7 @@ private final class EditorViewModel { enum Action { case undo case redo + case find } var perform: (_ action: Action) -> Void = { _ in assertionFailure() } From 2ef446466aaed8eeb1b728daa5f2f02d2d0ea385 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Wed, 16 Sep 2026 13:06:28 -0400 Subject: [PATCH 4/5] fix(ios): restore web view height after dismissing the find navigator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dismissing the find navigator while the software keyboard is visible left the web view short by the navigator's height, exposing a strip of the view controller's background below the editor. `UIKeyboardLayoutGuide` folds the find navigator into the region it tracks. When the navigator and the keyboard go away together the guide never receives a final update, so it stays short and the web view's bottom constraint inherits the stale value. Track the keyboard's visibility directly and pin the web view to the safe area whenever neither the keyboard nor the navigator is present, leaving the guide in charge only while something is actually docked there. The navigator still reports itself visible during `keyboardWillHide` and only flips one run loop later, so re-check then — still inside the keyboard's animation, and matching its duration, so the web view grows alongside the keyboard instead of leaving a gap that snaps shut once the keyboard has gone. Co-Authored-By: Claude Opus 5 (1M context) --- .../Sources/EditorViewController.swift | 104 +++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift index 389c99cea..ac3e857be 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift @@ -283,8 +283,9 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), webView.topAnchor.constraint(equalTo: view.topAnchor), webView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - webView.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor) + webViewBottomToSafeArea ]) + observeKeyboardVisibility() // WebView starts hidden and fades in when editor navigation completes webView.alpha = 0 @@ -324,6 +325,107 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro setupNavigationOverlay() } + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + // The find navigator can be dismissed without any accompanying keyboard + // notification, so re-evaluate on layout as well. + updateWebViewBottomConstraint() + } + + // MARK: - Web View Bottom Inset + + /// Pins the web view above the software keyboard. + private lazy var webViewBottomToKeyboard = webView.bottomAnchor.constraint( + equalTo: view.keyboardLayoutGuide.topAnchor + ) + + /// Pins the web view to the bottom of the view, used whenever nothing is + /// docked above it. + /// + /// `UIKeyboardLayoutGuide` folds the system find navigator into the region + /// it tracks. When the navigator and the keyboard are dismissed together, + /// the guide never receives a final update and stays short by the + /// navigator's height, stranding the web view above the bottom of the + /// screen. Swapping to the safe area whenever neither the keyboard nor the + /// navigator is present sidesteps the stale value without changing how the + /// keyboard itself is tracked. + private lazy var webViewBottomToSafeArea = webView.bottomAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.bottomAnchor + ) + + private var isKeyboardVisible = false + + private func observeKeyboardVisibility() { + let center = NotificationCenter.default + center.addObserver( + self, + selector: #selector(keyboardWillShow), + name: UIResponder.keyboardWillShowNotification, + object: nil + ) + center.addObserver( + self, + selector: #selector(keyboardWillHide(_:)), + name: UIResponder.keyboardWillHideNotification, + object: nil + ) + center.addObserver( + self, + selector: #selector(keyboardDidHide), + name: UIResponder.keyboardDidHideNotification, + object: nil + ) + } + + @objc private func keyboardWillShow() { + isKeyboardVisible = true + updateWebViewBottomConstraint() + } + + @objc private func keyboardWillHide(_ notification: Notification) { + isKeyboardVisible = false + updateWebViewBottomConstraint() + + // The find navigator still reports itself visible at this point, so the + // pass above cannot yet tell that nothing is left docked at the bottom. + // It flips one run loop later, while the keyboard is still animating + // away, so re-check then and borrow the keyboard's own duration: the web + // view grows alongside the keyboard instead of leaving a gap that snaps + // shut once the keyboard has gone. + let duration = notification.userInfo?[ + UIResponder.keyboardAnimationDurationUserInfoKey + ] as? TimeInterval ?? 0.25 + DispatchQueue.main.async { [weak self] in + guard let self, self.updateWebViewBottomConstraint() else { return } + UIView.animate(withDuration: duration) { + self.view.layoutIfNeeded() + } + } + } + + /// Final safety net for hosts where the check above still runs too early. + @objc private func keyboardDidHide() { + isKeyboardVisible = false + updateWebViewBottomConstraint() + } + + /// - Returns: Whether the active constraint changed. + @discardableResult + private func updateWebViewBottomConstraint() -> Bool { + let isFindNavigatorVisible = webView.findInteraction?.isFindNavigatorVisible ?? false + let shouldTrackKeyboard = isKeyboardVisible || isFindNavigatorVisible + guard shouldTrackKeyboard != webViewBottomToKeyboard.isActive else { return false } + + if shouldTrackKeyboard { + webViewBottomToSafeArea.isActive = false + webViewBottomToKeyboard.isActive = true + } else { + webViewBottomToKeyboard.isActive = false + webViewBottomToSafeArea.isActive = true + } + return true + } + public override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) removeNavigationOverlay() From 7e4bc8a0b757c43999465627392c045a7ca56f85 Mon Sep 17 00:00:00 2001 From: Yunus Ahmed Choudhury Date: Sun, 27 Sep 2026 00:48:52 +0530 Subject: [PATCH 5/5] Android: add native find decision spike --- .../org/wordpress/gutenberg/GutenbergView.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt index d893265f4..0a793599f 100644 --- a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt +++ b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt @@ -867,6 +867,28 @@ class GutenbergView : FrameLayout { webView.evaluateJavascript("editor.undo();", null) } } + /** + * Temporary Android native Find decision spike. + * This is not the final Find in Post API. + */ + fun debugFind(query: String) { + if (!isEditorLoaded) { + Log.e("GutenbergView", "Can't search until the editor has loaded") + return + } + + handler.post { + webView.setFindListener { activeMatchOrdinal, numberOfMatches, isDoneCounting -> + Log.d( + "GutenbergFindSpike", + "query=$query activeMatchOrdinal=$activeMatchOrdinal " + + "numberOfMatches=$numberOfMatches isDoneCounting=$isDoneCounting" + ) + } + + webView.findAllAsync(query) + } + } fun redo() { handler.post {