mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-02 12:47:41 +02:00
Mobile: Fixes #11028: Accessibility: Fix sidebar broken in right-to-left mode, improve screen reader accessibility (#11056)
This commit is contained in:
parent
2594c1edb1
commit
e0daf807a6
@ -761,6 +761,7 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
|
|||||||
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||||
packages/app-mobile/utils/getPackageInfo.js
|
packages/app-mobile/utils/getPackageInfo.js
|
||||||
packages/app-mobile/utils/getVersionInfoText.js
|
packages/app-mobile/utils/getVersionInfoText.js
|
||||||
|
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
|
||||||
packages/app-mobile/utils/image/fileToImage.web.js
|
packages/app-mobile/utils/image/fileToImage.web.js
|
||||||
packages/app-mobile/utils/image/getImageDimensions.js
|
packages/app-mobile/utils/image/getImageDimensions.js
|
||||||
packages/app-mobile/utils/image/resizeImage.js
|
packages/app-mobile/utils/image/resizeImage.js
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -738,6 +738,7 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
|
|||||||
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||||
packages/app-mobile/utils/getPackageInfo.js
|
packages/app-mobile/utils/getPackageInfo.js
|
||||||
packages/app-mobile/utils/getVersionInfoText.js
|
packages/app-mobile/utils/getVersionInfoText.js
|
||||||
|
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
|
||||||
packages/app-mobile/utils/image/fileToImage.web.js
|
packages/app-mobile/utils/image/fileToImage.web.js
|
||||||
packages/app-mobile/utils/image/getImageDimensions.js
|
packages/app-mobile/utils/image/getImageDimensions.js
|
||||||
packages/app-mobile/utils/image/resizeImage.js
|
packages/app-mobile/utils/image/resizeImage.js
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
const { connect } = require('react-redux');
|
|
||||||
const SideMenu_ = require('react-native-side-menu-updated').default;
|
|
||||||
import { Dimensions } from 'react-native';
|
|
||||||
import { State } from '@joplin/lib/reducer';
|
|
||||||
|
|
||||||
class SideMenuComponent extends SideMenu_ {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
public onLayoutChange(e: any) {
|
|
||||||
const { width, height } = e.nativeEvent.layout;
|
|
||||||
const openMenuOffsetPercentage = this.props.openMenuOffset / Dimensions.get('window').width;
|
|
||||||
const openMenuOffset = width * openMenuOffsetPercentage;
|
|
||||||
const hiddenMenuOffset = width * this.state.hiddenMenuOffsetPercentage;
|
|
||||||
this.setState({ width, height, openMenuOffset, hiddenMenuOffset });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const SideMenu = connect((state: State) => {
|
|
||||||
return {
|
|
||||||
isOpen: state.showSideMenu,
|
|
||||||
};
|
|
||||||
})(SideMenuComponent);
|
|
||||||
|
|
||||||
export default SideMenu;
|
|
326
packages/app-mobile/components/SideMenu.tsx
Normal file
326
packages/app-mobile/components/SideMenu.tsx
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { AccessibilityInfo, Animated, Dimensions, Easing, I18nManager, LayoutChangeEvent, PanResponder, Pressable, StyleSheet, useWindowDimensions, View } from 'react-native';
|
||||||
|
import { State } from '@joplin/lib/reducer';
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import AccessibleView from './accessibility/AccessibleView';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import useReduceMotionEnabled from '../utils/hooks/useReduceMotionEnabled';
|
||||||
|
import { themeStyle } from './global-style';
|
||||||
|
|
||||||
|
export enum SideMenuPosition {
|
||||||
|
Left = 'left',
|
||||||
|
Right = 'right',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OnChangeCallback = (isOpen: boolean)=> void;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
themeId: number;
|
||||||
|
isOpen: boolean;
|
||||||
|
|
||||||
|
menu: React.ReactNode;
|
||||||
|
children: React.ReactNode|React.ReactNode[];
|
||||||
|
edgeHitWidth: number;
|
||||||
|
toleranceX: number;
|
||||||
|
toleranceY: number;
|
||||||
|
openMenuOffset: number;
|
||||||
|
menuPosition: SideMenuPosition;
|
||||||
|
|
||||||
|
onChange: OnChangeCallback;
|
||||||
|
disableGestures: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseStylesProps {
|
||||||
|
themeId: number;
|
||||||
|
isLeftMenu: boolean;
|
||||||
|
menuWidth: number;
|
||||||
|
menuOpenFraction: Animated.AnimatedInterpolation<number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = ({ themeId, isLeftMenu, menuWidth, menuOpenFraction }: UseStylesProps) => {
|
||||||
|
const { height: windowHeight, width: windowWidth } = useWindowDimensions();
|
||||||
|
return useMemo(() => {
|
||||||
|
const theme = themeStyle(themeId);
|
||||||
|
return StyleSheet.create({
|
||||||
|
mainContainer: {
|
||||||
|
display: 'flex',
|
||||||
|
alignContent: 'stretch',
|
||||||
|
height: windowHeight,
|
||||||
|
},
|
||||||
|
contentOuterWrapper: {
|
||||||
|
width: windowWidth,
|
||||||
|
height: windowHeight,
|
||||||
|
transform: [{
|
||||||
|
translateX: menuOpenFraction.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: [0, isLeftMenu ? menuWidth : -menuWidth],
|
||||||
|
}),
|
||||||
|
// The RN Animation docs suggests setting "perspective" while setting other transform styles:
|
||||||
|
// https://reactnative.dev/docs/animations#bear-in-mind
|
||||||
|
}, { perspective: 1000 }],
|
||||||
|
},
|
||||||
|
contentWrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
menuWrapper: {
|
||||||
|
position: 'absolute',
|
||||||
|
height: windowHeight,
|
||||||
|
width: menuWidth,
|
||||||
|
|
||||||
|
// In React Native, RTL replaces `left` with `right` and `right` with `left`.
|
||||||
|
// As such, we need to reverse the normal direction in RTL mode.
|
||||||
|
...(isLeftMenu === !I18nManager.isRTL ? {
|
||||||
|
left: 0,
|
||||||
|
} : {
|
||||||
|
right: 0,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
closeButtonOverlay: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
|
||||||
|
zIndex: 1,
|
||||||
|
width: windowWidth,
|
||||||
|
height: windowHeight,
|
||||||
|
|
||||||
|
opacity: menuOpenFraction.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: [0, 0.1],
|
||||||
|
extrapolate: 'clamp',
|
||||||
|
}),
|
||||||
|
backgroundColor: theme.colorFaded,
|
||||||
|
display: 'flex',
|
||||||
|
alignContent: 'stretch',
|
||||||
|
},
|
||||||
|
overlayContent: {
|
||||||
|
height: windowHeight,
|
||||||
|
width: windowWidth,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [themeId, isLeftMenu, windowWidth, windowHeight, menuWidth, menuOpenFraction]);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface UseAnimationsProps {
|
||||||
|
menuWidth: number;
|
||||||
|
isLeftMenu: boolean;
|
||||||
|
open: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useAnimations = ({ menuWidth, isLeftMenu, open }: UseAnimationsProps) => {
|
||||||
|
const [animating, setIsAnimating] = useState(false);
|
||||||
|
const menuDragOffset = useMemo(() => new Animated.Value(0), []);
|
||||||
|
const basePositioningFraction = useMemo(() => new Animated.Value(0), []);
|
||||||
|
const maximumDragOffsetValue = useMemo(() => new Animated.Value(1), []);
|
||||||
|
|
||||||
|
// Update the value in a useEffect to prevent delays in applying the animation caused by
|
||||||
|
// re-renders.
|
||||||
|
useEffect(() => {
|
||||||
|
// In a right-side menu, the drag offset increases while the menu is closing.
|
||||||
|
// It needs to be inverted in that case:
|
||||||
|
// || 1: Prevents division by zero
|
||||||
|
maximumDragOffsetValue.setValue((menuWidth || 1) * (isLeftMenu ? 1 : -1));
|
||||||
|
}, [menuWidth, isLeftMenu, maximumDragOffsetValue]);
|
||||||
|
|
||||||
|
const menuOpenFraction = useMemo(() => {
|
||||||
|
const animatedDragFraction = Animated.divide(menuDragOffset, maximumDragOffsetValue);
|
||||||
|
|
||||||
|
return Animated.add(basePositioningFraction, animatedDragFraction);
|
||||||
|
}, [menuDragOffset, basePositioningFraction, maximumDragOffsetValue]);
|
||||||
|
|
||||||
|
const reduceMotionEnabled = useReduceMotionEnabled();
|
||||||
|
const reduceMotionEnabledRef = useRef(false);
|
||||||
|
reduceMotionEnabledRef.current = reduceMotionEnabled;
|
||||||
|
|
||||||
|
const updateMenuPosition = useCallback(() => {
|
||||||
|
const baseAnimationProps = {
|
||||||
|
easing: Easing.elastic(0.5),
|
||||||
|
duration: reduceMotionEnabledRef.current ? 0 : 200,
|
||||||
|
useNativeDriver: true,
|
||||||
|
};
|
||||||
|
setIsAnimating(true);
|
||||||
|
|
||||||
|
const animation = Animated.parallel([
|
||||||
|
Animated.timing(basePositioningFraction, { toValue: open ? 1 : 0, ...baseAnimationProps }),
|
||||||
|
Animated.timing(menuDragOffset, { toValue: 0, ...baseAnimationProps }),
|
||||||
|
]);
|
||||||
|
animation.start((result) => {
|
||||||
|
if (result.finished) {
|
||||||
|
setIsAnimating(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [open, menuDragOffset, basePositioningFraction]);
|
||||||
|
useEffect(() => {
|
||||||
|
updateMenuPosition();
|
||||||
|
}, [updateMenuPosition]);
|
||||||
|
|
||||||
|
return { setIsAnimating, animating, updateMenuPosition, menuOpenFraction, menuDragOffset };
|
||||||
|
};
|
||||||
|
|
||||||
|
const SideMenuComponent: React.FC<Props> = props => {
|
||||||
|
const [open, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsOpen(props.isOpen);
|
||||||
|
}, [props.isOpen]);
|
||||||
|
|
||||||
|
const [menuWidth, setMenuWidth] = useState(0);
|
||||||
|
const [contentWidth, setContentWidth] = useState(0);
|
||||||
|
|
||||||
|
// In right-to-left layout, swap left and right to be consistent with other parts of
|
||||||
|
// the app's layout.
|
||||||
|
const isLeftMenu = props.menuPosition === (I18nManager.isRTL ? SideMenuPosition.Right : SideMenuPosition.Left);
|
||||||
|
|
||||||
|
const onLayoutChange = useCallback((e: LayoutChangeEvent) => {
|
||||||
|
const { width } = e.nativeEvent.layout;
|
||||||
|
const openMenuOffsetPercentage = props.openMenuOffset / Dimensions.get('window').width;
|
||||||
|
const menuWidth = Math.floor(width * openMenuOffsetPercentage);
|
||||||
|
|
||||||
|
setContentWidth(width);
|
||||||
|
setMenuWidth(menuWidth);
|
||||||
|
}, [props.openMenuOffset]);
|
||||||
|
|
||||||
|
const { animating, setIsAnimating, menuDragOffset, updateMenuPosition, menuOpenFraction } = useAnimations({
|
||||||
|
isLeftMenu, menuWidth, open,
|
||||||
|
});
|
||||||
|
|
||||||
|
const panResponder = useMemo(() => {
|
||||||
|
return PanResponder.create({
|
||||||
|
onMoveShouldSetPanResponderCapture: (_event, gestureState) => {
|
||||||
|
if (props.disableGestures) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let startX;
|
||||||
|
let dx;
|
||||||
|
const dy = gestureState.dy;
|
||||||
|
|
||||||
|
// Untransformed start position of the gesture -- moveX is the current position of
|
||||||
|
// the pointer. Subtracting dx gives us the original start position.
|
||||||
|
const gestureStartScreenX = gestureState.moveX - gestureState.dx;
|
||||||
|
|
||||||
|
// Transform x, dx such that they are relative to the target screen edge -- this simplifies later
|
||||||
|
// math.
|
||||||
|
if (isLeftMenu) {
|
||||||
|
startX = gestureStartScreenX;
|
||||||
|
dx = gestureState.dx;
|
||||||
|
} else {
|
||||||
|
startX = contentWidth - gestureStartScreenX;
|
||||||
|
dx = -gestureState.dx;
|
||||||
|
}
|
||||||
|
|
||||||
|
const motionWithinToleranceY = Math.abs(dy) <= props.toleranceY;
|
||||||
|
let startWithinTolerance, motionWithinToleranceX;
|
||||||
|
if (open) {
|
||||||
|
startWithinTolerance = startX >= menuWidth - props.edgeHitWidth;
|
||||||
|
motionWithinToleranceX = dx <= -props.toleranceX;
|
||||||
|
} else {
|
||||||
|
startWithinTolerance = startX <= props.edgeHitWidth;
|
||||||
|
motionWithinToleranceX = dx >= props.toleranceX;
|
||||||
|
}
|
||||||
|
|
||||||
|
return startWithinTolerance && motionWithinToleranceX && motionWithinToleranceY;
|
||||||
|
},
|
||||||
|
onPanResponderGrant: () => {
|
||||||
|
setIsAnimating(true);
|
||||||
|
},
|
||||||
|
onPanResponderMove: Animated.event([
|
||||||
|
null,
|
||||||
|
// Updates menuDragOffset with the .dx property of the second argument:
|
||||||
|
{ dx: menuDragOffset },
|
||||||
|
], { useNativeDriver: false }),
|
||||||
|
onPanResponderEnd: (_event, gestureState) => {
|
||||||
|
const newOpen = (gestureState.dx > 0) === isLeftMenu;
|
||||||
|
if (newOpen === open) {
|
||||||
|
updateMenuPosition();
|
||||||
|
} else {
|
||||||
|
setIsOpen(newOpen);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [isLeftMenu, menuDragOffset, menuWidth, props.toleranceX, props.toleranceY, contentWidth, open, props.disableGestures, props.edgeHitWidth, updateMenuPosition, setIsAnimating]);
|
||||||
|
|
||||||
|
const onChangeRef = useRef(props.onChange);
|
||||||
|
onChangeRef.current = props.onChange;
|
||||||
|
useEffect(() => {
|
||||||
|
onChangeRef.current(open);
|
||||||
|
|
||||||
|
AccessibilityInfo.announceForAccessibility(
|
||||||
|
open ? _('Side menu opened') : _('Side menu closed'),
|
||||||
|
);
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const onCloseButtonPress = useCallback(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
// Set isAnimating as soon as possible to avoid components disappearing, then reappearing.
|
||||||
|
setIsAnimating(true);
|
||||||
|
}, [setIsAnimating]);
|
||||||
|
|
||||||
|
const styles = useStyles({ themeId: props.themeId, menuOpenFraction, menuWidth, isLeftMenu });
|
||||||
|
|
||||||
|
const menuComponent = (
|
||||||
|
<AccessibleView
|
||||||
|
inert={!open}
|
||||||
|
style={styles.menuWrapper}
|
||||||
|
>
|
||||||
|
<AccessibleView
|
||||||
|
// Auto-focuses an empty view at the beginning of the sidemenu -- if we instead
|
||||||
|
// focus the container view, VoiceOver fails to focus to any components within
|
||||||
|
// the sidebar.
|
||||||
|
refocusCounter={!open ? 1 : undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{props.menu}
|
||||||
|
</AccessibleView>
|
||||||
|
);
|
||||||
|
|
||||||
|
const contentComponent = (
|
||||||
|
<AccessibleView
|
||||||
|
inert={open}
|
||||||
|
style={styles.contentWrapper}
|
||||||
|
>
|
||||||
|
<AccessibleView refocusCounter={open ? 1 : undefined} />
|
||||||
|
{props.children}
|
||||||
|
</AccessibleView>
|
||||||
|
);
|
||||||
|
const closeButtonOverlay = (open || animating) ? (
|
||||||
|
<Animated.View
|
||||||
|
style={styles.closeButtonOverlay}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
aria-label={_('Close side menu')}
|
||||||
|
role='button'
|
||||||
|
onPress={onCloseButtonPress}
|
||||||
|
style={styles.overlayContent}
|
||||||
|
></Pressable>
|
||||||
|
</Animated.View>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
onLayout={onLayoutChange}
|
||||||
|
style={styles.mainContainer}
|
||||||
|
{...panResponder.panHandlers}
|
||||||
|
>
|
||||||
|
{menuComponent}
|
||||||
|
<Animated.View style={styles.contentOuterWrapper}>
|
||||||
|
{contentComponent}
|
||||||
|
{closeButtonOverlay}
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SideMenu = connect((state: State) => {
|
||||||
|
return {
|
||||||
|
themeId: state.settings.theme,
|
||||||
|
isOpen: state.showSideMenu,
|
||||||
|
};
|
||||||
|
})(SideMenuComponent);
|
||||||
|
|
||||||
|
export default SideMenu;
|
@ -19,14 +19,12 @@ import restoreItems from '@joplin/lib/services/trash/restoreItems';
|
|||||||
import emptyTrash from '@joplin/lib/services/trash/emptyTrash';
|
import emptyTrash from '@joplin/lib/services/trash/emptyTrash';
|
||||||
import { ModelType } from '@joplin/lib/BaseModel';
|
import { ModelType } from '@joplin/lib/BaseModel';
|
||||||
import { DialogContext } from './DialogManager';
|
import { DialogContext } from './DialogManager';
|
||||||
import AccessibleView from './accessibility/AccessibleView';
|
|
||||||
const { TouchableRipple } = require('react-native-paper');
|
const { TouchableRipple } = require('react-native-paper');
|
||||||
const { substrWithEllipsis } = require('@joplin/lib/string-utils');
|
const { substrWithEllipsis } = require('@joplin/lib/string-utils');
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
syncStarted: boolean;
|
syncStarted: boolean;
|
||||||
themeId: number;
|
themeId: number;
|
||||||
sideMenuVisible: boolean;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
collapsedFolderIds: string[];
|
collapsedFolderIds: string[];
|
||||||
@ -583,34 +581,22 @@ const SideMenuContentComponent = (props: Props) => {
|
|||||||
items = items.concat(folderItems);
|
items = items.concat(folderItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isHidden = !props.sideMenuVisible;
|
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
borderRightWidth: 1,
|
borderRightWidth: 1,
|
||||||
borderRightColor: theme.dividerColor,
|
borderRightColor: theme.dividerColor,
|
||||||
backgroundColor: theme.backgroundColor,
|
backgroundColor: theme.backgroundColor,
|
||||||
|
|
||||||
// Have the UI reflect whether the View is hidden to the screen reader.
|
|
||||||
// This way, there will be visual feedback if isHidden is incorrect.
|
|
||||||
opacity: isHidden ? 0.5 : undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessibleView
|
<View style={style}>
|
||||||
style={style}
|
|
||||||
|
|
||||||
// Accessibility, keyboard, and touch hidden.
|
|
||||||
inert={isHidden}
|
|
||||||
refocusCounter={isHidden ? undefined : 1}
|
|
||||||
>
|
|
||||||
<View style={{ flex: 1, opacity: props.opacity }}>
|
<View style={{ flex: 1, opacity: props.opacity }}>
|
||||||
<ScrollView scrollsToTop={false} style={styles_.menu}>
|
<ScrollView scrollsToTop={false} style={styles_.menu}>
|
||||||
{items}
|
{items}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
{renderBottomPanel()}
|
{renderBottomPanel()}
|
||||||
</View>
|
</View>
|
||||||
</AccessibleView>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -624,9 +610,6 @@ export default connect((state: AppState) => {
|
|||||||
notesParentType: state.notesParentType,
|
notesParentType: state.notesParentType,
|
||||||
locale: state.settings.locale,
|
locale: state.settings.locale,
|
||||||
themeId: state.settings.theme,
|
themeId: state.settings.theme,
|
||||||
sideMenuVisible: state.showSideMenu,
|
|
||||||
// Don't do the opacity animation as it means re-rendering the list multiple times
|
|
||||||
// opacity: state.sideMenuOpenPercent,
|
|
||||||
collapsedFolderIds: state.collapsedFolderIds,
|
collapsedFolderIds: state.collapsedFolderIds,
|
||||||
decryptionWorker: state.decryptionWorker,
|
decryptionWorker: state.decryptionWorker,
|
||||||
resourceFetcher: state.resourceFetcher,
|
resourceFetcher: state.resourceFetcher,
|
||||||
|
@ -66,7 +66,6 @@
|
|||||||
"react-native-safe-area-context": "4.10.5",
|
"react-native-safe-area-context": "4.10.5",
|
||||||
"react-native-securerandom": "1.0.1",
|
"react-native-securerandom": "1.0.1",
|
||||||
"react-native-share": "10.2.1",
|
"react-native-share": "10.2.1",
|
||||||
"react-native-side-menu-updated": "1.3.2",
|
|
||||||
"react-native-sqlite-storage": "6.0.1",
|
"react-native-sqlite-storage": "6.0.1",
|
||||||
"react-native-url-polyfill": "2.0.0",
|
"react-native-url-polyfill": "2.0.0",
|
||||||
"react-native-vector-icons": "10.1.0",
|
"react-native-vector-icons": "10.1.0",
|
||||||
|
@ -29,7 +29,7 @@ import SyncTargetOneDrive from '@joplin/lib/SyncTargetOneDrive';
|
|||||||
import initProfile from '@joplin/lib/services/profileConfig/initProfile';
|
import initProfile from '@joplin/lib/services/profileConfig/initProfile';
|
||||||
const VersionInfo = require('react-native-version-info').default;
|
const VersionInfo = require('react-native-version-info').default;
|
||||||
const { Keyboard, BackHandler, Animated, StatusBar, Platform, Dimensions } = require('react-native');
|
const { Keyboard, BackHandler, Animated, StatusBar, Platform, Dimensions } = require('react-native');
|
||||||
import { AppState as RNAppState, EmitterSubscription, View, Text, Linking, NativeEventSubscription, Appearance, AccessibilityInfo, ActivityIndicator } from 'react-native';
|
import { AppState as RNAppState, EmitterSubscription, View, Text, Linking, NativeEventSubscription, Appearance, ActivityIndicator } from 'react-native';
|
||||||
import getResponsiveValue from './components/getResponsiveValue';
|
import getResponsiveValue from './components/getResponsiveValue';
|
||||||
import NetInfo from '@react-native-community/netinfo';
|
import NetInfo from '@react-native-community/netinfo';
|
||||||
const DropdownAlert = require('react-native-dropdownalert').default;
|
const DropdownAlert = require('react-native-dropdownalert').default;
|
||||||
@ -66,7 +66,7 @@ const { OneDriveLoginScreen } = require('./components/screens/onedrive-login.js'
|
|||||||
import EncryptionConfigScreen from './components/screens/encryption-config';
|
import EncryptionConfigScreen from './components/screens/encryption-config';
|
||||||
const { DropboxLoginScreen } = require('./components/screens/dropbox-login.js');
|
const { DropboxLoginScreen } = require('./components/screens/dropbox-login.js');
|
||||||
import { MenuProvider } from 'react-native-popup-menu';
|
import { MenuProvider } from 'react-native-popup-menu';
|
||||||
import SideMenu from './components/SideMenu';
|
import SideMenu, { SideMenuPosition } from './components/SideMenu';
|
||||||
import SideMenuContent from './components/side-menu-content';
|
import SideMenuContent from './components/side-menu-content';
|
||||||
const { SideMenuContentNote } = require('./components/side-menu-content-note.js');
|
const { SideMenuContentNote } = require('./components/side-menu-content-note.js');
|
||||||
import { reg } from '@joplin/lib/registry';
|
import { reg } from '@joplin/lib/registry';
|
||||||
@ -137,8 +137,6 @@ import lockToSingleInstance from './utils/lockToSingleInstance';
|
|||||||
import { AppState } from './utils/types';
|
import { AppState } from './utils/types';
|
||||||
import { getDisplayParentId } from '@joplin/lib/services/trash';
|
import { getDisplayParentId } from '@joplin/lib/services/trash';
|
||||||
|
|
||||||
type SideMenuPosition = 'left' | 'right';
|
|
||||||
|
|
||||||
const logger = Logger.create('root');
|
const logger = Logger.create('root');
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
@ -393,12 +391,6 @@ const appReducer = (state = appDefaultState, action: any) => {
|
|||||||
newState.showSideMenu = false;
|
newState.showSideMenu = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'SIDE_MENU_OPEN_PERCENT':
|
|
||||||
|
|
||||||
newState = { ...state };
|
|
||||||
newState.sideMenuOpenPercent = action.value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'SET_PLUGIN_PANELS_DIALOG_VISIBLE':
|
case 'SET_PLUGIN_PANELS_DIALOG_VISIBLE':
|
||||||
newState = { ...state };
|
newState = { ...state };
|
||||||
newState.showPanelsDialog = action.visible;
|
newState.showPanelsDialog = action.visible;
|
||||||
@ -1188,9 +1180,6 @@ class AppComponent extends React.Component {
|
|||||||
this.props.dispatch({
|
this.props.dispatch({
|
||||||
type: isOpen ? 'SIDE_MENU_OPEN' : 'SIDE_MENU_CLOSE',
|
type: isOpen ? 'SIDE_MENU_OPEN' : 'SIDE_MENU_CLOSE',
|
||||||
});
|
});
|
||||||
AccessibilityInfo.announceForAccessibility(
|
|
||||||
isOpen ? _('Side menu opened') : _('Side menu closed'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSideMenuWidth = () => {
|
private getSideMenuWidth = () => {
|
||||||
@ -1223,12 +1212,12 @@ class AppComponent extends React.Component {
|
|||||||
const theme: Theme = themeStyle(this.props.themeId);
|
const theme: Theme = themeStyle(this.props.themeId);
|
||||||
|
|
||||||
let sideMenuContent: ReactNode = null;
|
let sideMenuContent: ReactNode = null;
|
||||||
let menuPosition: SideMenuPosition = 'left';
|
let menuPosition = SideMenuPosition.Left;
|
||||||
let disableSideMenuGestures = this.props.disableSideMenuGestures;
|
let disableSideMenuGestures = this.props.disableSideMenuGestures;
|
||||||
|
|
||||||
if (this.props.routeName === 'Note') {
|
if (this.props.routeName === 'Note') {
|
||||||
sideMenuContent = <SafeAreaView style={{ flex: 1, backgroundColor: theme.backgroundColor }}><SideMenuContentNote options={this.props.noteSideMenuOptions}/></SafeAreaView>;
|
sideMenuContent = <SafeAreaView style={{ flex: 1, backgroundColor: theme.backgroundColor }}><SideMenuContentNote options={this.props.noteSideMenuOptions}/></SafeAreaView>;
|
||||||
menuPosition = 'right';
|
menuPosition = SideMenuPosition.Right;
|
||||||
} else if (this.props.routeName === 'Config') {
|
} else if (this.props.routeName === 'Config') {
|
||||||
disableSideMenuGestures = true;
|
disableSideMenuGestures = true;
|
||||||
} else {
|
} else {
|
||||||
@ -1283,12 +1272,6 @@ class AppComponent extends React.Component {
|
|||||||
menuPosition={menuPosition}
|
menuPosition={menuPosition}
|
||||||
onChange={(isOpen: boolean) => this.sideMenu_change(isOpen)}
|
onChange={(isOpen: boolean) => this.sideMenu_change(isOpen)}
|
||||||
disableGestures={disableSideMenuGestures}
|
disableGestures={disableSideMenuGestures}
|
||||||
onSliding={(percent: number) => {
|
|
||||||
this.props.dispatch({
|
|
||||||
type: 'SIDE_MENU_OPEN_PERCENT',
|
|
||||||
value: percent,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<StatusBar barStyle={statusBarStyle} />
|
<StatusBar barStyle={statusBarStyle} />
|
||||||
<MenuProvider style={{ flex: 1 }}>
|
<MenuProvider style={{ flex: 1 }}>
|
||||||
|
@ -11,7 +11,6 @@ export const DEFAULT_ROUTE = {
|
|||||||
const appDefaultState: AppState = {
|
const appDefaultState: AppState = {
|
||||||
smartFilterId: undefined,
|
smartFilterId: undefined,
|
||||||
...defaultState,
|
...defaultState,
|
||||||
sideMenuOpenPercent: 0,
|
|
||||||
route: DEFAULT_ROUTE,
|
route: DEFAULT_ROUTE,
|
||||||
noteSelectionEnabled: false,
|
noteSelectionEnabled: false,
|
||||||
noteSideMenuOptions: null,
|
noteSideMenuOptions: null,
|
||||||
|
19
packages/app-mobile/utils/hooks/useReduceMotionEnabled.ts
Normal file
19
packages/app-mobile/utils/hooks/useReduceMotionEnabled.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { AccessibilityInfo } from 'react-native';
|
||||||
|
|
||||||
|
const useReduceMotionEnabled = () => {
|
||||||
|
const [reduceMotionEnabled, setReduceMotionEnabled] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
AccessibilityInfo.addEventListener('reduceMotionChanged', (enabled) => {
|
||||||
|
setReduceMotionEnabled(enabled);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
useAsyncEffect(async () => {
|
||||||
|
setReduceMotionEnabled(await AccessibilityInfo.isReduceMotionEnabled());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return reduceMotionEnabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useReduceMotionEnabled;
|
@ -1,7 +1,6 @@
|
|||||||
import { State } from '@joplin/lib/reducer';
|
import { State } from '@joplin/lib/reducer';
|
||||||
|
|
||||||
export interface AppState extends State {
|
export interface AppState extends State {
|
||||||
sideMenuOpenPercent: number;
|
|
||||||
showPanelsDialog: boolean;
|
showPanelsDialog: boolean;
|
||||||
isOnMobileData: boolean;
|
isOnMobileData: boolean;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
|
12
yarn.lock
12
yarn.lock
@ -7535,7 +7535,6 @@ __metadata:
|
|||||||
react-native-safe-area-context: 4.10.5
|
react-native-safe-area-context: 4.10.5
|
||||||
react-native-securerandom: 1.0.1
|
react-native-securerandom: 1.0.1
|
||||||
react-native-share: 10.2.1
|
react-native-share: 10.2.1
|
||||||
react-native-side-menu-updated: 1.3.2
|
|
||||||
react-native-sqlite-storage: 6.0.1
|
react-native-sqlite-storage: 6.0.1
|
||||||
react-native-url-polyfill: 2.0.0
|
react-native-url-polyfill: 2.0.0
|
||||||
react-native-vector-icons: 10.1.0
|
react-native-vector-icons: 10.1.0
|
||||||
@ -36465,7 +36464,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"prop-types@npm:15.8.1, prop-types@npm:^15.5.10, prop-types@npm:^15.8.1":
|
"prop-types@npm:15.8.1, prop-types@npm:^15.8.1":
|
||||||
version: 15.8.1
|
version: 15.8.1
|
||||||
resolution: "prop-types@npm:15.8.1"
|
resolution: "prop-types@npm:15.8.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -37402,15 +37401,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-native-side-menu-updated@npm:1.3.2":
|
|
||||||
version: 1.3.2
|
|
||||||
resolution: "react-native-side-menu-updated@npm:1.3.2"
|
|
||||||
dependencies:
|
|
||||||
prop-types: ^15.5.10
|
|
||||||
checksum: 5d7ae7d2b372c80d9f7a3472f945daa6c11b43f00193ebec92fdb40ee853e86f522686736aa6a510a4fb09479c8eb7e4f14b53ad117d7e23749cd58c3c9d6cb7
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"react-native-sqlite-storage@npm:6.0.1":
|
"react-native-sqlite-storage@npm:6.0.1":
|
||||||
version: 6.0.1
|
version: 6.0.1
|
||||||
resolution: "react-native-sqlite-storage@npm:6.0.1"
|
resolution: "react-native-sqlite-storage@npm:6.0.1"
|
||||||
|
Loading…
Reference in New Issue
Block a user