1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-07-16 00:14:34 +02:00

iOS: Fixes #11711: Fix Markdown toolbar partially covered by keyboard on some iOS devices (#12027)

This commit is contained in:
Henry Heino
2025-03-29 05:46:37 -07:00
committed by GitHub
parent 6bc1965ec0
commit 18ebd16428
8 changed files with 86 additions and 258 deletions

View File

@ -686,7 +686,6 @@ packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenuContentNote.js
packages/app-mobile/components/TextInput.js
packages/app-mobile/components/ToggleSpaceButton.js
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
packages/app-mobile/components/accessibility/AccessibleView.test.js
packages/app-mobile/components/accessibility/AccessibleView.js
@ -859,7 +858,7 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
packages/app-mobile/utils/getPackageInfo.js
packages/app-mobile/utils/getVersionInfoText.js
packages/app-mobile/utils/hooks/useKeyboardVisible.js
packages/app-mobile/utils/hooks/useKeyboardState.js
packages/app-mobile/utils/hooks/useOnLongPressProps.js
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
packages/app-mobile/utils/image/fileToImage.web.js

3
.gitignore vendored
View File

@ -661,7 +661,6 @@ packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenuContentNote.js
packages/app-mobile/components/TextInput.js
packages/app-mobile/components/ToggleSpaceButton.js
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
packages/app-mobile/components/accessibility/AccessibleView.test.js
packages/app-mobile/components/accessibility/AccessibleView.js
@ -834,7 +833,7 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
packages/app-mobile/utils/getPackageInfo.js
packages/app-mobile/utils/getVersionInfoText.js
packages/app-mobile/utils/hooks/useKeyboardVisible.js
packages/app-mobile/utils/hooks/useKeyboardState.js
packages/app-mobile/utils/hooks/useOnLongPressProps.js
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
packages/app-mobile/utils/image/fileToImage.web.js

View File

@ -1,97 +0,0 @@
// On some devices, the SafeAreaView conflicts with the KeyboardAvoidingView, creating
// additional (or a lack of additional) space at the bottom of the screen. Because this
// is different on different devices, this button allows toggling additional space a the bottom
// of the screen to compensate.
// Works around https://github.com/facebook/react-native/issues/13393 by adding additional
// space below the given component when the keyboard is visible unless a button is pressed.
import Setting from '@joplin/lib/models/Setting';
import { themeStyle } from '@joplin/lib/theme';
import * as React from 'react';
import { ReactNode, useCallback, useState, useEffect } from 'react';
import { Platform, useWindowDimensions, View, ViewStyle } from 'react-native';
import IconButton from './IconButton';
import useKeyboardVisible from '../utils/hooks/useKeyboardVisible';
interface Props {
children: ReactNode;
themeId: number;
style?: ViewStyle;
}
const ToggleSpaceButton = (props: Props) => {
const [additionalSpace, setAdditionalSpace] = useState(0);
const [decreaseSpaceBtnVisible, setDecreaseSpaceBtnVisible] = useState(true);
// Some devices need space added, others need space removed.
const additionalPositiveSpace = 14;
const additionalNegativeSpace = -14;
// Switch from adding +14px to -14px.
const onDecreaseSpace = useCallback(() => {
setAdditionalSpace(additionalNegativeSpace);
setDecreaseSpaceBtnVisible(false);
Setting.setValue('editor.mobile.removeSpaceBelowToolbar', true);
}, [setAdditionalSpace, setDecreaseSpaceBtnVisible, additionalNegativeSpace]);
useEffect(() => {
if (Setting.value('editor.mobile.removeSpaceBelowToolbar')) {
onDecreaseSpace();
}
}, [onDecreaseSpace]);
const theme = themeStyle(props.themeId);
const decreaseSpaceButton = (
<>
<View style={{
height: additionalPositiveSpace,
zIndex: -2,
}} />
<IconButton
themeId={props.themeId}
description={'Move toolbar to bottom of screen'}
containerStyle={{
height: additionalPositiveSpace,
width: '100%',
// Ensure that the icon is near the bottom of the screen,
// and thus invisible on devices where it isn't necessary.
position: 'absolute',
bottom: 0,
// Don't show the button on top of views with content.
zIndex: -1,
alignItems: 'center',
}}
onPress={onDecreaseSpace}
iconName='material chevron-down'
iconStyle={{ color: theme.color }}
/>
</>
);
const { keyboardVisible } = useKeyboardVisible();
const windowSize = useWindowDimensions();
const isPortrait = windowSize.height > windowSize.width;
const spaceApplicable = keyboardVisible && Platform.OS === 'ios' && isPortrait;
const style: ViewStyle = {
marginBottom: spaceApplicable ? additionalSpace : 0,
...props.style,
};
return (
<View style={style}>
{props.children}
{ decreaseSpaceBtnVisible && spaceApplicable ? decreaseSpaceButton : null }
</View>
);
};
export default ToggleSpaceButton;

View File

