Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
@@ -0,0 +1,45 @@
appId: swmansion.enriched.example
---
# fix PR #637 - mention popups not closing when switching between mentions

- launchApp

- tapOn:
id: "toggle-screen-button"

- tapOn:
id: "editor-input"

- inputText: "mentions #gen @J"

# user popup visible
- runFlow:
file: "../subflows/capture_or_assert_fullscreen_screenshot.yaml"
env:
SCREENSHOT_NAME: "mention_popup_closing_on_cursor_travel_1"

- tapOn:
id: "focus-button"

- tapOn:
id: "editor-input"
point: "30%, 50%"

# channel popup visible
- runFlow:
file: "../subflows/capture_or_assert_fullscreen_screenshot.yaml"
env:
SCREENSHOT_NAME: "mention_popup_closing_on_cursor_travel_2"

- tapOn:
id: "focus-button"

- tapOn:
id: "editor-input"
point: "10%, 50%"

# no popup visible
- runFlow:
file: "../subflows/capture_or_assert_fullscreen_screenshot.yaml"
env:
SCREENSHOT_NAME: "mention_popup_closing_on_cursor_travel_3"
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.
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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
appId: swmansion.enriched.example
---
- tapOn:
id: 'blur-button'

- runFlow:
file: '../../subflows/capture_or_assert_screenshot.yaml'
env:
ELEMENT_ID: 'full-screen'
SCREENSHOT_PREFIX: 'enrichedInput'
50 changes: 50 additions & 0 deletions .playwright/tests/mentions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const sel = {
eventType: '[data-testid="mention-event-type"]',
eventIndicator: '[data-testid="mention-event-indicator"]',
eventText: '[data-testid="mention-event-text"]',
lastEndEvent: '[data-testid="mention-last-end-event"]',
htmlOutput: '[data-testid="mention-html-output"]',
detectedCount: '[data-testid="mention-detected-count"]',
detectedText: '[data-testid="mention-detected-text"]',
Expand Down Expand Up @@ -41,6 +42,9 @@ function eventIndicator(page: Page) {
function eventText(page: Page) {
return page.locator(sel.eventText);
}
function lastEndEvent(page: Page) {
return page.locator(sel.lastEndEvent);
}
function htmlOutput(page: Page) {
return page.locator(sel.htmlOutput);
}
Expand Down Expand Up @@ -263,3 +267,49 @@ test('mention renders correctly', async ({ page }) => {
);
await expect(editorLocator(page)).toHaveScreenshot('mention-visual.png');
});

test('switching to a different mention starts it and ends the previous one', async ({
page,
}) => {
await gotoMentionTest(page);
const editor = mentionEditor(page);
await editor.click();
await editor.pressSequentially('foo #g ', { delay: 80 });
await expect(eventType(page)).toHaveText('change');
await expect(eventIndicator(page)).toHaveText('#');
await editor.pressSequentially('@', { delay: 80 });
await expect(eventType(page)).toHaveText('start');
await expect(eventIndicator(page)).toHaveText('@');
await expect(lastEndEvent(page)).toHaveText('#');
await editor.press('ArrowLeft');
await editor.press('ArrowLeft'); // back to the '#' mention
await expect(eventType(page)).toHaveText('change');
await expect(eventIndicator(page)).toHaveText('#');
await expect(lastEndEvent(page)).toHaveText('@');
await editor.press('ArrowLeft');
await editor.press('ArrowLeft');
await editor.press('ArrowLeft'); // leaving the '#' mention
await expect(eventType(page)).toHaveText('end');
await expect(eventIndicator(page)).toHaveText('#');
});

test("inserting a mention between text doesn't produce a double space", async ({
page,
}) => {
await gotoMentionTest(page);
const editor = mentionEditor(page);
await editor.click();
await editor.pressSequentially('example ', { delay: 80 });
await editor.pressSequentially(' test', { delay: 80 });
for (let i = 0; i < 5; i++) {
await editor.press('ArrowLeft');
}
await editor.press('@');
await page.locator(sel.setUserButton).click();
await page.waitForTimeout(2000);
await expect
.poll(async () => await htmlOutput(page).textContent())
.toEqual(
'<html><p>example <mention text="Jane" indicator="@" id="1">Jane</mention> test</p></html>'
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class EnrichedTextInputView :
val alignmentStyles: AlignmentStyles? = AlignmentStyles(this)
var isDuringTransaction: Boolean = false
var isRemovingMany: Boolean = false
var recentInputString: String = ""
var scrollEnabled: Boolean = true
var allowFontScaling: Boolean = EnrichedConstants.ALLOW_FONT_SCALING_DEFAULT
set(value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,30 @@ class MentionHandler(
indicator: String,
text: String?,
) {
var startMention = false

// switching directly to an active mention
if (previousIndicator != indicator) {
startMention = true
endMention()
}

// explicit startMention event before changeMention event
if (startMention && !text.isNullOrEmpty()) {
emitEvent(indicator, "")
}

emitEvent(indicator, text)
previousIndicator = indicator
}

private fun emitEvent(
indicator: String,
text: String?,
) {
// Do not emit events too often
if (previousText == text) return
if (previousIndicator == indicator && previousText == text) return

previousIndicator = indicator
previousText = text
val context = view.context as ReactContext
val surfaceId = UIManagerHelper.getSurfaceId(context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ class OnMentionEvent(
) : Event<OnMentionEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME

// start/change/end can be emitted as a burst within a single frame
// (e.g. when switching mentions: end -> start -> change).
// The default coalescing would merge them in the batch and drop the
// intermediate ones, so it must be disabled to deliver every event in order.
override fun canCoalesce(): Boolean = false

override fun getEventData(): WritableMap? {
val eventData: WritableMap = Arguments.createMap()
eventData.putString("indicator", indicator)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,23 @@ class ParametrizedStyles(
endCursorPosition: Int,
) {
afterTextChangedLinks(startCursorPosition, endCursorPosition)
afterTextChangedMentions(s, startCursorPosition)
detectActiveMention(s, startCursorPosition)
}

// Re-runs in-progress mention detection on a pure caret move (no text change),
fun afterSelectionChangedMentions(
start: Int,
end: Int,
) {
val s = view.text ?: return

// A non-collapsed selection can't be editing a single mention.
if (start != end) {
view.mentionHandler?.endMention()
return
}

detectActiveMention(s, end)
}

fun onStyleToggled(
Expand Down Expand Up @@ -239,7 +255,7 @@ class ParametrizedStyles(
detectLinksInRange(spannable, affectedRange.first, affectedRange.last)
}

private fun afterTextChangedMentions(
private fun detectActiveMention(
s: CharSequence,
endCursorPosition: Int,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ class EnrichedSelection(
start = finalStart
end = finalEnd
validateStyles()

if (view.text?.toString() == view.recentInputString) {
view.parametrizedStyles?.afterSelectionChangedMentions(finalStart, finalEnd)
}

emitSelectionChangeEvent(view.text, finalStart, finalEnd)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ class EnrichedTextWatcher(
if (s == null) return
emitEvents(s)

view.recentInputString = s.toString()

if (view.isDuringTransaction) return
applyStyles(s)
view.layoutManager.invalidateLayout()
Expand Down
2 changes: 0 additions & 2 deletions apps/example-web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,8 @@ function App() {
console.log('[EnrichedTextInput] Change mention', indicator, text);
if (indicator === '@') {
userMention.onMentionChange(text);
if (!isUserPopupOpen) setIsUserPopupOpen(true);
} else {
channelMention.onMentionChange(text);
if (!isChannelPopupOpen) setIsChannelPopupOpen(true);
}
};

Expand Down
9 changes: 7 additions & 2 deletions apps/example-web/src/testScreens/TestMentions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function TestMentions() {
const [detectedCount, setDetectedCount] = useState(0);
const [detectedText, setDetectedText] = useState('');
const [detectedIndicator, setDetectedIndicator] = useState('');
const [lastEndEvent, setLastEndEvent] = useState('');

const preventDefault = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
Expand All @@ -40,10 +41,11 @@ export function TestMentions() {
setEventIndicator(indicator);
setEventText(text);
}}
onEndMention={() => {
onEndMention={(indicator) => {
setEventType('end');
setEventIndicator('');
setEventIndicator(indicator);
setEventText('');
setLastEndEvent(indicator);
}}
onMentionDetected={({ text, indicator }) => {
setDetectedCount((c) => c + 1);
Expand All @@ -65,6 +67,9 @@ export function TestMentions() {
<Field label="event text">
<span data-testid="mention-event-text">{eventText}</span>
</Field>
<Field label="last end event">
<span data-testid="mention-last-end-event">{lastEndEvent}</span>
</Field>
<Field label="detected count">
<span data-testid="mention-detected-count">{detectedCount}</span>
</Field>
Expand Down
16 changes: 15 additions & 1 deletion apps/example/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,21 @@ const root = path.resolve(__dirname, '../..');
*
* @type {import('metro-config').MetroConfig}
*/
module.exports = withMetroConfig(getDefaultConfig(__dirname), {
const config = withMetroConfig(getDefaultConfig(__dirname), {
root,
dirname: __dirname,
});

config.resolver = {
...config.resolver,
blockList: [
...(Array.isArray(config.resolver?.blockList)
? config.resolver.blockList
: config.resolver?.blockList
? [config.resolver.blockList]
: []),
/.*\/\.maestro\/.*/,
],
};

module.exports = config;
6 changes: 3 additions & 3 deletions apps/example/src/hooks/useEditorState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export function useEditorState() {
};

const handleStartMention = (indicator: string) => {
console.log('Start Mention', indicator);
if (indicator === '@') {
userMention.onMentionChange('');
openUserMentionPopup();
Expand All @@ -115,6 +116,7 @@ export function useEditorState() {
};

const handleEndMention = (indicator: string) => {
console.log('End Mention', indicator);
if (indicator === '@') {
closeUserMentionPopup();
userMention.onMentionChange('');
Expand All @@ -125,12 +127,10 @@ export function useEditorState() {
};

const handleChangeMention = ({ indicator, text }: OnChangeMentionEvent) => {
console.log('Change Mention', indicator, text);
indicator === '@'
? userMention.onMentionChange(text)
: channelMention.onMentionChange(text);
indicator === '@'
? !isUserPopupOpen && setIsUserPopupOpen(true)
: !isChannelPopupOpen && setIsChannelPopupOpen(true);
};

const handleUserMentionSelected = (item: MentionItem) => {
Expand Down
16 changes: 10 additions & 6 deletions apps/example/src/screens/TestScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ export function TestScreen({
const [sizeMode, setSizeMode] = useState<'base' | 'max'>('base');

return (
<>
<View style={styles.container}>
<ScrollView
style={styles.container}
contentContainerStyle={styles.content}
style={styles.scrollContainer}
contentContainerStyle={styles.scrollContent}
testID="full-screen"
>
<View style={styles.buttonStack}>
<Button
Expand Down Expand Up @@ -165,19 +166,22 @@ export function TestScreen({
isOpen={editor.isChannelPopupOpen}
onItemPress={editor.handleChannelMentionSelected}
/>
</>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'white',
paddingTop: 100,
},
scrollContainer: {
flex: 1,
},
content: {
scrollContent: {
flexGrow: 1,
padding: 16,
paddingTop: 100,
alignItems: 'center',
},
editor: {
Expand Down
5 changes: 2 additions & 3 deletions ios/EnrichedTextInputView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1722,12 +1722,11 @@ - (void)anyTextMayHaveBeenModified {
}

if (![textView.textStorage.string isEqualToString:_recentInputString]) {
_recentInputString = [textView.textStorage.string copy];

// emit onChangeText event
auto emitter = [self getEventEmitter];
if (emitter != nullptr && _emitTextChange) {
// set the recent input string only if the emitter is defined
_recentInputString = [textView.textStorage.string copy];

// emit string without zero width spaces
NSString *stringToBeEmitted = [[textView.textStorage.string
stringByReplacingOccurrencesOfString:@"\u200B"
Expand Down
Loading
Loading