Skip to content

Commit

Permalink
fix(undo/redo): send on change event when undoing / redoing
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcMcIntosh committed Feb 25, 2024
1 parent c0cf019 commit 258d667
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 20 deletions.
2 changes: 2 additions & 0 deletions src/components/ComboBox/ComboBox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<App onSubmit={submitSpy} />);
Expand Down
30 changes: 12 additions & 18 deletions src/components/ComboBox/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,6 @@ export const ComboBox: React.FC<ComboBoxProps> = ({
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;
Expand All @@ -103,6 +90,15 @@ export const ComboBox: React.FC<ComboBoxProps> = ({
}
}, [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<HTMLTextAreaElement>) => {
const state = combobox.getState();

Expand Down Expand Up @@ -153,11 +149,9 @@ export const ComboBox: React.FC<ComboBoxProps> = ({
}

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) {
Expand All @@ -166,7 +160,7 @@ export const ComboBox: React.FC<ComboBoxProps> = ({
setSelectedCommand("");
}
combobox.show();
} else if (wasDelete && !maybeCommand) {
} else if (wasDelete) {
setTrigger("");
setSelectedCommand("");
setStartPosition(null);
Expand Down
44 changes: 42 additions & 2 deletions src/components/TextArea/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof RadixTextArea> &
Expand All @@ -12,6 +18,7 @@ export type TextAreaProps = React.ComponentProps<typeof RadixTextArea> &

export const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
({ onTextAreaHeightChange, value, onKeyDown, onChange, ...props }, ref) => {
const [callChange, setCallChange] = React.useState(true);
const innerRef = useRef<HTMLTextAreaElement>(null);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
useImperativeHandle(ref, () => innerRef.current!, []);
Expand All @@ -22,11 +29,13 @@ export const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
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) {
Expand All @@ -51,10 +60,41 @@ export const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
}, [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<HTMLTextAreaElement>;

queueMicrotask(() => onChange(syntheticEvent));
setCallChange(false);
} else if (callChange) {
setCallChange(false);
}
}, [callChange, undoRedo.state, onChange, value]);

return (
<RadixTextArea
{...props}
Expand Down
31 changes: 31 additions & 0 deletions src/utils/createSyntheticEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export const createSyntheticEvent = <T extends Element, E extends Event>(
event: E,
): React.SyntheticEvent<T, E> => {
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,
};
};

0 comments on commit 258d667

Please sign in to comment.