@ -2,15 +2,12 @@ import * as React from 'react';
import { connect } from 'react-redux';
import NotesScreen from './screens/Notes';
import SearchScreen from './screens/SearchScreen';
import { Component } from 'react';
import { KeyboardAvoidingView, Keyboard, Platform, View, KeyboardEvent, Dimensions, EmitterSubscription } from 'react-native';
import { KeyboardAvoidingView, Platform, View } from 'react-native';
import { AppState } from '../utils/types';
import { themeStyle } from './global-style';
interface State {
autoCompletionBarExtraHeight: number;
floatingKeyboardEnabled: boolean;
}
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import useKeyboardState from '../utils/hooks/useKeyboardState';
import usePrevious from '@joplin/lib/hooks/usePrevious';
interface Props {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@ -22,65 +19,16 @@ interface Props {
themeId: number;
}
class AppNavComponent extends Component<Props, State> {
private previousRouteName_: string|null = null;
private keyboardDidShowListener: EmitterSubscription|null = null;
private keyboardDidHideListener: EmitterSubscription|null = null;
private keyboardWillChangeFrameListener: EmitterSubscription|null = null;
const AppNavComponent: React.FC<Props> = (props) => {
const keyboardState = useKeyboardState();
const safeAreaPadding = useSafeAreaInsets();
public constructor(props: Props) {
super(props);
this.previousRouteName_ = null;
this.state = {
autoCompletionBarExtraHeight: 0, // Extra padding for the auto completion bar at the top of the keyboard
floatingKeyboardEnabled: false,
};
}
public UNSAFE_componentWillMount() {
if (Platform.OS === 'ios') {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow.bind(this));
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide.bind(this));
this.keyboardWillChangeFrameListener = Keyboard.addListener('keyboardWillChangeFrame', this.keyboardWillChangeFrame);
}
}
public componentWillUnmount() {
this.keyboardDidShowListener?.remove();
this.keyboardDidHideListener?.remove();
this.keyboardWillChangeFrameListener?.remove();
this.keyboardDidShowListener = null;
this.keyboardDidHideListener = null;
this.keyboardWillChangeFrameListener = null;
}
public keyboardDidShow() {
this.setState({ autoCompletionBarExtraHeight: 30 });
}
public keyboardDidHide() {
this.setState({ autoCompletionBarExtraHeight: 0 });
}
private keyboardWillChangeFrame = (evt: KeyboardEvent) => {
const windowWidth = Dimensions.get('window').width;
// If the keyboard isn't as wide as the window, the floating keyboard is disabled.
// See https://github.com/facebook/react-native/issues/29473#issuecomment-696658937
this.setState({
floatingKeyboardEnabled: evt.endCoordinates.width < windowWidth,
});
};
public render() {
if (!this.props.route) throw new Error('Route must not be null');
if (!props.route) throw new Error('Route must not be null');
// Note: certain screens are kept into memory, in particular Notes and Search
// so that the scroll position is not lost when the user navigate away from them.
const route = this.props.route;
const route = props.route;
let Screen = null;
let notesScreenVisible = false;
let searchScreenVisible = false;
@ -90,24 +38,25 @@ class AppNavComponent extends Component<Props, State> {
} else if (route.routeName === 'Search') {
searchScreenVisible = true;
} else {
Screen = this.props.screens[route.routeName].screen;
Screen = props.screens[route.routeName].screen;
}
const previousRouteName = usePrevious(route.routeName, '');
// Keep the search screen loaded if the user is viewing a note from that search screen
// so that if the back button is pressed, the screen is still loaded. However, unload
// it if navigating away.
const searchScreenLoaded = searchScreenVisible || (this.previousRouteName_ === 'Search' && route.routeName === 'Note');
const searchScreenLoaded = searchScreenVisible || (previousRouteName === 'Search' && route.routeName === 'Note');
this.previousRouteName_ = route.routeName;
const theme = themeStyle(this.props.themeId);
const theme = themeStyle(props.themeId);
const style = { flex: 1, backgroundColor: theme.backgroundColor };
// When the floating keyboard is enabled, the KeyboardAvoidingView can have a very small
// height. Don't use the KeyboardAvoidingView when the floating keyboard is enabled.
// See https://github.com/facebook/react-native/issues/29473
const keyboardAvoidingViewEnabled = !this.state.floatingKeyboardEnabled;
const keyboardAvoidingViewEnabled = !keyboardState.isFloatingKeyboard;
const autocompletionBarPadding = Platform.OS === 'ios' && keyboardState.keyboardVisible ? safeAreaPadding.top : 0;
return (
<KeyboardAvoidingView
@ -117,12 +66,11 @@ class AppNavComponent extends Component<Props, State> {
>
<NotesScreen visible={notesScreenVisible} />
{searchScreenLoaded && <SearchScreen visible={searchScreenVisible} />}
{!notesScreenVisible && !searchScreenVisible && <Screen navigation={{ state: route }} themeId={this.props.themeId} dispatch={this.props.dispatch} />}
<View style={{ height: this.state.autoCompletionBarExtraHeight }} />
{!notesScreenVisible && !searchScreenVisible && <Screen navigation={{ state: route }} themeId={props.themeId} dispatch={props.dispatch} />}
<View style={{ height: autocompletionBarPadding }} />
</KeyboardAvoidingView>
);
}
}
};
const AppNav = connect((state: AppState) => {
return {

View File

@ -61,7 +61,6 @@ import { DialogContext, DialogControl } from '../../DialogManager';
import { CommandRuntimeProps, EditorMode, PickerResponse } from './types';
import commands from './commands';
import { AttachFileAction, AttachFileOptions } from './commands/attachFile';
import ToggleSpaceButton from '../../ToggleSpaceButton';
import PluginService from '@joplin/lib/services/plugins/PluginService';
import PluginUserWebView from '../../plugins/dialogs/PluginUserWebView';
import getShownPluginEditorView from '@joplin/lib/services/plugins/utils/getShownPluginEditorView';
@ -1675,19 +1674,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
return result;
};
const renderWrappedContent = () => {
const content = <>
{bodyComponent}
{renderVoiceTypingDialogs()}
</>;
return this.state.mode === 'edit' ? (
<ToggleSpaceButton themeId={this.props.themeId} style={this.styles().toggleSpaceButtonContent}>
{content}
</ToggleSpaceButton>
) : content;
};
const { editorPlugin: activeEditorPlugin } = getActivePluginEditorView(this.props.plugins);
return (
@ -1709,7 +1695,8 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
title={getDisplayParentTitle(this.state.note, this.state.folder)}
/>
{titleComp}
{renderWrappedContent()}
{bodyComponent}
{renderVoiceTypingDialogs()}
{renderActionButton()}
<SelectDateTimeDialog themeId={this.props.themeId} shown={this.state.alarmDialogShown} date={dueDate} onAccept={this.onAlarmDialogAccept} onReject={this.onAlarmDialogReject} />

View File

@ -0,0 +1,35 @@
import { useEffect, useMemo, useState } from 'react';
import { Dimensions, Keyboard } from 'react-native';
const useKeyboardState = () => {
const [keyboardVisible, setKeyboardVisible] = useState(false);
const [hasSoftwareKeyboard, setHasSoftwareKeyboard] = useState(false);
const [isFloatingKeyboard, setIsFloatingKeyboard] = useState(false);
useEffect(() => {
const showListener = Keyboard.addListener('keyboardDidShow', () => {
setKeyboardVisible(true);
setHasSoftwareKeyboard(true);
});
const hideListener = Keyboard.addListener('keyboardDidHide', () => {
setKeyboardVisible(false);
});
const floatingListener = Keyboard.addListener('keyboardWillChangeFrame', (evt) => {
const windowWidth = Dimensions.get('window').width;
// If the keyboard isn't as wide as the window, the floating keyboard is disabled.
// See https://github.com/facebook/react-native/issues/29473#issuecomment-696658937
setIsFloatingKeyboard(evt.endCoordinates.width < windowWidth);
});
return (() => {
showListener.remove();
hideListener.remove();
floatingListener.remove();
});
});
return useMemo(() => {
return { keyboardVisible, hasSoftwareKeyboard, isFloatingKeyboard };
}, [keyboardVisible, hasSoftwareKeyboard, isFloatingKeyboard]);
};
export default useKeyboardState;

View File

@ -1,27 +0,0 @@
import { useEffect, useMemo, useState } from 'react';
import { Keyboard } from 'react-native';
const useKeyboardVisible = () => {
const [keyboardVisible, setKeyboardVisible] = useState(false);
const [hasSoftwareKeyboard, setHasSoftwareKeyboard] = useState(false);
useEffect(() => {
const showListener = Keyboard.addListener('keyboardDidShow', () => {
setKeyboardVisible(true);
setHasSoftwareKeyboard(true);
});
const hideListener = Keyboard.addListener('keyboardDidHide', () => {
setKeyboardVisible(false);
});
return (() => {
showListener.remove();
hideListener.remove();
});
});
return useMemo(() => {
return { keyboardVisible, hasSoftwareKeyboard };
}, [keyboardVisible, hasSoftwareKeyboard]);
};
export default useKeyboardVisible;

View File

@ -839,22 +839,6 @@ const builtInMetadata = (Setting: typeof SettingType) => {
isGlobal: true,
},
// Works around a bug in which additional space is visible beneath the toolbar on some devices.
// See https://github.com/laurent22/joplin/pull/6823
'editor.mobile.removeSpaceBelowToolbar': {
value: false,
type: SettingItemType.Bool,
section: 'note',
public: true,
appTypes: [AppType.Mobile],
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
show: (settings: any) => settings['editor.mobile.removeSpaceBelowToolbar'],
label: () => 'Remove extra space below the markdown toolbar',
description: () => 'Works around bug on some devices where the markdown toolbar does not touch the bottom of the screen.',
storage: SettingStorage.File,
isGlobal: true,
},
newTodoFocus: {
value: 'title',
type: SettingItemType.String,