Skip to content

Commit

Permalink
Merge pull request #38152 from software-mansion-labs/@Skalakid/live-m…
Browse files Browse the repository at this point in the history
…arkdown-input-for-web

Enable Live Markdown Preview on web
  • Loading branch information
thienlnam authored Mar 27, 2024
2 parents fecb709 + 8fe7c78 commit b2ab3a9
Show file tree
Hide file tree
Showing 5 changed files with 57 additions and 27 deletions.
4 changes: 2 additions & 2 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1363,7 +1363,7 @@ PODS:
- RNGoogleSignin (10.0.1):
- GoogleSignIn (~> 7.0)
- React-Core
- RNLiveMarkdown (0.1.5):
- RNLiveMarkdown (0.1.33):
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
Expand Down Expand Up @@ -1904,7 +1904,7 @@ SPEC CHECKSUMS:
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: 25b969a1ffc806b9f9ad2e170d4a3b049c6af85e
RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0
RNLiveMarkdown: 35eeecf7e57eb26fdc279d5d4815982a9a9f7beb
RNLiveMarkdown: aaf75630fb2129db43fb5a873d33125e7173f3a0
RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81
rnmapbox-maps: fcf7f1cbdc8bd7569c267d07284e8a5c7bee06ed
RNPermissions: 9b086c8f05b2e2faa587fdc31f4c5ab4509728aa
Expand Down
10 changes: 4 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
},
"dependencies": {
"@dotlottie/react-player": "^1.6.3",
"@expensify/react-native-live-markdown": "0.1.5",
"@expensify/react-native-live-markdown": "0.1.33",
"@expo/metro-runtime": "~3.1.1",
"@formatjs/intl-datetimeformat": "^6.10.0",
"@formatjs/intl-listformat": "^7.2.2",
Expand Down
58 changes: 49 additions & 9 deletions src/components/Composer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import lodashDebounce from 'lodash/debounce';
import type {BaseSyntheticEvent, ForwardedRef} from 'react';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {flushSync} from 'react-dom';
// eslint-disable-next-line no-restricted-imports
import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputSelectionChangeEventData} from 'react-native';
import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputSelectionChangeEventData, TextStyle} from 'react-native';
import {StyleSheet, View} from 'react-native';
import type {AnimatedTextInputRef} from '@components/RNTextInput';
import RNTextInput from '@components/RNTextInput';
import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput';
import RNMarkdownTextInput from '@components/RNMarkdownTextInput';
import Text from '@components/Text';
import useHtmlPaste from '@hooks/useHtmlPaste';
import useIsScrollBarVisible from '@hooks/useIsScrollBarVisible';
import useMarkdownStyle from '@hooks/useMarkdownStyle';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
Expand Down Expand Up @@ -79,10 +81,11 @@ function Composer(
) {
const theme = useTheme();
const styles = useThemeStyles();
const markdownStyle = useMarkdownStyle();
const StyleUtils = useStyleUtils();
const {windowWidth} = useWindowDimensions();
const textRef = useRef<HTMLElement & RNText>(null);
const textInput = useRef<AnimatedTextInputRef | null>(null);
const textInput = useRef<AnimatedMarkdownTextInputRef | null>(null);
const [numberOfLines, setNumberOfLines] = useState(numberOfLinesProp);
const [selection, setSelection] = useState<
| {
Expand All @@ -97,7 +100,9 @@ function Composer(
const [caretContent, setCaretContent] = useState('');
const [valueBeforeCaret, setValueBeforeCaret] = useState('');
const [textInputWidth, setTextInputWidth] = useState('');
const [isRendered, setIsRendered] = useState(false);
const isScrollBarVisible = useIsScrollBarVisible(textInput, value ?? '');
const [prevScroll, setPrevScroll] = useState<number | undefined>();

useEffect(() => {
if (!shouldClear) {
Expand All @@ -123,7 +128,7 @@ function Composer(
const addCursorPositionToSelectionChange = (event: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
const webEvent = event as BaseSyntheticEvent<TextInputSelectionChangeEventData>;

if (shouldCalculateCaretPosition) {
if (shouldCalculateCaretPosition && isRendered) {
// we do flushSync to make sure that the valueBeforeCaret is updated before we calculate the caret position to receive a proper position otherwise we will calculate position for the previous state
flushSync(() => {
setValueBeforeCaret(webEvent.target.value.slice(0, webEvent.nativeEvent.selection.start));
Expand Down Expand Up @@ -158,18 +163,19 @@ function Composer(
(event: ClipboardEvent) => {
const isVisible = checkComposerVisibility();
const isFocused = textInput.current?.isFocused();
const isContenteditableDivFocused = document.activeElement?.nodeName === 'DIV' && document.activeElement?.hasAttribute('contenteditable');

if (!(isVisible || isFocused)) {
return true;
}

if (textInput.current !== event.target) {
if (textInput.current !== event.target && !(isContenteditableDivFocused && !event.clipboardData?.files.length)) {
const eventTarget = event.target as HTMLInputElement | HTMLTextAreaElement | null;

// To make sure the composer does not capture paste events from other inputs, we check where the event originated
// If it did originate in another input, we return early to prevent the composer from handling the paste
const isTargetInput = eventTarget?.nodeName === 'INPUT' || eventTarget?.nodeName === 'TEXTAREA' || eventTarget?.contentEditable === 'true';
if (isTargetInput) {
if (isTargetInput || (!isFocused && isContenteditableDivFocused && event.clipboardData?.files.length)) {
return true;
}

Expand Down Expand Up @@ -256,12 +262,44 @@ function Composer(
updateNumberOfLines();
}, [updateNumberOfLines]);

const currentNumberOfLines = useMemo(
() => (isComposerFullSize ? undefined : numberOfLines),

[isComposerFullSize, numberOfLines],
);

useEffect(() => {
if (!textInput.current) {
return;
}
const debouncedSetPrevScroll = lodashDebounce(() => {
if (!textInput.current) {
return;
}
setPrevScroll(textInput.current.scrollTop);
}, 100);

textInput.current.addEventListener('scroll', debouncedSetPrevScroll);
return () => {
textInput.current?.removeEventListener('scroll', debouncedSetPrevScroll);
};
}, []);

useEffect(() => {
if (!textInput.current || prevScroll === undefined) {
return;
}
textInput.current.scrollTop = prevScroll;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isComposerFullSize]);

useHtmlPaste(textInput, handlePaste, true);

useEffect(() => {
if (typeof ref === 'function') {
ref(textInput.current);
}
setIsRendered(true);

return () => {
if (isReportActionCompose) {
Expand Down Expand Up @@ -321,27 +359,29 @@ function Composer(
StyleUtils.getComposeTextAreaPadding(numberOfLines, isComposerFullSize),
Browser.isMobileSafari() || Browser.isSafari() ? styles.rtlTextRenderForSafari : {},
scrollStyleMemo,
isComposerFullSize ? ({height: '100%', maxHeight: 'none' as DimensionValue} as TextStyle) : undefined,
],

[numberOfLines, scrollStyleMemo, styles.rtlTextRenderForSafari, style, StyleUtils, isComposerFullSize],
);

return (
<>
<RNTextInput
<RNMarkdownTextInput
autoComplete="off"
autoCorrect={!Browser.isMobileSafari()}
placeholderTextColor={theme.placeholderText}
ref={(el) => (textInput.current = el)}
selection={selection}
style={inputStyleMemo}
markdownStyle={markdownStyle}
value={value}
defaultValue={defaultValue}
autoFocus={autoFocus}
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...props}
onSelectionChange={addCursorPositionToSelectionChange}
numberOfLines={numberOfLines}
numberOfLines={currentNumberOfLines}
disabled={isDisabled}
onKeyPress={handleKeyPress}
onFocus={(e) => {
Expand Down
10 changes: 1 addition & 9 deletions src/pages/home/report/ReportActionCompose/SuggestionMention.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {usePersonalDetails} from '@components/OnyxProvider';
import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
import * as LoginUtils from '@libs/LoginUtils';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as SuggestionsUtils from '@libs/SuggestionUtils';
Expand Down Expand Up @@ -44,7 +43,6 @@ function SuggestionMention(
) {
const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT;
const {translate, formatPhoneNumber} = useLocalize();
const previousValue = usePrevious(value);
const [suggestionValues, setSuggestionValues] = useState(defaultSuggestionsValues);

const currentUserPersonalDetails = useCurrentUserPersonalDetails();
Expand Down Expand Up @@ -281,14 +279,8 @@ function SuggestionMention(
);

useEffect(() => {
if (value.length < previousValue.length) {
// A workaround to not show the suggestions list when the user deletes a character before the mention.
// It is caused by a buggy behavior of the TextInput on iOS. Should be fixed after migration to Fabric.
// See: https://github.com/facebook/react-native/pull/36930#issuecomment-1593028467
return;
}
calculateMentionSuggestion(selection.end);
}, [selection, value, previousValue, calculateMentionSuggestion]);
}, [selection, calculateMentionSuggestion]);

const updateShouldShowSuggestionMenuToFalse = useCallback(() => {
setSuggestionValues((prevState) => {
Expand Down

0 comments on commit b2ab3a9

Please sign in to comment.