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, + }; +};