diff --git a/src/components/ComboBox/ComboBox.test.tsx b/src/components/ComboBox/ComboBox.test.tsx
index 7a72b5c5..fb962dff 100644
--- a/src/components/ComboBox/ComboBox.test.tsx
+++ b/src/components/ComboBox/ComboBox.test.tsx
@@ -294,6 +294,8 @@ describe("ComboBox", () => {
expect(textarea.textContent).toEqual("@file /foo");
});
+ test.todo("@, enter, enter, ctrl+z, enter");
+
// test("textarea should be empty after submit", async () => {
// const submitSpy = vi.fn();
// const { user, ...app } = render();
diff --git a/src/components/ComboBox/ComboBox.tsx b/src/components/ComboBox/ComboBox.tsx
index f62c579c..b9d7ddf6 100644
--- a/src/components/ComboBox/ComboBox.tsx
+++ b/src/components/ComboBox/ComboBox.tsx
@@ -67,19 +67,6 @@ export const ComboBox: React.FC = ({
return trigger;
};
- React.useEffect(() => {
- if (!ref.current) return;
- const maybeTrigger = !selectedCommand && trigger ? trigger : null;
-
- const cursor = wasDelete
- ? ref.current.selectionStart - 1
- : startPosition !== null
- ? startPosition + trigger.length
- : ref.current.selectionStart;
- requestCommandsCompletion(value, cursor, maybeTrigger);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [startPosition, trigger, value]);
-
React.useLayoutEffect(() => {
if (!ref.current) return;
if (endPosition === null) return;
@@ -103,6 +90,15 @@ export const ComboBox: React.FC = ({
}
}, [trigger, setSelectedCommand, selectedCommand]);
+ React.useLayoutEffect(() => {
+ if (!ref.current) return;
+ const maybeTrigger = !selectedCommand && trigger ? trigger : null;
+ const cursor =
+ endPosition ?? Math.max(startPosition ?? 0, ref.current.selectionStart);
+ requestCommandsCompletion(value, cursor, maybeTrigger);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [value]);
+
const onKeyDown = (event: React.KeyboardEvent) => {
const state = combobox.getState();
@@ -153,11 +149,9 @@ export const ComboBox: React.FC = ({
}
const maybeCommand = detectCommand(ref.current);
- const maybeCommandWithArguments = maybeCommand?.command
- .split(" ")
- .filter((_) => _);
+ const maybeCommandWithArguments = maybeCommand?.command.split(" ");
- if (wasDelete && maybeCommand) {
+ if (maybeCommand) {
setTrigger(maybeCommand.command);
setStartPosition(maybeCommand.startPosition);
if (maybeCommandWithArguments && maybeCommandWithArguments.length > 1) {
@@ -166,7 +160,7 @@ export const ComboBox: React.FC = ({
setSelectedCommand("");
}
combobox.show();
- } else if (wasDelete && !maybeCommand) {
+ } else if (wasDelete) {
setTrigger("");
setSelectedCommand("");
setStartPosition(null);
diff --git a/src/components/TextArea/TextArea.tsx b/src/components/TextArea/TextArea.tsx
index 8a4975dc..c0bfeff7 100644
--- a/src/components/TextArea/TextArea.tsx
+++ b/src/components/TextArea/TextArea.tsx
@@ -1,7 +1,13 @@
-import React, { useEffect, useImperativeHandle, useRef } from "react";
+import React, {
+ useEffect,
+ useImperativeHandle,
+ useRef,
+ useLayoutEffect,
+} from "react";
import { TextArea as RadixTextArea } from "@radix-ui/themes";
import classNames from "classnames";
import { useUndoRedo } from "../../hooks";
+import { createSyntheticEvent } from "../../utils/createSyntheticEvent";
import styles from "./TextArea.module.css";
export type TextAreaProps = React.ComponentProps &
@@ -12,6 +18,7 @@ export type TextAreaProps = React.ComponentProps &
export const TextArea = React.forwardRef(
({ onTextAreaHeightChange, value, onKeyDown, onChange, ...props }, ref) => {
+ const [callChange, setCallChange] = React.useState(true);
const innerRef = useRef(null);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
useImperativeHandle(ref, () => innerRef.current!, []);
@@ -22,11 +29,13 @@ export const TextArea = React.forwardRef(
if (isMod && event.key === "z" && !event.shiftKey) {
event.preventDefault();
undoRedo.undo();
+ setCallChange(true);
}
if (isMod && event.key === "z" && event.shiftKey) {
event.preventDefault();
undoRedo.redo();
+ setCallChange(true);
}
if (event.key === "Enter" && !event.shiftKey) {
@@ -51,10 +60,41 @@ export const TextArea = React.forwardRef(
}, [innerRef.current?.value, onTextAreaHeightChange]);
useEffect(() => {
- undoRedo.setState(value);
+ if (value !== undoRedo.state) {
+ undoRedo.setState(value);
+ }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
+ useLayoutEffect(() => {
+ if (innerRef.current && callChange && undoRedo.state !== value) {
+ const e = new Event("change", { bubbles: true });
+ Object.defineProperty(e, "target", {
+ writable: true,
+ value: {
+ ...innerRef.current,
+ value: undoRedo.state,
+ },
+ });
+
+ Object.defineProperty(e, "currentTarget", {
+ writable: true,
+ value: {
+ ...innerRef.current,
+ value: undoRedo.state,
+ },
+ });
+ const syntheticEvent = createSyntheticEvent(
+ e,
+ ) as React.ChangeEvent;
+
+ queueMicrotask(() => onChange(syntheticEvent));
+ setCallChange(false);
+ } else if (callChange) {
+ setCallChange(false);
+ }
+ }, [callChange, undoRedo.state, onChange, value]);
+
return (
(
+ event: E,
+): React.SyntheticEvent => {
+ let isDefaultPrevented = false;
+ let isPropagationStopped = false;
+ const preventDefault = () => {
+ isDefaultPrevented = true;
+ event.preventDefault();
+ };
+ const stopPropagation = () => {
+ isPropagationStopped = true;
+ event.stopPropagation();
+ };
+ return {
+ nativeEvent: event,
+ currentTarget: event.currentTarget as EventTarget & T,
+ target: event.target as EventTarget & T,
+ bubbles: event.bubbles,
+ cancelable: event.cancelable,
+ defaultPrevented: event.defaultPrevented,
+ eventPhase: event.eventPhase,
+ isTrusted: event.isTrusted,
+ preventDefault,
+ isDefaultPrevented: () => isDefaultPrevented,
+ stopPropagation,
+ isPropagationStopped: () => isPropagationStopped,
+ persist: () => ({}),
+ timeStamp: event.timeStamp,
+ type: event.type,
+ };
+};