Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ appId: swmansion.enriched.example
- pressKey: Enter
- inputText: "Numbered item three"
- pressKey: Enter
- pressKey: Enter

- pressKey: Backspace

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .playwright/screenshots/list-keyboard-enter-bullet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .playwright/screenshots/list-keyboard-enter-checkbox.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .playwright/screenshots/list-keyboard-enter-ordered.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 13 additions & 5 deletions .playwright/tests/listKeyboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ for (const {
await gotoVisualRegression(page);
});

test('Enter extends list', async ({ page }) => {
test('Enter extends list, Enter on empty item ends it', async ({
page,
}) => {
const editor = editorLocator(page);
const wrapper = page.locator(wrapperSelector);
const items = wrapper.locator('li');
Expand All @@ -64,12 +66,18 @@ for (const {
await items.first().click();
await editor.press('End');

for (let i = 0; i < 3; i++) {
await editor.press('Enter', { delay: KEY_ACTION_DELAY });
}
await editor.press('Enter', { delay: KEY_ACTION_DELAY });

await page.waitForTimeout(SCREENSHOT_STABILIZE_MS);
await expect(editor).toHaveScreenshot(`list-keyboard-enter-${label}.png`);

await editor.press('Enter', { delay: KEY_ACTION_DELAY });
await page.keyboard.type('Plain');

await page.waitForTimeout(SCREENSHOT_STABILIZE_MS);
await expect(editor).toHaveScreenshot(
`list-keyboard-enter-exit-${label}.png`
);
});

test('Enter causes scrolling', async ({ page }) => {
Expand All @@ -86,7 +94,7 @@ for (const {
await items.last().click();
await page.keyboard.press('Enter', { delay: KEY_ACTION_DELAY });

await page.keyboard.type('\n\n\n\n\n', { delay: 10 });
await page.keyboard.type('d\ne\nf\ng\nh\n', { delay: 10 });

await page.waitForTimeout(SCREENSHOT_STABILIZE_MS);
await expect(editor).toHaveScreenshot(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,15 @@ class ListStyles(
}

if (!isBackspace && isNewLine && isPreviousParagraphList(s, start, config.clazz)) {
// Enter in an empty list item ends the list instead of adding another empty item
val (previousStart, previousEnd) = s.getParagraphBounds(start - 1)
if (s.substring(previousStart, previousEnd) == EnrichedConstants.ZWS_STRING) {
Comment thread
szydlovsky marked this conversation as resolved.
Resolved
s.delete(start - 1, start)
removeSpansForRange(s, previousStart, previousEnd, config.clazz)
view.selection?.validateStyles()
return
}

// Check if the span from the previous line "leaked" into this one
if (spans.isNotEmpty()) {
val existingSpan = spans[0]
Expand Down
5 changes: 5 additions & 0 deletions ios/EnrichedTextInputView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2104,6 +2104,11 @@ - (bool)textView:(UITextView *)textView
[ZeroWidthSpaceUtils handleBackspaceInRange:range
replacementText:text
host:self] ||
// Enter in an empty list item ends the list. Has to run before the
// checkbox newline handling.
[ParagraphAttributesUtils handleNewlineInEmptyListItem:range
replacementText:text
input:self] ||
[cbLStyle handleNewlinesInRange:range replacementText:text] ||
[h1Style handleNewlinesInRange:range replacementText:text] ||
[h2Style handleNewlinesInRange:range replacementText:text] ||
Expand Down
3 changes: 3 additions & 0 deletions ios/utils/ParagraphAttributesUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
+ (BOOL)handleResetTypingAttributesOnBackspace:(NSRange)range
replacementText:(NSString *)text
input:(id)input;
+ (BOOL)handleNewlineInEmptyListItem:(NSRange)range
replacementText:(NSString *)text
input:(id)input;
+ (BOOL)isParagraphEmpty:(NSRange)range inString:(NSString *)string;
+ (void)resetTypingAttributes:(EnrichedTextInputView *)input
preservingAlignment:(NSTextAlignment)alignment;
Expand Down
60 changes: 60 additions & 0 deletions ios/utils/ParagraphAttributesUtils.mm
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,66 @@ + (void)resetTypingAttributes:(EnrichedTextInputView *)input
input->textView.typingAttributes = resetAttrs;
}

// Pressing Enter in an empty list item (unordered, ordered or checkbox) ends
// the list instead of adding yet another empty item.
+ (BOOL)handleNewlineInEmptyListItem:(NSRange)range
replacementText:(NSString *)text
input:(id)input {
EnrichedTextInputView *typedInput = (EnrichedTextInputView *)input;
if (typedInput == nullptr) {
return NO;
}

// Don't intervene during IME composition.
if (typedInput->textView.markedTextRange != nullptr) {
return NO;
}

// A single newline character only.
if (text.length != 1 || ![[NSCharacterSet newlineCharacterSet]
characterIsMember:[text characterAtIndex:0]]) {
return NO;
}

NSString *string = typedInput->textView.textStorage.string;
NSRange paragraphRange = [string paragraphRangeForRange:range];

// Content of the paragraph without its trailing newline.
NSUInteger contentLength = paragraphRange.length;
if (contentLength > 0 &&
[[NSCharacterSet newlineCharacterSet]
characterIsMember:[string
characterAtIndex:NSMaxRange(paragraphRange) -
1]]) {
contentLength -= 1;
}

// An empty list item consists of exactly one zero-width space.
if (contentLength != 1 ||
[string characterAtIndex:paragraphRange.location] != 0x200B) {
return NO;
}
// We want the caret before or after the ZWS, or have the ZWS itself selected
// - a selection that also covers the trailing newline isn't handled here.
if (NSMaxRange(range) > paragraphRange.location + 1) {
return NO;
}

NSArray<NSNumber *> *listTypes = @[
@([UnorderedListStyle getType]), @([OrderedListStyle getType]),
@([CheckboxListStyle getType])
];
for (NSNumber *type in listTypes) {
StyleBase *style = typedInput->stylesDict[type];
if (style != nullptr && [style detect:paragraphRange]) {
[style remove:paragraphRange withDirtyRange:YES];
return YES;
}
}

return NO;
}

+ (BOOL)isParagraphEmpty:(NSRange)range inString:(NSString *)string {
if (range.length == 0)
return YES;
Expand Down
28 changes: 6 additions & 22 deletions src/web/formats/listKeyboard.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import { isTextSelection, type Editor, type JSONContent } from '@tiptap/core';
import { isTextSelection, type Editor } from '@tiptap/core';

import { lineStartBackspace } from './wrappedBlockKeyboard';
import { withPreservedAlignment } from './formatRules';

function emptyListItemContent(itemName: string): JSONContent {
return {
type: itemName,
content: [{ type: 'paragraph' }],
};
}

// Enter: always extend the list — splitListItem fails on an empty last item (see TipTap splitListItem).
// Enter: extend the list — splitListItem fails on an empty item (see TipTap splitListItem),
// in which case the item gets lifted out, ending the list.
export function listEnter(editor: Editor, itemName: string): boolean {
if (!editor.isActive(itemName)) {
return false;
Expand All @@ -29,19 +23,9 @@ export function listEnter(editor: Editor, itemName: string): boolean {
return false;
}

// Flat lists only: list item is always the parent block of the paragraph (depth − 1).
const itemDepth = $from.depth - 1;
if (itemDepth < 1) {
return false;
}
const insertPos = $from.after(itemDepth);

return editor
.chain()
.focus()
.insertContentAt(insertPos, emptyListItemContent(itemName))
.scrollIntoView()
.run();
return withPreservedAlignment(editor, editor.chain(), (c) =>
c.focus().liftListItem(itemName)
);
}

// Backspace: first press at line start lifts the list item; second press (paragraph below list) joins backward.
Expand Down
Loading