You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-26 22:41:17 +02:00
Mobile: Implement new note menu redesign (#11780)
This commit is contained in:
@@ -593,6 +593,7 @@ packages/app-mobile/commands/scrollToHash.js
|
|||||||
packages/app-mobile/commands/util/goToNote.js
|
packages/app-mobile/commands/util/goToNote.js
|
||||||
packages/app-mobile/commands/util/showResource.js
|
packages/app-mobile/commands/util/showResource.js
|
||||||
packages/app-mobile/components/BetaChip.js
|
packages/app-mobile/components/BetaChip.js
|
||||||
|
packages/app-mobile/components/BottomDrawer.js
|
||||||
packages/app-mobile/components/CameraView/ActionButtons.js
|
packages/app-mobile/components/CameraView/ActionButtons.js
|
||||||
packages/app-mobile/components/CameraView/Camera/index.jest.js
|
packages/app-mobile/components/CameraView/Camera/index.jest.js
|
||||||
packages/app-mobile/components/CameraView/Camera/index.js
|
packages/app-mobile/components/CameraView/Camera/index.js
|
||||||
@@ -686,7 +687,6 @@ packages/app-mobile/components/SelectDateTimeDialog.js
|
|||||||
packages/app-mobile/components/SideMenu.js
|
packages/app-mobile/components/SideMenu.js
|
||||||
packages/app-mobile/components/SideMenuContentNote.js
|
packages/app-mobile/components/SideMenuContentNote.js
|
||||||
packages/app-mobile/components/TextInput.js
|
packages/app-mobile/components/TextInput.js
|
||||||
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
|
|
||||||
packages/app-mobile/components/accessibility/AccessibleView.test.js
|
packages/app-mobile/components/accessibility/AccessibleView.test.js
|
||||||
packages/app-mobile/components/accessibility/AccessibleView.js
|
packages/app-mobile/components/accessibility/AccessibleView.js
|
||||||
packages/app-mobile/components/accessibility/FocusControl/AutoFocusProvider.js
|
packages/app-mobile/components/accessibility/FocusControl/AutoFocusProvider.js
|
||||||
@@ -701,6 +701,7 @@ packages/app-mobile/components/biometrics/BiometricPopup.js
|
|||||||
packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
||||||
packages/app-mobile/components/biometrics/sensorInfo.js
|
packages/app-mobile/components/biometrics/sensorInfo.js
|
||||||
packages/app-mobile/components/buttons/FloatingActionButton.js
|
packages/app-mobile/components/buttons/FloatingActionButton.js
|
||||||
|
packages/app-mobile/components/buttons/LabelledIconButton.js
|
||||||
packages/app-mobile/components/buttons/TextButton.js
|
packages/app-mobile/components/buttons/TextButton.js
|
||||||
packages/app-mobile/components/buttons/index.js
|
packages/app-mobile/components/buttons/index.js
|
||||||
packages/app-mobile/components/getResponsiveValue.test.js
|
packages/app-mobile/components/getResponsiveValue.test.js
|
||||||
@@ -791,7 +792,9 @@ packages/app-mobile/components/screens/Note/commands/setTags.js
|
|||||||
packages/app-mobile/components/screens/Note/commands/toggleVisiblePanes.js
|
packages/app-mobile/components/screens/Note/commands/toggleVisiblePanes.js
|
||||||
packages/app-mobile/components/screens/Note/types.js
|
packages/app-mobile/components/screens/Note/types.js
|
||||||
packages/app-mobile/components/screens/NoteTagsDialog.js
|
packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||||
packages/app-mobile/components/screens/Notes.js
|
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||||
|
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||||
|
packages/app-mobile/components/screens/Notes/Notes.js
|
||||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||||
packages/app-mobile/components/screens/SearchScreen/index.js
|
packages/app-mobile/components/screens/SearchScreen/index.js
|
||||||
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
||||||
@@ -846,6 +849,7 @@ packages/app-mobile/utils/createRootStyle.js
|
|||||||
packages/app-mobile/utils/database-driver-react-native.js
|
packages/app-mobile/utils/database-driver-react-native.js
|
||||||
packages/app-mobile/utils/database-driver-react-native.web.js
|
packages/app-mobile/utils/database-driver-react-native.web.js
|
||||||
packages/app-mobile/utils/debounce.js
|
packages/app-mobile/utils/debounce.js
|
||||||
|
packages/app-mobile/utils/focusView.js
|
||||||
packages/app-mobile/utils/fs-driver/constants.js
|
packages/app-mobile/utils/fs-driver/constants.js
|
||||||
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
|
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
|
||||||
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.js
|
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.js
|
||||||
@@ -861,6 +865,7 @@ packages/app-mobile/utils/getVersionInfoText.js
|
|||||||
packages/app-mobile/utils/hooks/useKeyboardState.js
|
packages/app-mobile/utils/hooks/useKeyboardState.js
|
||||||
packages/app-mobile/utils/hooks/useOnLongPressProps.js
|
packages/app-mobile/utils/hooks/useOnLongPressProps.js
|
||||||
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
|
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
|
||||||
|
packages/app-mobile/utils/hooks/useSafeAreaPadding.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
|
||||||
|
|||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -568,6 +568,7 @@ packages/app-mobile/commands/scrollToHash.js
|
|||||||
packages/app-mobile/commands/util/goToNote.js
|
packages/app-mobile/commands/util/goToNote.js
|
||||||
packages/app-mobile/commands/util/showResource.js
|
packages/app-mobile/commands/util/showResource.js
|
||||||
packages/app-mobile/components/BetaChip.js
|
packages/app-mobile/components/BetaChip.js
|
||||||
|
packages/app-mobile/components/BottomDrawer.js
|
||||||
packages/app-mobile/components/CameraView/ActionButtons.js
|
packages/app-mobile/components/CameraView/ActionButtons.js
|
||||||
packages/app-mobile/components/CameraView/Camera/index.jest.js
|
packages/app-mobile/components/CameraView/Camera/index.jest.js
|
||||||
packages/app-mobile/components/CameraView/Camera/index.js
|
packages/app-mobile/components/CameraView/Camera/index.js
|
||||||
@@ -661,7 +662,6 @@ packages/app-mobile/components/SelectDateTimeDialog.js
|
|||||||
packages/app-mobile/components/SideMenu.js
|
packages/app-mobile/components/SideMenu.js
|
||||||
packages/app-mobile/components/SideMenuContentNote.js
|
packages/app-mobile/components/SideMenuContentNote.js
|
||||||
packages/app-mobile/components/TextInput.js
|
packages/app-mobile/components/TextInput.js
|
||||||
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
|
|
||||||
packages/app-mobile/components/accessibility/AccessibleView.test.js
|
packages/app-mobile/components/accessibility/AccessibleView.test.js
|
||||||
packages/app-mobile/components/accessibility/AccessibleView.js
|
packages/app-mobile/components/accessibility/AccessibleView.js
|
||||||
packages/app-mobile/components/accessibility/FocusControl/AutoFocusProvider.js
|
packages/app-mobile/components/accessibility/FocusControl/AutoFocusProvider.js
|
||||||
@@ -676,6 +676,7 @@ packages/app-mobile/components/biometrics/BiometricPopup.js
|
|||||||
packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
||||||
packages/app-mobile/components/biometrics/sensorInfo.js
|
packages/app-mobile/components/biometrics/sensorInfo.js
|
||||||
packages/app-mobile/components/buttons/FloatingActionButton.js
|
packages/app-mobile/components/buttons/FloatingActionButton.js
|
||||||
|
packages/app-mobile/components/buttons/LabelledIconButton.js
|
||||||
packages/app-mobile/components/buttons/TextButton.js
|
packages/app-mobile/components/buttons/TextButton.js
|
||||||
packages/app-mobile/components/buttons/index.js
|
packages/app-mobile/components/buttons/index.js
|
||||||
packages/app-mobile/components/getResponsiveValue.test.js
|
packages/app-mobile/components/getResponsiveValue.test.js
|
||||||
@@ -766,7 +767,9 @@ packages/app-mobile/components/screens/Note/commands/setTags.js
|
|||||||
packages/app-mobile/components/screens/Note/commands/toggleVisiblePanes.js
|
packages/app-mobile/components/screens/Note/commands/toggleVisiblePanes.js
|
||||||
packages/app-mobile/components/screens/Note/types.js
|
packages/app-mobile/components/screens/Note/types.js
|
||||||
packages/app-mobile/components/screens/NoteTagsDialog.js
|
packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||||
packages/app-mobile/components/screens/Notes.js
|
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||||
|
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||||
|
packages/app-mobile/components/screens/Notes/Notes.js
|
||||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||||
packages/app-mobile/components/screens/SearchScreen/index.js
|
packages/app-mobile/components/screens/SearchScreen/index.js
|
||||||
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
||||||
@@ -821,6 +824,7 @@ packages/app-mobile/utils/createRootStyle.js
|
|||||||
packages/app-mobile/utils/database-driver-react-native.js
|
packages/app-mobile/utils/database-driver-react-native.js
|
||||||
packages/app-mobile/utils/database-driver-react-native.web.js
|
packages/app-mobile/utils/database-driver-react-native.web.js
|
||||||
packages/app-mobile/utils/debounce.js
|
packages/app-mobile/utils/debounce.js
|
||||||
|
packages/app-mobile/utils/focusView.js
|
||||||
packages/app-mobile/utils/fs-driver/constants.js
|
packages/app-mobile/utils/fs-driver/constants.js
|
||||||
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
|
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
|
||||||
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.js
|
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.js
|
||||||
@@ -836,6 +840,7 @@ packages/app-mobile/utils/getVersionInfoText.js
|
|||||||
packages/app-mobile/utils/hooks/useKeyboardState.js
|
packages/app-mobile/utils/hooks/useKeyboardState.js
|
||||||
packages/app-mobile/utils/hooks/useOnLongPressProps.js
|
packages/app-mobile/utils/hooks/useOnLongPressProps.js
|
||||||
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
|
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
|
||||||
|
packages/app-mobile/utils/hooks/useSafeAreaPadding.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
|
||||||
|
|||||||
106
packages/app-mobile/components/BottomDrawer.tsx
Normal file
106
packages/app-mobile/components/BottomDrawer.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { NativeScrollEvent, NativeSyntheticEvent, StyleSheet, useWindowDimensions, View } from 'react-native';
|
||||||
|
import useSafeAreaPadding from '../utils/hooks/useSafeAreaPadding';
|
||||||
|
import { themeStyle, ThemeStyle } from './global-style';
|
||||||
|
import Modal from './Modal';
|
||||||
|
import { AppState } from '../utils/types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
themeId: number;
|
||||||
|
children: React.ReactNode;
|
||||||
|
visible: boolean;
|
||||||
|
onDismiss: ()=> void;
|
||||||
|
onShow: ()=> void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = (theme: ThemeStyle) => {
|
||||||
|
const { width: windowWidth } = useWindowDimensions();
|
||||||
|
const safeAreaPadding = useSafeAreaPadding();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const isSmallWidthScreen = windowWidth < 500;
|
||||||
|
const menuGapLeft = safeAreaPadding.paddingLeft + 6;
|
||||||
|
const menuGapRight = safeAreaPadding.paddingRight + 6;
|
||||||
|
|
||||||
|
return StyleSheet.create({
|
||||||
|
menuStyle: {
|
||||||
|
alignSelf: 'flex-end',
|
||||||
|
...(isSmallWidthScreen ? {
|
||||||
|
// Center on small screens, rather than float right.
|
||||||
|
alignSelf: 'center',
|
||||||
|
} : {}),
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginRight: menuGapRight,
|
||||||
|
marginLeft: menuGapLeft,
|
||||||
|
paddingBottom: 0,
|
||||||
|
|
||||||
|
backgroundColor: theme.backgroundColor,
|
||||||
|
borderRadius: 16,
|
||||||
|
borderBottomRightRadius: 0,
|
||||||
|
borderBottomLeftRadius: 0,
|
||||||
|
maxWidth: Math.min(400, windowWidth - menuGapRight - menuGapLeft),
|
||||||
|
},
|
||||||
|
contentContainer: {
|
||||||
|
padding: 20,
|
||||||
|
paddingBottom: 14,
|
||||||
|
gap: 8,
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
},
|
||||||
|
modalBackground: {
|
||||||
|
paddingTop: 0,
|
||||||
|
paddingLeft: 0,
|
||||||
|
paddingRight: 0,
|
||||||
|
paddingBottom: 0,
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
dismissButton: {
|
||||||
|
top: 0,
|
||||||
|
bottom: undefined,
|
||||||
|
height: 12,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [theme, safeAreaPadding, windowWidth]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const BottomDrawer: React.FC<Props> = props => {
|
||||||
|
const theme = themeStyle(props.themeId);
|
||||||
|
const styles = useStyles(theme);
|
||||||
|
|
||||||
|
const onContainerScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||||
|
const offsetY = event.nativeEvent.contentOffset.y;
|
||||||
|
if (offsetY < -50) {
|
||||||
|
props.onDismiss();
|
||||||
|
}
|
||||||
|
}, [props.onDismiss]);
|
||||||
|
|
||||||
|
return <Modal
|
||||||
|
visible={props.visible}
|
||||||
|
onDismiss={props.onDismiss}
|
||||||
|
onRequestClose={props.onDismiss}
|
||||||
|
onShow={props.onShow}
|
||||||
|
animationType='fade'
|
||||||
|
backgroundColor={theme.backgroundColorTransparent2}
|
||||||
|
transparent
|
||||||
|
modalBackgroundStyle={styles.modalBackground}
|
||||||
|
dismissButtonStyle={styles.dismissButton}
|
||||||
|
containerStyle={styles.menuStyle}
|
||||||
|
scrollOverflow={{
|
||||||
|
overScrollMode: 'always',
|
||||||
|
onScroll: onContainerScroll,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={styles.contentContainer}>
|
||||||
|
{props.children}
|
||||||
|
</View>
|
||||||
|
</Modal>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect((state: AppState) => {
|
||||||
|
return {
|
||||||
|
themeId: state.settings.theme,
|
||||||
|
};
|
||||||
|
})(BottomDrawer);
|
||||||
@@ -8,6 +8,7 @@ import makeShowMessageBox from '../../utils/makeShowMessageBox';
|
|||||||
import { DialogControl, PromptDialogData } from './types';
|
import { DialogControl, PromptDialogData } from './types';
|
||||||
import useDialogControl from './hooks/useDialogControl';
|
import useDialogControl from './hooks/useDialogControl';
|
||||||
import PromptDialog from './PromptDialog';
|
import PromptDialog from './PromptDialog';
|
||||||
|
import { themeStyle } from '../global-style';
|
||||||
|
|
||||||
export type { DialogControl } from './types';
|
export type { DialogControl } from './types';
|
||||||
export const DialogContext = createContext<DialogControl>(null);
|
export const DialogContext = createContext<DialogControl>(null);
|
||||||
@@ -49,6 +50,7 @@ const DialogManager: React.FC<Props> = props => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const theme = themeStyle(props.themeId);
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
|
||||||
const dialogComponents: React.ReactNode[] = [];
|
const dialogComponents: React.ReactNode[] = [];
|
||||||
@@ -73,7 +75,7 @@ const DialogManager: React.FC<Props> = props => {
|
|||||||
scrollOverflow={true}
|
scrollOverflow={true}
|
||||||
containerStyle={styles.modalContainer}
|
containerStyle={styles.modalContainer}
|
||||||
animationType='fade'
|
animationType='fade'
|
||||||
backgroundColor='rgba(0, 0, 0, 0.1)'
|
backgroundColor={theme.backgroundColorTransparent2}
|
||||||
transparent={true}
|
transparent={true}
|
||||||
onRequestClose={dialogModels[dialogComponents.length - 1]?.onDismiss}
|
onRequestClose={dialogModels[dialogComponents.length - 1]?.onDismiss}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ const useStyles = (themeId: number, containerStyle: ViewStyle, size: DialogSize)
|
|||||||
};
|
};
|
||||||
|
|
||||||
const DismissibleDialog: React.FC<Props> = props => {
|
const DismissibleDialog: React.FC<Props> = props => {
|
||||||
|
const theme = themeStyle(props.themeId);
|
||||||
const styles = useStyles(props.themeId, props.containerStyle, props.size);
|
const styles = useStyles(props.themeId, props.containerStyle, props.size);
|
||||||
|
|
||||||
const heading = props.heading ? (
|
const heading = props.heading ? (
|
||||||
@@ -92,7 +93,7 @@ const DismissibleDialog: React.FC<Props> = props => {
|
|||||||
onRequestClose={props.onDismiss}
|
onRequestClose={props.onDismiss}
|
||||||
containerStyle={styles.dialogContainer}
|
containerStyle={styles.dialogContainer}
|
||||||
animationType='fade'
|
animationType='fade'
|
||||||
backgroundColor='rgba(0, 0, 0, 0.1)'
|
backgroundColor={theme.backgroundColorTransparent2}
|
||||||
transparent={true}
|
transparent={true}
|
||||||
>
|
>
|
||||||
<Surface style={styles.dialogSurface} elevation={1}>
|
<Surface style={styles.dialogSurface} elevation={1}>
|
||||||
|
|||||||
@@ -1,40 +1,35 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { RefObject, useCallback, useMemo, useRef, useState } from 'react';
|
import { RefObject, useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { GestureResponderEvent, Modal, ModalProps, Platform, Pressable, ScrollView, StyleSheet, View, ViewStyle, useWindowDimensions } from 'react-native';
|
import { GestureResponderEvent, Modal, ModalProps, Platform, Pressable, ScrollView, ScrollViewProps, StyleSheet, View, ViewStyle } from 'react-native';
|
||||||
import { hasNotch } from 'react-native-device-info';
|
|
||||||
import FocusControl from './accessibility/FocusControl/FocusControl';
|
import FocusControl from './accessibility/FocusControl/FocusControl';
|
||||||
import { msleep, Second } from '@joplin/utils/time';
|
import { msleep, Second } from '@joplin/utils/time';
|
||||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||||
import { ModalState } from './accessibility/FocusControl/types';
|
import { ModalState } from './accessibility/FocusControl/types';
|
||||||
|
import useSafeAreaPadding from '../utils/hooks/useSafeAreaPadding';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
|
||||||
interface ModalElementProps extends ModalProps {
|
interface ModalElementProps extends ModalProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
containerStyle?: ViewStyle;
|
containerStyle?: ViewStyle;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
|
modalBackgroundStyle?: ViewStyle;
|
||||||
|
// Extra styles for the accessibility tools dismiss button. For example,
|
||||||
|
// this might be used to display the dismiss button near the top of the
|
||||||
|
// screen (rather than the bottom).
|
||||||
|
dismissButtonStyle?: ViewStyle;
|
||||||
|
|
||||||
// If scrollOverflow is provided, the modal is wrapped in a vertical
|
// If scrollOverflow is provided, the modal is wrapped in a vertical
|
||||||
// ScrollView. This allows the user to scroll parts of dialogs into
|
// ScrollView. This allows the user to scroll parts of dialogs into
|
||||||
// view that would otherwise be clipped by the screen edge.
|
// view that would otherwise be clipped by the screen edge.
|
||||||
scrollOverflow?: boolean;
|
scrollOverflow?: boolean|ScrollViewProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = (hasScrollView: boolean, backgroundColor: string|undefined) => {
|
const useStyles = (hasScrollView: boolean, backgroundColor: string|undefined) => {
|
||||||
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
const safeAreaPadding = useSafeAreaPadding();
|
||||||
const isLandscape = windowWidth > windowHeight;
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const backgroundPadding: ViewStyle = isLandscape ? {
|
|
||||||
paddingRight: hasNotch() ? 60 : 0,
|
|
||||||
paddingLeft: hasNotch() ? 60 : 0,
|
|
||||||
paddingTop: 15,
|
|
||||||
paddingBottom: 15,
|
|
||||||
} : {
|
|
||||||
paddingTop: hasNotch() ? 65 : 15,
|
|
||||||
paddingBottom: hasNotch() ? 35 : 15,
|
|
||||||
};
|
|
||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
modalBackground: {
|
modalBackground: {
|
||||||
...backgroundPadding,
|
...safeAreaPadding,
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
|
|
||||||
@@ -62,7 +57,7 @@ const useStyles = (hasScrollView: boolean, backgroundColor: string|undefined) =>
|
|||||||
zIndex: -1,
|
zIndex: -1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [hasScrollView, isLandscape, backgroundColor]);
|
}, [hasScrollView, safeAreaPadding, backgroundColor]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const useBackgroundTouchListeners = (onRequestClose: (event: GestureResponderEvent)=> void, backdropRef: RefObject<View>) => {
|
const useBackgroundTouchListeners = (onRequestClose: (event: GestureResponderEvent)=> void, backdropRef: RefObject<View>) => {
|
||||||
@@ -114,9 +109,11 @@ const ModalElement: React.FC<ModalElementProps> = ({
|
|||||||
containerStyle,
|
containerStyle,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
scrollOverflow,
|
scrollOverflow,
|
||||||
|
modalBackgroundStyle: extraModalBackgroundStyles,
|
||||||
|
dismissButtonStyle,
|
||||||
...modalProps
|
...modalProps
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles(scrollOverflow, backgroundColor);
|
const styles = useStyles(!!scrollOverflow, backgroundColor);
|
||||||
|
|
||||||
// contentWrapper adds padding. To allow styling the region outside of the modal
|
// contentWrapper adds padding. To allow styling the region outside of the modal
|
||||||
// (e.g. to add a background), the content is wrapped twice.
|
// (e.g. to add a background), the content is wrapped twice.
|
||||||
@@ -134,18 +131,18 @@ const ModalElement: React.FC<ModalElementProps> = ({
|
|||||||
containerRef.current = containerComponent;
|
containerRef.current = containerComponent;
|
||||||
const { onShouldBackgroundCaptureTouch, onBackgroundTouchFinished } = useBackgroundTouchListeners(modalProps.onRequestClose, containerRef);
|
const { onShouldBackgroundCaptureTouch, onBackgroundTouchFinished } = useBackgroundTouchListeners(modalProps.onRequestClose, containerRef);
|
||||||
|
|
||||||
|
|
||||||
// A close button for accessibility tools. Since iOS accessibility focus order is based on the position
|
// A close button for accessibility tools. Since iOS accessibility focus order is based on the position
|
||||||
// of the element on the screen, the close button is placed after the modal content, rather than behind.
|
// of the element on the screen, the close button is placed after the modal content, rather than behind.
|
||||||
const closeButton = modalProps.onRequestClose ? <Pressable
|
const closeButton = modalProps.onRequestClose ? <Pressable
|
||||||
style={styles.dismissButton}
|
style={[styles.dismissButton, dismissButtonStyle]}
|
||||||
onPress={modalProps.onRequestClose}
|
onPress={modalProps.onRequestClose}
|
||||||
accessibilityLabel={_('Close dialog')}
|
accessibilityLabel={_('Close dialog')}
|
||||||
accessibilityRole='button'
|
accessibilityRole='button'
|
||||||
/> : null;
|
/> : null;
|
||||||
|
|
||||||
const contentAndBackdrop = <View
|
const contentAndBackdrop = <View
|
||||||
ref={setContainerComponent}
|
ref={setContainerComponent}
|
||||||
style={styles.modalBackground}
|
style={[styles.modalBackground, extraModalBackgroundStyles]}
|
||||||
onStartShouldSetResponder={onShouldBackgroundCaptureTouch}
|
onStartShouldSetResponder={onShouldBackgroundCaptureTouch}
|
||||||
onResponderRelease={onBackgroundTouchFinished}
|
onResponderRelease={onBackgroundTouchFinished}
|
||||||
>
|
>
|
||||||
@@ -153,6 +150,7 @@ const ModalElement: React.FC<ModalElementProps> = ({
|
|||||||
{closeButton}
|
{closeButton}
|
||||||
</View>;
|
</View>;
|
||||||
|
|
||||||
|
const extraScrollViewProps = (typeof scrollOverflow === 'object' ? scrollOverflow : {});
|
||||||
return (
|
return (
|
||||||
<FocusControl.ModalWrapper state={modalStatus}>
|
<FocusControl.ModalWrapper state={modalStatus}>
|
||||||
<Modal
|
<Modal
|
||||||
@@ -162,8 +160,9 @@ const ModalElement: React.FC<ModalElementProps> = ({
|
|||||||
>
|
>
|
||||||
{scrollOverflow ? (
|
{scrollOverflow ? (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.modalScrollView}
|
{...extraScrollViewProps}
|
||||||
contentContainerStyle={styles.modalScrollViewContent}
|
style={[styles.modalScrollView, extraScrollViewProps.style]}
|
||||||
|
contentContainerStyle={[styles.modalScrollViewContent, extraScrollViewProps.contentContainerStyle]}
|
||||||
>{contentAndBackdrop}</ScrollView>
|
>{contentAndBackdrop}</ScrollView>
|
||||||
) : contentAndBackdrop}
|
) : contentAndBackdrop}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { View } from 'react-native';
|
|
||||||
import Modal from '../Modal';
|
|
||||||
import { useCallback, useState } from 'react';
|
|
||||||
import { _ } from '@joplin/lib/locale';
|
|
||||||
import { PrimaryButton, SecondaryButton } from '../buttons';
|
|
||||||
|
|
||||||
interface MenuItem {
|
|
||||||
label: string;
|
|
||||||
onPress?: ()=> void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
label: string;
|
|
||||||
onPress: ()=> void;
|
|
||||||
actions: MenuItem[]|null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// react-native-paper's floating action button menu is inaccessible on web
|
|
||||||
// (can't be activated by a screen reader, and, in some cases, by the tab key).
|
|
||||||
// This component provides an alternative.
|
|
||||||
|
|
||||||
const AccessibleModalMenu: React.FC<Props> = props => {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
|
||||||
if (props.onPress) {
|
|
||||||
props.onPress();
|
|
||||||
} else {
|
|
||||||
setOpen(!open);
|
|
||||||
}
|
|
||||||
}, [open, props.onPress]);
|
|
||||||
|
|
||||||
const options: React.ReactElement[] = [];
|
|
||||||
for (const action of (props.actions ?? [])) {
|
|
||||||
options.push(
|
|
||||||
<PrimaryButton key={action.label} onPress={action.onPress}>
|
|
||||||
{action.label}
|
|
||||||
</PrimaryButton>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const modal = (
|
|
||||||
<Modal visible={open}>
|
|
||||||
{options}
|
|
||||||
<SecondaryButton onPress={onClick}>{_('Close menu')}</SecondaryButton>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
|
|
||||||
return <View style={{ height: 0, overflow: 'visible' }}>
|
|
||||||
{modal}
|
|
||||||
<SecondaryButton onPress={onClick}>{props.label}</SecondaryButton>
|
|
||||||
</View>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AccessibleModalMenu;
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
|
||||||
import Logger from '@joplin/utils/Logger';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useContext, useEffect, useRef, useState } from 'react';
|
import { useContext, useEffect, useRef, useState } from 'react';
|
||||||
import { AccessibilityInfo, findNodeHandle, Platform, UIManager, View, ViewProps } from 'react-native';
|
import { Platform, View, ViewProps } from 'react-native';
|
||||||
import { AutoFocusContext } from './FocusControl/AutoFocusProvider';
|
import { AutoFocusContext } from './FocusControl/AutoFocusProvider';
|
||||||
|
import Logger from '@joplin/utils/Logger';
|
||||||
|
import focusView from '../../utils/focusView';
|
||||||
|
|
||||||
const logger = Logger.create('AccessibleView');
|
const logger = Logger.create('AccessibleView');
|
||||||
|
|
||||||
@@ -29,33 +29,7 @@ const useAutoFocus = (refocusCounter: number|null, containerNode: View|HTMLEleme
|
|||||||
if (!containerNode) return () => {};
|
if (!containerNode) return () => {};
|
||||||
|
|
||||||
const focusContainer = () => {
|
const focusContainer = () => {
|
||||||
const doFocus = () => {
|
focusView(`AccessibleView::${debugLabelRef.current}`, containerNode);
|
||||||
if (Platform.OS === 'web') {
|
|
||||||
// react-native-web defines UIManager.focus for setting the keyboard focus. However,
|
|
||||||
// this property is not available in standard react-native. As such, access it using type
|
|
||||||
// narrowing:
|
|
||||||
// eslint-disable-next-line no-restricted-properties
|
|
||||||
if (!('focus' in UIManager) || typeof UIManager.focus !== 'function') {
|
|
||||||
throw new Error('Failed to focus sidebar. UIManager.focus is not a function.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable the "use focusHandler for all focus calls" rule -- UIManager.focus requires
|
|
||||||
// an argument, which is not supported by focusHandler.
|
|
||||||
// eslint-disable-next-line no-restricted-properties
|
|
||||||
UIManager.focus(containerNode);
|
|
||||||
} else {
|
|
||||||
const handle = findNodeHandle(containerNode as View);
|
|
||||||
if (handle !== null) {
|
|
||||||
AccessibilityInfo.setAccessibilityFocus(handle);
|
|
||||||
} else {
|
|
||||||
logger.warn('Couldn\'t find a view to focus.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
focus(`AccessibleView::${debugLabelRef.current}`, {
|
|
||||||
focus: doFocus,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const canFocusNow = !autoFocusControlRef.current || autoFocusControlRef.current.canAutoFocus();
|
const canFocusNow = !autoFocusControlRef.current || autoFocusControlRef.current.canAutoFocus();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import NotesScreen from './screens/Notes';
|
import NotesScreen from './screens/Notes/Notes';
|
||||||
import SearchScreen from './screens/SearchScreen';
|
import SearchScreen from './screens/SearchScreen';
|
||||||
import { KeyboardAvoidingView, Platform, View } from 'react-native';
|
import { KeyboardAvoidingView, Platform, View } from 'react-native';
|
||||||
import { AppState } from '../utils/types';
|
import { AppState } from '../utils/types';
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
const React = require('react');
|
import * as React from 'react';
|
||||||
import { useState, useCallback, useMemo } from 'react';
|
import { useState, useCallback, useMemo, useRef } from 'react';
|
||||||
import { FAB, Portal } from 'react-native-paper';
|
import { FAB } from 'react-native-paper';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import { Platform, View, ViewStyle } from 'react-native';
|
import { AccessibilityActionEvent, AccessibilityActionInfo, View } from 'react-native';
|
||||||
import shim from '@joplin/lib/shim';
|
import { connect } from 'react-redux';
|
||||||
import AccessibleWebMenu from '../accessibility/AccessibleModalMenu';
|
import BottomDrawer from '../BottomDrawer';
|
||||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef -- Don't know why it says React is undefined when it's defined above
|
|
||||||
type FABGroupProps = React.ComponentProps<typeof FAB.Group>;
|
|
||||||
|
|
||||||
type OnButtonPress = ()=> void;
|
type OnButtonPress = ()=> void;
|
||||||
interface ButtonSpec {
|
interface ButtonSpec {
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -20,14 +17,18 @@ interface ButtonSpec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ActionButtonProps {
|
interface ActionButtonProps {
|
||||||
buttons?: ButtonSpec[];
|
|
||||||
|
|
||||||
// If not given, an "add" button will be used.
|
// If not given, an "add" button will be used.
|
||||||
mainButton?: ButtonSpec;
|
mainButton: ButtonSpec;
|
||||||
dispatch: Dispatch;
|
dispatch: Dispatch;
|
||||||
}
|
|
||||||
|
|
||||||
const defaultOnPress = () => {};
|
menuContent?: React.ReactNode;
|
||||||
|
onMenuShow?: ()=> void;
|
||||||
|
|
||||||
|
accessibilityActions?: readonly AccessibilityActionInfo[];
|
||||||
|
// Can return a Promise to simplify unit testing
|
||||||
|
onAccessibilityAction?: (event: AccessibilityActionEvent)=> void|Promise<void>;
|
||||||
|
accessibilityHint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Returns a render function compatible with React Native Paper.
|
// Returns a render function compatible with React Native Paper.
|
||||||
const getIconRenderFunction = (iconName: string) => {
|
const getIconRenderFunction = (iconName: string) => {
|
||||||
@@ -43,95 +44,55 @@ const useIcon = (iconName: string) => {
|
|||||||
|
|
||||||
const FloatingActionButton = (props: ActionButtonProps) => {
|
const FloatingActionButton = (props: ActionButtonProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const onMenuToggled: FABGroupProps['onStateChange'] = useCallback(state => {
|
const onMenuToggled = useCallback(() => {
|
||||||
props.dispatch({
|
props.dispatch({
|
||||||
type: 'SIDE_MENU_CLOSE',
|
type: 'SIDE_MENU_CLOSE',
|
||||||
});
|
});
|
||||||
setOpen(state.open);
|
const newOpen = !open;
|
||||||
}, [setOpen, props.dispatch]);
|
setOpen(newOpen);
|
||||||
|
}, [setOpen, open, props.dispatch]);
|
||||||
|
|
||||||
const actions = useMemo(() => (props.buttons ?? []).map(button => {
|
const onDismiss = useCallback(() => {
|
||||||
return {
|
if (open) onMenuToggled();
|
||||||
...button,
|
}, [open, onMenuToggled]);
|
||||||
icon: getIconRenderFunction(button.icon),
|
|
||||||
onPress: button.onPress ?? defaultOnPress,
|
const mainButtonRef = useRef<View>();
|
||||||
};
|
|
||||||
}), [props.buttons]);
|
|
||||||
|
|
||||||
const closedIcon = useIcon(props.mainButton?.icon ?? 'add');
|
const closedIcon = useIcon(props.mainButton?.icon ?? 'add');
|
||||||
const openIcon = useIcon('close');
|
const openIcon = useIcon('close');
|
||||||
|
|
||||||
// To work around an Android accessibility bug, we decrease the
|
|
||||||
// size of the container for the FAB. According to the documentation for
|
|
||||||
// RN Paper, a large action button has size 96x96. As such, we allocate
|
|
||||||
// a larger than this space for the button.
|
|
||||||
//
|
|
||||||
// To prevent the accessibility issue from regressing (which makes it
|
|
||||||
// very hard to access some UI features), we also enable this when Talkback
|
|
||||||
// is disabled.
|
|
||||||
//
|
|
||||||
// See https://github.com/callstack/react-native-paper/issues/4064
|
|
||||||
// May be possible to remove if https://github.com/callstack/react-native-paper/pull/4514
|
|
||||||
// is merged.
|
|
||||||
const adjustMargins = !open && shim.mobilePlatform() === 'android';
|
|
||||||
const marginStyles = useMemo((): ViewStyle => {
|
|
||||||
if (!adjustMargins) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internally, React Native Paper uses absolute positioning to make its
|
|
||||||
// (usually invisible) view fill the screen. Setting top and left to
|
|
||||||
// undefined causes the view to take up only part of the screen.
|
|
||||||
return {
|
|
||||||
top: undefined,
|
|
||||||
left: undefined,
|
|
||||||
};
|
|
||||||
}, [adjustMargins]);
|
|
||||||
|
|
||||||
const label = props.mainButton?.label ?? _('Add new');
|
const label = props.mainButton?.label ?? _('Add new');
|
||||||
|
|
||||||
// On Web, FAB.Group can't be used at all with accessibility tools. Work around this
|
const menuButton = <FAB
|
||||||
// by hiding the FAB for accessibility, and providing a screen-reader-only custom menu.
|
ref={mainButtonRef}
|
||||||
const isWeb = Platform.OS === 'web';
|
|
||||||
const accessibleMenu = isWeb ? (
|
|
||||||
<AccessibleWebMenu
|
|
||||||
label={label}
|
|
||||||
onPress={props.mainButton?.onPress}
|
|
||||||
actions={props.buttons}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const menuContent = <FAB.Group
|
|
||||||
open={open}
|
|
||||||
accessibilityLabel={label}
|
|
||||||
style={marginStyles}
|
|
||||||
icon={open ? openIcon : closedIcon}
|
icon={open ? openIcon : closedIcon}
|
||||||
fabStyle={{
|
accessibilityLabel={label}
|
||||||
backgroundColor: props.mainButton?.color ?? 'rgba(231,76,60,1)',
|
onPress={props.mainButton?.onPress ?? onMenuToggled}
|
||||||
|
style={{
|
||||||
|
alignSelf: 'flex-end',
|
||||||
}}
|
}}
|
||||||
onStateChange={onMenuToggled}
|
accessibilityActions={props.accessibilityActions}
|
||||||
actions={actions}
|
onAccessibilityAction={props.onAccessibilityAction}
|
||||||
onPress={props.mainButton?.onPress ?? defaultOnPress}
|
|
||||||
// The long press delay is too short by default (and we don't use the long press event). See https://github.com/laurent22/joplin/issues/11183.
|
|
||||||
// Increase to a large value:
|
|
||||||
delayLongPress={10_000}
|
|
||||||
visible={true}
|
|
||||||
/>;
|
/>;
|
||||||
const mainMenu = isWeb ? (
|
|
||||||
<View
|
|
||||||
aria-hidden={true}
|
|
||||||
pointerEvents='box-none'
|
|
||||||
tabIndex={-1}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
>{menuContent}</View>
|
|
||||||
) : menuContent;
|
|
||||||
|
|
||||||
return (
|
return <>
|
||||||
<Portal>
|
<View
|
||||||
{mainMenu}
|
style={{
|
||||||
{accessibleMenu}
|
position: 'absolute',
|
||||||
</Portal>
|
bottom: 10,
|
||||||
);
|
right: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{menuButton}
|
||||||
|
</View>
|
||||||
|
<BottomDrawer
|
||||||
|
visible={open}
|
||||||
|
onDismiss={onDismiss}
|
||||||
|
onShow={props.onMenuShow}
|
||||||
|
>
|
||||||
|
{props.menuContent}
|
||||||
|
</BottomDrawer>
|
||||||
|
</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FloatingActionButton;
|
export default connect()(FloatingActionButton);
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Text, TouchableRipple } from 'react-native-paper';
|
||||||
|
import Icon from '../Icon';
|
||||||
|
import { themeStyle } from '../global-style';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { AppState } from '../../utils/types';
|
||||||
|
import { StyleSheet, View, ViewProps } from 'react-native';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
interface Props extends ViewProps {
|
||||||
|
themeId: number;
|
||||||
|
title: string;
|
||||||
|
icon: string;
|
||||||
|
onPress: ()=> void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = (themeId: number) => {
|
||||||
|
return useMemo(() => {
|
||||||
|
const theme = themeStyle(themeId);
|
||||||
|
|
||||||
|
return StyleSheet.create({
|
||||||
|
icon: {
|
||||||
|
fontSize: 27,
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
textAlign: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
|
||||||
|
color: theme.color3,
|
||||||
|
borderColor: theme.codeBorderColor, // TODO: Use a different theme variable
|
||||||
|
borderRadius: 22,
|
||||||
|
padding: 6,
|
||||||
|
borderWidth: 2,
|
||||||
|
backgroundColor: theme.backgroundColor3,
|
||||||
|
},
|
||||||
|
buttonContent: {
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [themeId]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LabelledIconButton: React.FC<Props> = ({ title, icon, style, themeId, ...otherProps }) => {
|
||||||
|
const styles = useStyles(themeId);
|
||||||
|
return <TouchableRipple
|
||||||
|
borderless={true}
|
||||||
|
role='button'
|
||||||
|
accessibilityRole='button'
|
||||||
|
{...otherProps}
|
||||||
|
style={[styles.button, style]}
|
||||||
|
>
|
||||||
|
<View style={styles.buttonContent}>
|
||||||
|
<Icon style={styles.icon} accessibilityLabel={null} name={icon}/>
|
||||||
|
<Text variant='labelMedium'>{title}</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableRipple>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect((state: AppState) => {
|
||||||
|
return { themeId: state.settings.theme };
|
||||||
|
})(LabelledIconButton);
|
||||||
@@ -4,6 +4,7 @@ import { themeStyle } from '../global-style';
|
|||||||
import { Button, ButtonProps } from 'react-native-paper';
|
import { Button, ButtonProps } from 'react-native-paper';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { AppState } from '../../utils/types';
|
import { AppState } from '../../utils/types';
|
||||||
|
import { TextStyle, StyleSheet, ViewStyle, StyleProp } from 'react-native';
|
||||||
|
|
||||||
export enum ButtonType {
|
export enum ButtonType {
|
||||||
Primary,
|
Primary,
|
||||||
@@ -12,9 +13,16 @@ export enum ButtonType {
|
|||||||
Link,
|
Link,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props extends Omit<ButtonProps, 'item'|'onPress'|'children'> {
|
export enum ButtonSize {
|
||||||
|
Normal,
|
||||||
|
Larger,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props extends Omit<ButtonProps, 'item'|'onPress'|'children'|'style'> {
|
||||||
themeId: number;
|
themeId: number;
|
||||||
type: ButtonType;
|
type: ButtonType;
|
||||||
|
size?: ButtonSize;
|
||||||
|
style?: TextStyle;
|
||||||
onPress: ()=> void;
|
onPress: ()=> void;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
@@ -41,12 +49,25 @@ const useStyles = ({ themeId }: Props) => {
|
|||||||
primaryButton: { },
|
primaryButton: { },
|
||||||
};
|
};
|
||||||
|
|
||||||
return { themeOverride };
|
return {
|
||||||
|
themeOverride,
|
||||||
|
styles: StyleSheet.create({
|
||||||
|
largeContainer: {
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
largeLabel: {
|
||||||
|
fontSize: theme.fontSize,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
}, [themeId]);
|
}, [themeId]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TextButton: React.FC<Props> = props => {
|
const TextButton: React.FC<Props> = props => {
|
||||||
const { themeOverride } = useStyles(props);
|
const { themeOverride, styles } = useStyles(props);
|
||||||
|
|
||||||
let mode: ButtonProps['mode'];
|
let mode: ButtonProps['mode'];
|
||||||
let theme: ButtonProps['theme'];
|
let theme: ButtonProps['theme'];
|
||||||
@@ -68,8 +89,19 @@ const TextButton: React.FC<Props> = props => {
|
|||||||
return exhaustivenessCheck;
|
return exhaustivenessCheck;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let labelStyle: TextStyle|undefined = undefined;
|
||||||
|
const containerStyle: StyleProp<ViewStyle>[] = [];
|
||||||
|
if (props.size === ButtonSize.Larger) {
|
||||||
|
labelStyle = styles.largeLabel;
|
||||||
|
containerStyle.push(styles.largeContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.style) containerStyle.push(props.style);
|
||||||
|
|
||||||
return <Button
|
return <Button
|
||||||
|
labelStyle={labelStyle}
|
||||||
{...props}
|
{...props}
|
||||||
|
style={containerStyle}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
onPress={props.onPress}
|
onPress={props.onPress}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const Color = require('color');
|
|||||||
const baseStyle = {
|
const baseStyle = {
|
||||||
appearance: 'light',
|
appearance: 'light',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
fontSizeLarger: 18,
|
||||||
fontSizeLarge: 20,
|
fontSizeLarge: 20,
|
||||||
margin: 15, // No text and no interactive component should be within this margin
|
margin: 15, // No text and no interactive component should be within this margin
|
||||||
itemMarginTop: 10,
|
itemMarginTop: 10,
|
||||||
|
|||||||
@@ -1622,7 +1622,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
|||||||
|
|
||||||
if (this.state.mode === 'edit') return null;
|
if (this.state.mode === 'edit') return null;
|
||||||
|
|
||||||
return <FloatingActionButton mainButton={editButton} dispatch={this.props.dispatch} />;
|
return <FloatingActionButton mainButton={editButton} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save button is not really needed anymore with the improved save logic
|
// Save button is not really needed anymore with the improved save logic
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import TestProviderStack from '../../testing/TestProviderStack';
|
||||||
|
import NewNoteButton from './NewNoteButton';
|
||||||
|
import { AppState } from '../../../utils/types';
|
||||||
|
import { Store } from 'redux';
|
||||||
|
import createMockReduxStore from '../../../utils/testing/createMockReduxStore';
|
||||||
|
import setupGlobalStore from '../../../utils/testing/setupGlobalStore';
|
||||||
|
import { act, render, screen, waitFor } from '@testing-library/react-native';
|
||||||
|
import '@testing-library/jest-native/extend-expect';
|
||||||
|
import { AccessibilityActionInfo } from 'react-native';
|
||||||
|
import { setupDatabaseAndSynchronizer } from '@joplin/lib/testing/test-utils';
|
||||||
|
import Folder from '@joplin/lib/models/Folder';
|
||||||
|
import NavService from '@joplin/lib/services/NavService';
|
||||||
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
import Note from '@joplin/lib/models/Note';
|
||||||
|
|
||||||
|
let testStore: Store<AppState>;
|
||||||
|
|
||||||
|
interface WrappedNewNoteButtonProps {}
|
||||||
|
|
||||||
|
const WrappedNewNoteButton: React.FC<WrappedNewNoteButtonProps> = () => {
|
||||||
|
return <TestProviderStack store={testStore}>
|
||||||
|
<NewNoteButton/>
|
||||||
|
</TestProviderStack>;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('NewNoteButton', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await setupDatabaseAndSynchronizer(0);
|
||||||
|
testStore = createMockReduxStore();
|
||||||
|
setupGlobalStore(testStore);
|
||||||
|
|
||||||
|
// Set an initial folder
|
||||||
|
const folder = await Folder.save({ title: 'Test folder' });
|
||||||
|
Setting.setValue('activeFolderId', folder.id);
|
||||||
|
await NavService.go('Notes', { folderId: folder.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be possible to create a note using accessibility actions', async () => {
|
||||||
|
const wrapper = render(<WrappedNewNoteButton/>);
|
||||||
|
|
||||||
|
const toggleButton = screen.getByRole('button', { name: 'Add new' });
|
||||||
|
expect(toggleButton).toBeVisible();
|
||||||
|
|
||||||
|
const actions: AccessibilityActionInfo[] = toggleButton.props.accessibilityActions;
|
||||||
|
const newNoteAction = actions.find(action => action.label === 'New note');
|
||||||
|
expect(newNoteAction).toBeTruthy();
|
||||||
|
|
||||||
|
const onAction = toggleButton.props.onAccessibilityAction;
|
||||||
|
await act(() => {
|
||||||
|
return onAction({ nativeEvent: { actionName: newNoteAction.name } });
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(async () => {
|
||||||
|
expect(await Note.allIds()).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
140
packages/app-mobile/components/screens/Notes/NewNoteButton.tsx
Normal file
140
packages/app-mobile/components/screens/Notes/NewNoteButton.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import CommandService from '@joplin/lib/services/CommandService';
|
||||||
|
import { Divider } from 'react-native-paper';
|
||||||
|
import FloatingActionButton from '../../buttons/FloatingActionButton';
|
||||||
|
import { AccessibilityActionEvent, AccessibilityActionInfo, StyleSheet, View } from 'react-native';
|
||||||
|
import { AttachFileAction } from '../Note/commands/attachFile';
|
||||||
|
import LabelledIconButton from '../../buttons/LabelledIconButton';
|
||||||
|
import TextButton, { ButtonSize, ButtonType } from '../../buttons/TextButton';
|
||||||
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
|
import Logger from '@joplin/utils/Logger';
|
||||||
|
import focusView from '../../../utils/focusView';
|
||||||
|
|
||||||
|
const logger = Logger.create('NewNoteButton');
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeNewNote = (isTodo: boolean, action?: AttachFileAction) => {
|
||||||
|
logger.debug(`New ${isTodo ? 'to-do' : 'note'} with action`, action);
|
||||||
|
const body = '';
|
||||||
|
return CommandService.instance().execute('newNote', body, isTodo, { attachFileAction: action });
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
buttonRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 2,
|
||||||
|
},
|
||||||
|
mainButtonRow: {
|
||||||
|
flexWrap: 'nowrap',
|
||||||
|
},
|
||||||
|
spacer: {
|
||||||
|
flexShrink: 1,
|
||||||
|
flexGrow: 0,
|
||||||
|
width: 12,
|
||||||
|
},
|
||||||
|
shortcutButton: {
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
mainButton: {
|
||||||
|
flexShrink: 1,
|
||||||
|
},
|
||||||
|
mainButtonLabel: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
menuContent: {
|
||||||
|
gap: 12,
|
||||||
|
flexShrink: 1,
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const NewNoteButton: React.FC<Props> = _props => {
|
||||||
|
const newNoteRef = useRef<View|null>(null);
|
||||||
|
|
||||||
|
const renderShortcutButton = (action: AttachFileAction, icon: string, title: string) => {
|
||||||
|
return <LabelledIconButton
|
||||||
|
onPress={() => makeNewNote(false, action)}
|
||||||
|
style={styles.shortcutButton}
|
||||||
|
title={title}
|
||||||
|
accessibilityHint={_('Creates a new note with an attachment of type %s', title)}
|
||||||
|
icon={icon}
|
||||||
|
/>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuContent = <View style={styles.menuContent}>
|
||||||
|
<View style={styles.buttonRow}>
|
||||||
|
{renderShortcutButton(AttachFileAction.AttachFile, 'material attachment', _('Attachment'))}
|
||||||
|
{renderShortcutButton(AttachFileAction.RecordAudio, 'material microphone', _('Recording'))}
|
||||||
|
{renderShortcutButton(AttachFileAction.TakePhoto, 'material camera', _('Camera'))}
|
||||||
|
{renderShortcutButton(AttachFileAction.AttachDrawing, 'material draw', _('Drawing'))}
|
||||||
|
</View>
|
||||||
|
<Divider/>
|
||||||
|
<View style={[styles.buttonRow, styles.mainButtonRow]}>
|
||||||
|
<TextButton
|
||||||
|
icon='checkbox-outline'
|
||||||
|
style={styles.mainButton}
|
||||||
|
onPress={() => {
|
||||||
|
void makeNewNote(true);
|
||||||
|
}}
|
||||||
|
type={ButtonType.Secondary}
|
||||||
|
size={ButtonSize.Larger}
|
||||||
|
>{_('New to-do')}</TextButton>
|
||||||
|
<View style={styles.spacer}/>
|
||||||
|
<TextButton
|
||||||
|
touchableRef={newNoteRef}
|
||||||
|
icon='file-document-outline'
|
||||||
|
style={styles.mainButton}
|
||||||
|
onPress={() => {
|
||||||
|
void makeNewNote(false);
|
||||||
|
}}
|
||||||
|
type={ButtonType.Primary}
|
||||||
|
size={ButtonSize.Larger}
|
||||||
|
>{_('New note')}</TextButton>
|
||||||
|
</View>
|
||||||
|
</View>;
|
||||||
|
|
||||||
|
// Android and iOS: Accessibility actions simplify creating new notes and to-dos. These
|
||||||
|
// are extra important because the "note with attachment" items are annoyingly first in
|
||||||
|
// the focus order (and it doesn't seem possible to change this without adding a new
|
||||||
|
// dependency).
|
||||||
|
const accessibilityActions = useMemo((): AccessibilityActionInfo[] => {
|
||||||
|
return [{
|
||||||
|
name: 'new-note',
|
||||||
|
label: _('New note'),
|
||||||
|
}, {
|
||||||
|
name: 'new-to-do',
|
||||||
|
label: _('New to-do'),
|
||||||
|
}];
|
||||||
|
}, []);
|
||||||
|
const onAccessibilityAction = useCallback((event: AccessibilityActionEvent) => {
|
||||||
|
if (event.nativeEvent.actionName === 'new-note') {
|
||||||
|
return makeNewNote(false);
|
||||||
|
} else if (event.nativeEvent.actionName === 'new-to-do') {
|
||||||
|
return makeNewNote(true);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}, []);
|
||||||
|
const onMenuShown = useCallback(() => {
|
||||||
|
// Note: May apply only to web:
|
||||||
|
focusView('NewNoteButton', newNoteRef.current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <FloatingActionButton
|
||||||
|
mainButton={{
|
||||||
|
icon: 'add',
|
||||||
|
label: _('Add new'),
|
||||||
|
}}
|
||||||
|
menuContent={menuContent}
|
||||||
|
onMenuShow={onMenuShown}
|
||||||
|
accessibilityActions={accessibilityActions}
|
||||||
|
onAccessibilityAction={onAccessibilityAction}
|
||||||
|
/>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewNoteButton;
|
||||||
@@ -2,24 +2,24 @@ import * as React from 'react';
|
|||||||
import { AppState as RNAppState, View, StyleSheet, NativeEventSubscription, ViewStyle, TextStyle } from 'react-native';
|
import { AppState as RNAppState, View, StyleSheet, NativeEventSubscription, ViewStyle, TextStyle } from 'react-native';
|
||||||
import { stateUtils } from '@joplin/lib/reducer';
|
import { stateUtils } from '@joplin/lib/reducer';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import NoteList from '../NoteList';
|
import NoteList from '../../NoteList';
|
||||||
import Folder from '@joplin/lib/models/Folder';
|
import Folder from '@joplin/lib/models/Folder';
|
||||||
import Tag from '@joplin/lib/models/Tag';
|
import Tag from '@joplin/lib/models/Tag';
|
||||||
import Note, { PreviewsOrder } from '@joplin/lib/models/Note';
|
import Note, { PreviewsOrder } from '@joplin/lib/models/Note';
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
import { themeStyle } from '../global-style';
|
import { themeStyle } from '../../global-style';
|
||||||
import { FolderPickerOptions, ScreenHeader } from '../ScreenHeader';
|
import { FolderPickerOptions, ScreenHeader } from '../../ScreenHeader';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import ActionButton from '../buttons/FloatingActionButton';
|
import { BaseScreenComponent } from '../../base-screen';
|
||||||
import { BaseScreenComponent } from '../base-screen';
|
import { AppState } from '../../../utils/types';
|
||||||
import { AppState } from '../../utils/types';
|
|
||||||
import { FolderEntity, NoteEntity, TagEntity } from '@joplin/lib/services/database/types';
|
import { FolderEntity, NoteEntity, TagEntity } from '@joplin/lib/services/database/types';
|
||||||
import { itemIsInTrash } from '@joplin/lib/services/trash';
|
import { itemIsInTrash } from '@joplin/lib/services/trash';
|
||||||
import AccessibleView from '../accessibility/AccessibleView';
|
import AccessibleView from '../../accessibility/AccessibleView';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import { DialogContext, DialogControl } from '../DialogManager';
|
import { DialogContext, DialogControl } from '../../DialogManager';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { MenuChoice } from '../DialogManager/types';
|
import { MenuChoice } from '../../DialogManager/types';
|
||||||
|
import NewNoteButton from './NewNoteButton';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dispatch: Dispatch;
|
dispatch: Dispatch;
|
||||||
@@ -252,27 +252,7 @@ class NotesScreenComponent extends BaseScreenComponent<ComponentProps, State> {
|
|||||||
if ((this.props.notesParentType === 'Folder' && itemIsInTrash(parent)) || !Folder.atLeastOneRealFolderExists(this.props.folders)) return null;
|
if ((this.props.notesParentType === 'Folder' && itemIsInTrash(parent)) || !Folder.atLeastOneRealFolderExists(this.props.folders)) return null;
|
||||||
|
|
||||||
if (addFolderNoteButtons && this.props.folders.length > 0) {
|
if (addFolderNoteButtons && this.props.folders.length > 0) {
|
||||||
const buttons = [];
|
return <NewNoteButton />;
|
||||||
buttons.push({
|
|
||||||
label: _('New to-do'),
|
|
||||||
onPress: async () => {
|
|
||||||
const isTodo = true;
|
|
||||||
void this.newNoteNavigate(buttonFolderId, isTodo);
|
|
||||||
},
|
|
||||||
color: '#9b59b6',
|
|
||||||
icon: 'checkbox-outline',
|
|
||||||
});
|
|
||||||
|
|
||||||
buttons.push({
|
|
||||||
label: _('New note'),
|
|
||||||
onPress: async () => {
|
|
||||||
const isTodo = false;
|
|
||||||
void this.newNoteNavigate(buttonFolderId, isTodo);
|
|
||||||
},
|
|
||||||
color: '#9b59b6',
|
|
||||||
icon: 'document',
|
|
||||||
});
|
|
||||||
return <ActionButton buttons={buttons} dispatch={this.props.dispatch}/>;
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@@ -55,7 +55,7 @@ import Revision from '@joplin/lib/models/Revision';
|
|||||||
import RevisionService from '@joplin/lib/services/RevisionService';
|
import RevisionService from '@joplin/lib/services/RevisionService';
|
||||||
import JoplinDatabase from '@joplin/lib/JoplinDatabase';
|
import JoplinDatabase from '@joplin/lib/JoplinDatabase';
|
||||||
import Database from '@joplin/lib/database';
|
import Database from '@joplin/lib/database';
|
||||||
import NotesScreen from './components/screens/Notes';
|
import NotesScreen from './components/screens/Notes/Notes';
|
||||||
import TagsScreen from './components/screens/tags';
|
import TagsScreen from './components/screens/tags';
|
||||||
import ConfigScreen from './components/screens/ConfigScreen/ConfigScreen';
|
import ConfigScreen from './components/screens/ConfigScreen/ConfigScreen';
|
||||||
const { FolderScreen } = require('./components/screens/folder.js');
|
const { FolderScreen } = require('./components/screens/folder.js');
|
||||||
|
|||||||
37
packages/app-mobile/utils/focusView.ts
Normal file
37
packages/app-mobile/utils/focusView.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||||
|
import Logger from '@joplin/utils/Logger';
|
||||||
|
import { AccessibilityInfo, findNodeHandle, Platform, UIManager, View } from 'react-native';
|
||||||
|
|
||||||
|
const logger = Logger.create('focusView');
|
||||||
|
|
||||||
|
const focusView = (source: string, view: View|HTMLElement) => {
|
||||||
|
const autoFocus = () => {
|
||||||
|
if (Platform.OS === 'web') {
|
||||||
|
// react-native-web defines UIManager.focus for setting the keyboard focus. However,
|
||||||
|
// this property is not available in standard react-native. As such, access it using type
|
||||||
|
// narrowing:
|
||||||
|
// eslint-disable-next-line no-restricted-properties
|
||||||
|
if (!('focus' in UIManager) || typeof UIManager.focus !== 'function') {
|
||||||
|
throw new Error('Failed to focus sidebar. UIManager.focus is not a function.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable the "use focusHandler for all focus calls" rule -- UIManager.focus requires
|
||||||
|
// an argument, which is not supported by focusHandler.
|
||||||
|
// eslint-disable-next-line no-restricted-properties
|
||||||
|
UIManager.focus(view);
|
||||||
|
} else {
|
||||||
|
const handle = findNodeHandle(view as View);
|
||||||
|
if (handle !== null) {
|
||||||
|
AccessibilityInfo.setAccessibilityFocus(handle);
|
||||||
|
} else {
|
||||||
|
logger.warn('Couldn\'t find a view to focus.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
focus(`focusView:${source}`, {
|
||||||
|
focus: autoFocus,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default focusView;
|
||||||
24
packages/app-mobile/utils/hooks/useSafeAreaPadding.ts
Normal file
24
packages/app-mobile/utils/hooks/useSafeAreaPadding.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useWindowDimensions } from 'react-native';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
|
const useSafeAreaPadding = () => {
|
||||||
|
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
||||||
|
const safeAreaInsets = useSafeAreaInsets();
|
||||||
|
const isLandscape = windowWidth > windowHeight;
|
||||||
|
return useMemo(() => {
|
||||||
|
return isLandscape ? {
|
||||||
|
paddingRight: safeAreaInsets.right,
|
||||||
|
paddingLeft: safeAreaInsets.left,
|
||||||
|
paddingTop: 15,
|
||||||
|
paddingBottom: 15,
|
||||||
|
} : {
|
||||||
|
paddingTop: safeAreaInsets.top,
|
||||||
|
paddingBottom: safeAreaInsets.bottom,
|
||||||
|
paddingLeft: 0,
|
||||||
|
paddingRight: 0,
|
||||||
|
};
|
||||||
|
}, [isLandscape, safeAreaInsets]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSafeAreaPadding;
|
||||||
@@ -27,6 +27,7 @@ const input: Theme = {
|
|||||||
colorError2: '#ff6c6c',
|
colorError2: '#ff6c6c',
|
||||||
colorWarn2: '#ffcb81',
|
colorWarn2: '#ffcb81',
|
||||||
colorWarn3: '#ff7626',
|
colorWarn3: '#ff7626',
|
||||||
|
backgroundColorTransparent2: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
|
||||||
// Color scheme "3" is used for the config screens for example/
|
// Color scheme "3" is used for the config screens for example/
|
||||||
// It's dark text over gray background.
|
// It's dark text over gray background.
|
||||||
@@ -71,6 +72,7 @@ const expected = `
|
|||||||
--joplin-background-color4: #ffffff;
|
--joplin-background-color4: #ffffff;
|
||||||
--joplin-background-color-hover3: #CBDAF1;
|
--joplin-background-color-hover3: #CBDAF1;
|
||||||
--joplin-background-color-transparent: rgba(255,255,255,0.9);
|
--joplin-background-color-transparent: rgba(255,255,255,0.9);
|
||||||
|
--joplin-background-color-transparent2: rgba(0, 0, 0, 0.1);
|
||||||
--joplin-block-quote-opacity: 0.7;
|
--joplin-block-quote-opacity: 0.7;
|
||||||
--joplin-code-background-color: rgb(243, 243, 243);
|
--joplin-code-background-color: rgb(243, 243, 243);
|
||||||
--joplin-code-border-color: rgb(220, 220, 220);
|
--joplin-code-border-color: rgb(220, 220, 220);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const theme: Theme = {
|
|||||||
colorError2: '#ff7070',
|
colorError2: '#ff7070',
|
||||||
colorWarn2: '#ffcb81',
|
colorWarn2: '#ffcb81',
|
||||||
colorWarn3: '#ff7626',
|
colorWarn3: '#ff7626',
|
||||||
|
backgroundColorTransparent2: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
|
||||||
// Color scheme "3" is used for the config screens for example/
|
// Color scheme "3" is used for the config screens for example/
|
||||||
// It's dark text over gray background.
|
// It's dark text over gray background.
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export interface Theme {
|
|||||||
// Color scheme "2" is used for the sidebar. It's white text over
|
// Color scheme "2" is used for the sidebar. It's white text over
|
||||||
// dark blue background.
|
// dark blue background.
|
||||||
backgroundColor2: string;
|
backgroundColor2: string;
|
||||||
|
backgroundColorTransparent2: string; // Used for dimmed region outside modals
|
||||||
color2: string;
|
color2: string;
|
||||||
selectedColor2: string;
|
selectedColor2: string;
|
||||||
colorError2: string;
|
colorError2: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user