Files
iOS/Sources/App/Frontend/WebView/WebViewJavascriptCommands.swift
Bruno Pantaleão Gonçalves 56b5c6a20e Add keyboard avoidance and focused element scroll (#4486)
<!-- Thank you for submitting a Pull Request and helping to improve Home
Assistant. Please complete the following sections to help the processing
and review of your changes. Please do not delete anything from this
template. -->

## Summary
<!-- Provide a brief summary of the changes you have made and most
importantly what they aim to achieve -->

Introduce keyboard handling for WKWebView: add WebViewKeyboardAvoidance
helpers for animation metrics and overlap calculation, a WKWebView
extension to run a JS command that scrolls the focused editable element
into view, and a managed bottom constraint for web views so the view is
animated above the keyboard. Wire up keyboard observers and
DispatchWorkItem scheduling/cleanup in WebViewController and
OnboardingAuthLoginViewController, and add the
scrollFocusedElementIntoView JavaScript to WebViewJavascriptCommands.
Add unit tests verifying the bottom constraint creation and JS contents.

## Screenshots
<!-- If this is a user-facing change not in the frontend, please include
screenshots in light and dark mode. -->

## Link to pull request in Documentation repository
<!-- Pull requests that add, change or remove functionality must have a
corresponding pull request in the Companion App Documentation repository
(https://github.com/home-assistant/companion.home-assistant). Please add
the number of this pull request after the "#" -->
Documentation: home-assistant/companion.home-assistant#

## Any other notes
<!-- If there is any other information of note, like if this Pull
Request is part of a bigger change, please include it here. -->
2026-04-08 12:19:04 +02:00

109 lines
2.8 KiB
Swift

import Foundation
enum WebViewJavascriptCommands {
static var searchEntitiesKeyEvent = """
var event = new KeyboardEvent('keydown', {
key: 'e',
code: 'KeyE',
keyCode: 69,
which: 69,
bubbles: true,
cancelable: true
});
document.dispatchEvent(event);
"""
static var quickSearchKeyEvent = """
var event = new KeyboardEvent('keydown', {
key: 'k',
code: 'KeyK',
keyCode: 75,
which: 75,
metaKey: true,
bubbles: true,
cancelable: true
});
document.dispatchEvent(event);
"""
static var searchDevicesKeyEvent = """
var event = new KeyboardEvent('keydown', {
key: 'd',
code: 'KeyD',
keyCode: 68,
which: 68,
bubbles: true,
cancelable: true
});
document.dispatchEvent(event);
"""
static var searchCommandsKeyEvent = """
var event = new KeyboardEvent('keydown', {
key: 'c',
code: 'KeyC',
keyCode: 67,
which: 67,
bubbles: true,
cancelable: true
});
document.dispatchEvent(event);
"""
static var assistKeyEvent = """
var event = new KeyboardEvent('keydown', {
key: 'a',
code: 'KeyA',
keyCode: 65,
which: 65,
bubbles: true,
cancelable: true
});
document.dispatchEvent(event);
"""
static var scrollFocusedElementIntoView = """
(function() {
function activeElement(root) {
let element = root.activeElement;
while (element && element.shadowRoot && element.shadowRoot.activeElement) {
element = element.shadowRoot.activeElement;
}
return element;
}
const element = activeElement(document);
if (!element) {
return false;
}
const tagName = element.tagName ? element.tagName.toUpperCase() : '';
const isEditable = element.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(tagName);
if (!isEditable) {
return false;
}
const viewport = window.visualViewport;
const viewportHeight = viewport ? viewport.height : window.innerHeight;
const viewportOffsetTop = viewport ? viewport.offsetTop : 0;
const padding = 24;
const rect = element.getBoundingClientRect();
const visibleTop = viewportOffsetTop + padding;
const visibleBottom = viewportOffsetTop + viewportHeight - padding;
if (rect.top >= visibleTop && rect.bottom <= visibleBottom) {
return true;
}
element.scrollIntoView({
block: 'center',
inline: 'nearest',
behavior: 'auto'
});
return true;
})();
"""
}