Keep provider settings above the model selector (#2476)
* fix(app): keep provider settings above model selector Desktop web comboboxes used the browser top layer, so ordinary modal portals could not cover them. Keep web overlays in one relative layer stack while preserving native bottom-sheet stacking. * fix(app): route overlay keyboard ownership * fix(app): preserve nested overlay ownership * fix(app): route command center overlay input * fix(app): preserve global overlay ancestry * fix(app): register global dialog hosts
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { createElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
Modal,
|
||||
Pressable,
|
||||
@@ -13,8 +13,13 @@ import { Server } from "lucide-react-native";
|
||||
import { create } from "zustand";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { HostStatusDotSlot } from "@/components/hosts/host-picker";
|
||||
import { isNative } from "@/constants/platform";
|
||||
import { isWeb } from "@/constants/platform";
|
||||
import { useLocalDaemonServerId } from "@/hooks/use-is-local-daemon";
|
||||
import {
|
||||
OverlayLayerProvider,
|
||||
useGlobalWebOverlayLayer,
|
||||
useWebOverlayRegistration,
|
||||
} from "@/lib/overlay-root";
|
||||
import { useHosts } from "@/runtime/host-runtime";
|
||||
import { orderHostsLocalFirst, type HostProfile } from "@/types/host-connection";
|
||||
import { buildSettingsAddHostRoute } from "@/utils/host-routes";
|
||||
@@ -143,6 +148,7 @@ export function HostChooserModal() {
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const [query, setQuery] = useState("");
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const modalLayer = useGlobalWebOverlayLayer("modal", isWeb && request != null);
|
||||
|
||||
const requestHosts = useMemo(() => {
|
||||
if (!request) return [];
|
||||
@@ -182,58 +188,60 @@ export function HostChooserModal() {
|
||||
[close, request],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!request || isNative || typeof window === "undefined") return;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const handleWebOverlayKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key !== "ArrowDown" &&
|
||||
event.key !== "ArrowUp" &&
|
||||
event.key !== "Enter" &&
|
||||
event.key !== "Escape"
|
||||
) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
close();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === "Enter") {
|
||||
const host = options[activeOptionIndex];
|
||||
if (!host) return;
|
||||
if (!host) return false;
|
||||
event.preventDefault();
|
||||
chooseHost(host.serverId);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (options.length === 0) return;
|
||||
if (options.length === 0) return false;
|
||||
event.preventDefault();
|
||||
const next = event.key === "ArrowDown" ? activeOptionIndex + 1 : activeOptionIndex - 1;
|
||||
if (next < 0) {
|
||||
setActiveIndex(options.length - 1);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
if (next >= options.length) {
|
||||
setActiveIndex(0);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
setActiveIndex(next);
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown, true);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown, true);
|
||||
}, [activeOptionIndex, chooseHost, close, options, request]);
|
||||
return true;
|
||||
},
|
||||
[activeOptionIndex, chooseHost, close, options],
|
||||
);
|
||||
const setWebOverlayScope = useWebOverlayRegistration({
|
||||
active: isWeb && request != null,
|
||||
layer: modalLayer,
|
||||
onKeyDown: handleWebOverlayKeyDown,
|
||||
});
|
||||
|
||||
if (!request) return null;
|
||||
|
||||
return (
|
||||
const modal = (
|
||||
<Modal visible transparent animationType="fade" onRequestClose={close} testID="host-chooser">
|
||||
<View style={styles.overlay}>
|
||||
<Pressable style={styles.backdrop} onPress={close} />
|
||||
<View style={styles.panel}>
|
||||
<View ref={setWebOverlayScope} style={styles.panel}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>{request.title}</Text>
|
||||
<TextInput
|
||||
@@ -268,6 +276,8 @@ export function HostChooserModal() {
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
return createElement(OverlayLayerProvider, { layer: isWeb ? modalLayer : 0 }, modal);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
|
||||
Reference in New Issue
Block a user