fix(mobile): prevent server input focus loss and text modification while typing
This commit is contained in:
149
overlay/src/app/pages/auth/ServerPicker.tsx
Normal file
149
overlay/src/app/pages/auth/ServerPicker.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import React, {
|
||||||
|
FocusEventHandler,
|
||||||
|
KeyboardEventHandler,
|
||||||
|
MouseEventHandler,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import {
|
||||||
|
Header,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
Icons,
|
||||||
|
Input,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
PopOut,
|
||||||
|
RectCords,
|
||||||
|
Text,
|
||||||
|
config,
|
||||||
|
} from 'folds';
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
|
||||||
|
export function ServerPicker({
|
||||||
|
server,
|
||||||
|
serverList,
|
||||||
|
allowCustomServer,
|
||||||
|
onServerChange,
|
||||||
|
}: {
|
||||||
|
server: string;
|
||||||
|
serverList: string[];
|
||||||
|
allowCustomServer?: boolean;
|
||||||
|
onServerChange: (server: string) => void;
|
||||||
|
}) {
|
||||||
|
const [serverMenuAnchor, setServerMenuAnchor] = useState<RectCords>();
|
||||||
|
const serverInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Only sync input when server changes externally (e.g., from menu selection)
|
||||||
|
// and input is not focused to avoid cursor jumping during typing
|
||||||
|
if (
|
||||||
|
serverInputRef.current &&
|
||||||
|
serverInputRef.current.value !== server &&
|
||||||
|
document.activeElement !== serverInputRef.current
|
||||||
|
) {
|
||||||
|
serverInputRef.current.value = server;
|
||||||
|
}
|
||||||
|
}, [server]);
|
||||||
|
|
||||||
|
const handleBlur: FocusEventHandler<HTMLInputElement> = (evt) => {
|
||||||
|
const inputServer = evt.target.value.trim();
|
||||||
|
if (inputServer && inputServer !== server) {
|
||||||
|
onServerChange(inputServer);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (evt) => {
|
||||||
|
if (evt.key === 'ArrowDown') {
|
||||||
|
evt.preventDefault();
|
||||||
|
setServerMenuAnchor(undefined);
|
||||||
|
}
|
||||||
|
if (evt.key === 'Enter') {
|
||||||
|
evt.preventDefault();
|
||||||
|
const inputServer = evt.currentTarget.value.trim();
|
||||||
|
if (inputServer) onServerChange(inputServer);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleServerSelect: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||||
|
const selectedServer = evt.currentTarget.getAttribute('data-server');
|
||||||
|
if (selectedServer) {
|
||||||
|
onServerChange(selectedServer);
|
||||||
|
}
|
||||||
|
setServerMenuAnchor(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenServerMenu: MouseEventHandler<HTMLElement> = (evt) => {
|
||||||
|
const target = evt.currentTarget.parentElement ?? evt.currentTarget;
|
||||||
|
setServerMenuAnchor(target.getBoundingClientRect());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
ref={serverInputRef}
|
||||||
|
style={{ paddingRight: config.space.S200 }}
|
||||||
|
variant={allowCustomServer ? 'Background' : 'Surface'}
|
||||||
|
outlined
|
||||||
|
defaultValue={server}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
size="500"
|
||||||
|
readOnly={!allowCustomServer}
|
||||||
|
onClick={allowCustomServer ? undefined : handleOpenServerMenu}
|
||||||
|
after={
|
||||||
|
serverList.length === 0 || (serverList.length === 1 && !allowCustomServer) ? undefined : (
|
||||||
|
<PopOut
|
||||||
|
anchor={serverMenuAnchor}
|
||||||
|
position="Bottom"
|
||||||
|
align="End"
|
||||||
|
offset={4}
|
||||||
|
content={
|
||||||
|
<FocusTrap
|
||||||
|
focusTrapOptions={{
|
||||||
|
initialFocus: false,
|
||||||
|
onDeactivate: () => setServerMenuAnchor(undefined),
|
||||||
|
clickOutsideDeactivates: true,
|
||||||
|
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||||
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu>
|
||||||
|
<Header size="300" style={{ padding: `0 ${config.space.S200}` }}>
|
||||||
|
<Text size="L400">Homeserver List</Text>
|
||||||
|
</Header>
|
||||||
|
<div style={{ padding: config.space.S100, paddingTop: 0 }}>
|
||||||
|
{serverList?.map((serverName) => (
|
||||||
|
<MenuItem
|
||||||
|
key={serverName}
|
||||||
|
radii="300"
|
||||||
|
aria-pressed={serverName === server}
|
||||||
|
data-server={serverName}
|
||||||
|
onClick={handleServerSelect}
|
||||||
|
>
|
||||||
|
<Text>{serverName}</Text>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Menu>
|
||||||
|
</FocusTrap>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleOpenServerMenu}
|
||||||
|
variant={allowCustomServer ? 'Background' : 'Surface'}
|
||||||
|
size="300"
|
||||||
|
aria-pressed={!!serverMenuAnchor}
|
||||||
|
radii="300"
|
||||||
|
>
|
||||||
|
<Icon src={Icons.ChevronBottom} />
|
||||||
|
</IconButton>
|
||||||
|
</PopOut>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user