Modal keybindings (#484)

* first draft at modal keybindings

* added modal keybindings and inline settings text edit keybindings

* added switch for keybindings in modal footer

* removed logs

* remove another console.log

* fix userinput keybindings -- should be generic:cancel for 2nd one
This commit is contained in:
Cole Lashley 2024-03-21 23:10:02 -07:00 committed by GitHub
parent 923cf71e0a
commit c0c53edb84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 128 additions and 46 deletions

View File

@ -8,6 +8,8 @@ import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { If } from "tsx-control-statements/components";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
import { GlobalModel } from "@/models";
import { v4 as uuidv4 } from "uuid";
import "./inlinesettingstextedit.less";
@ -27,6 +29,11 @@ class InlineSettingsTextEdit extends React.Component<
tempText: OV<string>;
shouldFocus: boolean = false;
inputRef: React.RefObject<any> = React.createRef();
curId: string;
componentDidMount(): void {
this.curId = uuidv4();
}
componentDidUpdate(): void {
if (this.shouldFocus) {
@ -52,6 +59,7 @@ class InlineSettingsTextEdit extends React.Component<
this.tempText = null;
this.props.onChange(newText);
})();
this.unregisterKeybindings();
}
@boundMethod
@ -60,24 +68,38 @@ class InlineSettingsTextEdit extends React.Component<
this.isEditing.set(false);
this.tempText = null;
})();
this.unregisterKeybindings();
}
@boundMethod
handleKeyDown(e: any): void {
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
if (checkKeyPressed(waveEvent, "Enter")) {
e.preventDefault();
e.stopPropagation();
handleFocus() {
this.registerKeybindings();
}
registerKeybindings() {
let keybindManager = GlobalModel.keybindManager;
let domain = "inline-settings" + this.curId;
keybindManager.registerKeybinding("mainview", domain, "generic:confirm", (waveEvent) => {
this.confirmChange();
return;
}
if (checkKeyPressed(waveEvent, "Escape")) {
e.preventDefault();
e.stopPropagation();
return true;
});
keybindManager.registerKeybinding("mainview", domain, "generic:cancel", (waveEvent) => {
this.cancelChange();
return;
}
return;
return true;
});
}
unregisterKeybindings() {
let domain = "inline-settings" + this.curId;
GlobalModel.keybindManager.unregisterDomain(domain);
}
handleBlur() {
this.unregisterKeybindings();
this.cancelChange();
}
componentWillUnmount(): void {
this.unregisterKeybindings();
}
@boundMethod
@ -99,7 +121,8 @@ class InlineSettingsTextEdit extends React.Component<
ref={this.inputRef}
className="input"
type="text"
onKeyDown={this.handleKeyDown}
onFocus={this.handleFocus.bind(this)}
onBlur={this.handleBlur.bind(this)}
placeholder={this.props.placeholder}
onChange={this.handleChangeText}
value={this.tempText.get()}

View File

@ -6,8 +6,11 @@ import * as mobx from "mobx";
import { If } from "tsx-control-statements/components";
import ReactDOM from "react-dom";
import { Button } from "./button";
import { v4 as uuidv4 } from "uuid";
import { GlobalModel } from "@/models";
import "./modal.less";
import { boundMethod } from "autobind-decorator";
interface ModalHeaderProps {
onClose?: () => void;
@ -30,10 +33,52 @@ interface ModalFooterProps {
onOk?: () => void;
cancelLabel?: string;
okLabel?: string;
keybindings?: boolean;
}
const ModalFooter: React.FC<ModalFooterProps> = ({ onCancel, onOk, cancelLabel = "Cancel", okLabel = "Ok" }) => (
class ModalKeybindings extends React.Component<{ onOk; onCancel }, {}> {
curId: string;
@boundMethod
componentDidMount(): void {
this.curId = uuidv4();
let domain = "modal-" + this.curId;
let keybindManager = GlobalModel.keybindManager;
if (this.props.onOk) {
keybindManager.registerKeybinding("modal", domain, "generic:confirm", (waveEvent) => {
this.props.onOk();
return true;
});
}
if (this.props.onCancel) {
keybindManager.registerKeybinding("modal", domain, "generic:cancel", (waveEvent) => {
this.props.onCancel();
return true;
});
}
}
@boundMethod
componentWillUnmount(): void {
GlobalModel.keybindManager.unregisterDomain("modal-" + this.curId);
}
render(): React.ReactNode {
return null;
}
}
const ModalFooter: React.FC<ModalFooterProps> = ({
onCancel,
onOk,
cancelLabel = "Cancel",
okLabel = "Ok",
keybindings = true,
}) => (
<div className="wave-modal-footer">
<If condition={keybindings}>
<ModalKeybindings onOk={onOk} onCancel={onCancel}></ModalKeybindings>
</If>
{onCancel && (
<Button className="secondary" onClick={onCancel}>
{cancelLabel}
@ -79,4 +124,4 @@ class Modal extends React.Component<ModalProps> {
}
}
export { Modal };
export { Modal, ModalKeybindings };

View File

@ -18,6 +18,8 @@ interface TextFieldProps {
className?: string;
onChange?: (value: string) => void;
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
onFocus?: () => void;
onBlur?: () => void;
placeholder?: string;
defaultValue?: string;
decoration?: TextFieldDecorationProps;
@ -78,6 +80,9 @@ class TextField extends React.Component<TextFieldProps, TextFieldState> {
@boundMethod
handleFocus() {
this.setState({ focused: true });
if (this.props.onFocus) {
this.props.onFocus();
}
}
@boundMethod
@ -91,6 +96,9 @@ class TextField extends React.Component<TextFieldProps, TextFieldState> {
this.setState({ error: false, focused: false });
}
}
if (this.props.onBlur) {
this.props.onBlur();
}
}
@boundMethod

View File

@ -9,6 +9,7 @@ import { Markdown, Modal, Button, Checkbox } from "@/elements";
import { GlobalModel, GlobalCommandRunner } from "@/models";
import "./alert.less";
import { ModalKeybindings } from "../elements/modal";
@mobxReact.observer
class AlertModal extends React.Component<{}, {}> {
@ -54,6 +55,7 @@ class AlertModal extends React.Component<{}, {}> {
</div>
<div className="wave-modal-footer">
<If condition={isConfirm}>
<ModalKeybindings onOk={this.handleOK} onCancel={this.closeModal}></ModalKeybindings>
<Button className="secondary" onClick={this.closeModal}>
Cancel
</Button>
@ -62,6 +64,7 @@ class AlertModal extends React.Component<{}, {}> {
</Button>
</If>
<If condition={!isConfirm}>
<ModalKeybindings onOk={this.handleOK} onCancel={null}></ModalKeybindings>
<Button autoFocus={true} onClick={this.handleOK}>
Ok
</Button>

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { GlobalModel } from "@/models";
import { Choose, When, If } from "tsx-control-statements/components";
import { Modal, PasswordField, TextField, Markdown, Checkbox } from "@/elements";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent, KeybindManager } from "@/util/keyutil";
import "./userinput.less";
@ -44,17 +44,20 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
[userInputRequest]
);
function handleTextKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
if (checkKeyPressed(waveEvent, "Enter")) {
e.preventDefault();
e.stopPropagation();
function handleTextFocus() {
let keybindManager = GlobalModel.keybindManager;
keybindManager.registerKeybinding("modal", "userinput", "generic:confirm", (waveEvent) => {
handleSendText();
} else if (checkKeyPressed(waveEvent, "Escape")) {
e.preventDefault();
e.stopPropagation();
return true;
});
keybindManager.registerKeybinding("modal", "userinput", "generic:cancel", (waveEvent) => {
handleSendCancel();
}
return true;
});
}
function handleTextBlur() {
GlobalModel.keybindManager.unregisterDomain("userinput");
}
React.useEffect(() => {
@ -89,7 +92,8 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
value={responseText}
maxLength={400}
autoFocus={true}
onKeyDown={(e) => handleTextKeyDown(e)}
onFocus={() => handleTextFocus()}
onBlur={() => handleTextBlur()}
/>
</If>
<If condition={!userInputRequest.publictext}>
@ -98,7 +102,8 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
value={responseText}
maxLength={400}
autoFocus={true}
onKeyDown={(e) => handleTextKeyDown(e)}
onFocus={() => handleTextFocus()}
onBlur={() => handleTextBlur()}
/>
</If>
</If>

View File

@ -14,6 +14,7 @@ import * as textmeasure from "@/util/textmeasure";
import * as appconst from "@/app/appconst";
import "./viewremoteconndetail.less";
import { ModalKeybindings } from "../elements/modal";
@mobxReact.observer
class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
@ -382,6 +383,20 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
</div>
</div>
<div className="wave-modal-footer">
<ModalKeybindings
onOk={() => {
if (selectedRemoteStatus == "connecting") {
return;
}
this.handleClose();
}}
onCancel={() => {
if (selectedRemoteStatus == "connecting") {
return;
}
this.handleClose();
}}
></ModalKeybindings>
<Button
className="secondary"
disabled={selectedRemoteStatus == "connecting"}

View File

@ -476,23 +476,6 @@ class Model {
if (isModKeyPress(e)) {
return;
}
if (this.alertMessage.get() != null) {
if (checkKeyPressed(waveEvent, "Escape")) {
e.preventDefault();
this.modalsModel.popModal(() => this.cancelAlert());
return;
}
if (checkKeyPressed(waveEvent, "Enter")) {
e.preventDefault();
this.confirmAlert();
return;
}
return;
}
if (checkKeyPressed(waveEvent, "Escape") && this.modalsModel.store.length > 0) {
this.modalsModel.popModal();
return;
}
if (this.keybindManager.processKeyEvent(e, waveEvent)) {
return;
}