elements as newlines (contentEditable creates these for Enter)
+ displayValue += "\n";
+ dataValue += "\n";
+ currentIndex += 1;
} else {
+ // Handle
with content and other block elements that should add newlines
+ const isBlockElement = ["div", "p", "h1", "h2", "h3", "h4", "h5", "h6"].includes(tagName);
+ const hasContent = element.childNodes.length > 0;
+
+ // Add newline before block element (except for the first element)
+ if (isBlockElement && hasContent && (displayValue.length > 0 || dataValue.length > 0)) {
+ displayValue += "\n";
+ dataValue += "\n";
+ currentIndex += 1;
+ }
+
// Recursively process child nodes
for (const child of Array.from(element.childNodes)) {
walkNodes(child);
}
+
+ // Add newline after block element (except for the last element in the container)
+ if (isBlockElement && hasContent && element.nextSibling) {
+ displayValue += "\n";
+ dataValue += "\n";
+ currentIndex += 1;
+ }
}
}
};
diff --git a/packages/mentis/src/utils/reconstructFromDataValue.ts b/packages/mentis/src/utils/reconstructFromDataValue.ts
index 9bb8faf..4398aa6 100644
--- a/packages/mentis/src/utils/reconstructFromDataValue.ts
+++ b/packages/mentis/src/utils/reconstructFromDataValue.ts
@@ -77,11 +77,17 @@ export const reconstructFromDataValue = ({
}
}
+ // Helper function to convert newlines to HTML
+ const convertNewlinesToHTML = (text: string): string => {
+ return text.replace(/\n/g, '
');
+ };
+
// Build the HTML content
for (const match of filteredMatches) {
// Add any text before this mention
if (match.index > currentIndex) {
- result += dataValue.slice(currentIndex, match.index);
+ const textBeforeMention = dataValue.slice(currentIndex, match.index);
+ result += convertNewlinesToHTML(textBeforeMention);
}
// Add the mention chip
@@ -91,7 +97,7 @@ export const reconstructFromDataValue = ({
result += `
${chipContent}`;
} else {
// If we can't find the option, just add the value as plain text
- result += match.value;
+ result += convertNewlinesToHTML(match.value);
}
currentIndex = match.index + match.length;
@@ -99,7 +105,8 @@ export const reconstructFromDataValue = ({
// Add any remaining text after the last mention
if (currentIndex < dataValue.length) {
- result += dataValue.slice(currentIndex);
+ const remainingText = dataValue.slice(currentIndex);
+ result += convertNewlinesToHTML(remainingText);
}
return result;
diff --git a/packages/mentis/tests/newlines.test.tsx b/packages/mentis/tests/newlines.test.tsx
new file mode 100644
index 0000000..360d3f7
--- /dev/null
+++ b/packages/mentis/tests/newlines.test.tsx
@@ -0,0 +1,131 @@
+import React from "react";
+import { render, screen, fireEvent } from "@testing-library/react";
+import { expect, test, vi, describe } from "vitest";
+import userEvent from "@testing-library/user-event";
+import { MentionInput } from "../src/components/MentionInput";
+import { extractMentionData } from "../src/utils/extractMentionData";
+import { reconstructFromDataValue } from "../src/utils/reconstructFromDataValue";
+
+const options = [
+ { label: "John Doe", value: "john" },
+ { label: "Jane Smith", value: "jane" },
+];
+
+describe("Newline handling", () => {
+ test("Should preserve newlines in dataValue and displayValue when pressing Enter", async () => {
+ const mockOnChange = vi.fn();
+ render(
);
+
+ const user = userEvent.setup();
+ const editorElement = screen.getByRole("combobox");
+
+ // Type some text, then press Enter, then type more text
+ await user.type(editorElement, "First line{enter}Second line");
+
+ // Check that onChange was called with newlines preserved
+ expect(mockOnChange).toHaveBeenCalledWith(
+ expect.objectContaining({
+ displayValue: "First line\nSecond line",
+ dataValue: "First line\nSecond line",
+ mentions: [],
+ })
+ );
+ });
+
+ test("Should extract newlines correctly from contentEditable with
elements", () => {
+ // Create a test element with
tags
+ const testElement = document.createElement("div");
+ testElement.innerHTML = "First line
Second line
Third line";
+
+ const result = extractMentionData(testElement);
+
+ expect(result.displayValue).toBe("First line\nSecond line\nThird line");
+ expect(result.dataValue).toBe("First line\nSecond line\nThird line");
+ expect(result.mentions).toEqual([]);
+ });
+
+ test("Should extract newlines correctly from contentEditable with empty
elements", () => {
+ // Create a test element with empty div tags (contentEditable creates these for Enter)
+ const testElement = document.createElement("div");
+ testElement.innerHTML = "First line
Second line";
+
+ const result = extractMentionData(testElement);
+
+ expect(result.displayValue).toBe("First line\nSecond line");
+ expect(result.dataValue).toBe("First line\nSecond line");
+ expect(result.mentions).toEqual([]);
+ });
+
+ test("Should reconstruct HTML correctly with newlines", () => {
+ const dataValue = "First line\nSecond line\nThird line";
+
+ const result = reconstructFromDataValue({
+ dataValue,
+ options,
+ trigger: "@",
+ keepTriggerOnSelect: true,
+ chipClassName: "mention-chip",
+ });
+
+ expect(result).toBe("First line
Second line
Third line");
+ });
+
+ test("Should handle newlines with mentions correctly", () => {
+ // Create a test element with mentions and newlines
+ const testElement = document.createElement("div");
+ testElement.innerHTML = 'Hello
@John DoeHow are you?';
+
+ const result = extractMentionData(testElement);
+
+ expect(result.displayValue).toBe("Hello @John Doe\nHow are you?");
+ expect(result.dataValue).toBe("Hello john\nHow are you?");
+ expect(result.mentions).toEqual([
+ {
+ label: "John Doe",
+ value: "john",
+ startIndex: 6,
+ endIndex: 15,
+ },
+ ]);
+ });
+
+ test("Should reconstruct HTML correctly with mentions and newlines", () => {
+ const dataValue = "Hello john\nHow are you?";
+
+ const result = reconstructFromDataValue({
+ dataValue,
+ options,
+ trigger: "@",
+ keepTriggerOnSelect: true,
+ chipClassName: "mention-chip",
+ });
+
+ expect(result).toBe('Hello
@John DoeHow are you?');
+ });
+
+ test("Should handle multiple newlines correctly", () => {
+ // Create a test element with multiple newlines
+ const testElement = document.createElement("div");
+ testElement.innerHTML = "Line 1
Line 3
Line 6";
+
+ const result = extractMentionData(testElement);
+
+ expect(result.displayValue).toBe("Line 1\n\nLine 3\n\n\nLine 6");
+ expect(result.dataValue).toBe("Line 1\n\nLine 3\n\n\nLine 6");
+ expect(result.mentions).toEqual([]);
+ });
+
+ test("Should reconstruct multiple newlines correctly", () => {
+ const dataValue = "Line 1\n\nLine 3\n\n\nLine 6";
+
+ const result = reconstructFromDataValue({
+ dataValue,
+ options,
+ trigger: "@",
+ keepTriggerOnSelect: true,
+ chipClassName: "mention-chip",
+ });
+
+ expect(result).toBe("Line 1
Line 3
Line 6");
+ });
+});