You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-10 22:11:50 +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/showResource.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/Camera/index.jest.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/SideMenuContentNote.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.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/sensorInfo.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/index.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/types.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/index.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.web.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/fs-driver-rn.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/useOnLongPressProps.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/getImageDimensions.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/showResource.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/Camera/index.jest.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/SideMenuContentNote.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.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/sensorInfo.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/index.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/types.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/index.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.web.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/fs-driver-rn.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/useOnLongPressProps.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/getImageDimensions.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 useDialogControl from './hooks/useDialogControl';
|
||||
import PromptDialog from './PromptDialog';
|
||||
import { themeStyle } from '../global-style';
|
||||
|
||||
export type { DialogControl } from './types';
|
||||
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 dialogComponents: React.ReactNode[] = [];
|
||||
@@ -73,7 +75,7 @@ const DialogManager: React.FC<Props> = props => {
|
||||
scrollOverflow={true}
|
||||
containerStyle={styles.modalContainer}
|
||||
animationType='fade'
|
||||
backgroundColor='rgba(0, 0, 0, 0.1)'
|
||||
backgroundColor={theme.backgroundColorTransparent2}
|
||||
transparent={true}
|
||||
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 theme = themeStyle(props.themeId);
|
||||
const styles = useStyles(props.themeId, props.containerStyle, props.size);
|
||||
|
||||
const heading = props.heading ? (
|
||||
@@ -92,7 +93,7 @@ const DismissibleDialog: React.FC<Props> = props => {
|
||||
onRequestClose={props.onDismiss}
|
||||
containerStyle={styles.dialogContainer}
|
||||
animationType='fade'
|
||||
backgroundColor='rgba(0, 0, 0, 0.1)'
|
||||
backgroundColor={theme.backgroundColorTransparent2}
|
||||
transparent={true}
|
||||
>
|
||||
<Surface style={styles.dialogSurface} elevation={1}>
|
||||
|
@@ -1,40 +1,35 @@
|
||||
import * as React 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 { hasNotch } from 'react-native-device-info';
|
||||
import { GestureResponderEvent, Modal, ModalProps, Platform, Pressable, ScrollView, ScrollViewProps, StyleSheet, View, ViewStyle } from 'react-native';
|
||||
import FocusControl from './accessibility/FocusControl/FocusControl';
|
||||
import { msleep, Second } from '@joplin/utils/time';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { ModalState } from './accessibility/FocusControl/types';
|
||||
import useSafeAreaPadding from '../utils/hooks/useSafeAreaPadding';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
interface ModalElementProps extends ModalProps {
|
||||
children: React.ReactNode;
|
||||
containerStyle?: ViewStyle;
|
||||
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
|
||||
// ScrollView. This allows the user to scroll parts of dialogs into
|
||||
// view that would otherwise be clipped by the screen edge.
|
||||
scrollOverflow?: boolean;
|
||||
scrollOverflow?: boolean|ScrollViewProps;
|
||||
}
|
||||
|
||||
const useStyles = (hasScrollView: boolean, backgroundColor: string|undefined) => {
|
||||
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
||||
const isLandscape = windowWidth > windowHeight;
|
||||
const safeAreaPadding = useSafeAreaPadding();
|
||||
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({
|
||||
modalBackground: {
|
||||
...backgroundPadding,
|
||||
...safeAreaPadding,
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
|
||||
@@ -62,7 +57,7 @@ const useStyles = (hasScrollView: boolean, backgroundColor: string|undefined) =>
|
||||
zIndex: -1,
|
||||
},
|
||||
});
|
||||
}, [hasScrollView, isLandscape, backgroundColor]);
|
||||
}, [hasScrollView, safeAreaPadding, backgroundColor]);
|
||||
};
|
||||
|
||||
const useBackgroundTouchListeners = (onRequestClose: (event: GestureResponderEvent)=> void, backdropRef: RefObject<View>) => {
|
||||
@@ -114,9 +109,11 @@ const ModalElement: React.FC<ModalElementProps> = ({
|
||||
containerStyle,
|
||||
backgroundColor,
|
||||
scrollOverflow,
|
||||
modalBackgroundStyle: extraModalBackgroundStyles,
|
||||
dismissButtonStyle,
|
||||
...modalProps
|
||||
}) => {
|
||||
const styles = useStyles(scrollOverflow, backgroundColor);
|
||||
const styles = useStyles(!!scrollOverflow, backgroundColor);
|
||||
|
||||
// contentWrapper adds padding. To allow styling the region outside of the modal
|
||||
// (e.g. to add a background), the content is wrapped twice.
|
||||
@@ -134,18 +131,18 @@ const ModalElement: React.FC<ModalElementProps> = ({
|
||||
containerRef.current = containerComponent;
|
||||
const { onShouldBackgroundCaptureTouch, onBackgroundTouchFinished } = useBackgroundTouchListeners(modalProps.onRequestClose, containerRef);
|
||||
|
||||
|
||||
// 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.
|
||||
const closeButton = modalProps.onRequestClose ? <Pressable
|
||||
style={styles.dismissButton}
|
||||
style={[styles.dismissButton, dismissButtonStyle]}
|
||||
onPress={modalProps.onRequestClose}
|
||||
accessibilityLabel={_('Close dialog')}
|
||||
accessibilityRole='button'
|
||||
/> : null;
|
||||
|
||||
const contentAndBackdrop = <View
|
||||
ref={setContainerComponent}
|
||||
style={styles.modalBackground}
|
||||
style={[styles.modalBackground, extraModalBackgroundStyles]}
|
||||
onStartShouldSetResponder={onShouldBackgroundCaptureTouch}
|
||||
onResponderRelease={onBackgroundTouchFinished}
|
||||
>
|
||||
@@ -153,6 +150,7 @@ const ModalElement: React.FC<ModalElementProps> = ({
|
||||
{closeButton}
|
||||
</View>;
|
||||
|
||||
const extraScrollViewProps = (typeof scrollOverflow === 'object' ? scrollOverflow : {});
|
||||
return (
|
||||
<FocusControl.ModalWrapper state={modalStatus}>
|
||||
<Modal
|
||||
@@ -162,8 +160,9 @@ const ModalElement: React.FC<ModalElementProps> = ({
|
||||
>
|
||||
{scrollOverflow ? (
|
||||
<ScrollView
|
||||
style={styles.modalScrollView}
|
||||
contentContainerStyle={styles.modalScrollViewContent}
|
||||
{...extraScrollViewProps}
|
||||
style={[styles.modalScrollView, extraScrollViewProps.style]}
|
||||
contentContainerStyle={[styles.modalScrollViewContent, extraScrollViewProps.contentContainerStyle]}
|
||||
>{contentAndBackdrop}</ScrollView>
|
||||
) : contentAndBackdrop}
|
||||
</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 { 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 Logger from '@joplin/utils/Logger';
|
||||
import focusView from '../../utils/focusView';
|
||||
|
||||
const logger = Logger.create('AccessibleView');
|
||||
|
||||
@@ -29,33 +29,7 @@ const useAutoFocus = (refocusCounter: number|null, containerNode: View|HTMLEleme
|
||||
if (!containerNode) return () => {};
|
||||
|
||||
const focusContainer = () => {
|
||||
const doFocus = () => {
|
||||
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,
|
||||
});
|
||||
focusView(`AccessibleView::${debugLabelRef.current}`, containerNode);
|
||||
};
|
||||
|
||||
const canFocusNow = !autoFocusControlRef.current || autoFocusControlRef.current.canAutoFocus();
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import NotesScreen from './screens/Notes';
|
||||
import NotesScreen from './screens/Notes/Notes';
|
||||
import SearchScreen from './screens/SearchScreen';
|
||||
import { KeyboardAvoidingView, Platform, View } from 'react-native';
|
||||
import { AppState } from '../utils/types';
|
||||
|
@@ -1,16 +1,13 @@
|
||||
const React = require('react');
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { FAB, Portal } from 'react-native-paper';
|
||||
import * as React from 'react';
|
||||
import { useState, useCallback, useMemo, useRef } from 'react';
|
||||
import { FAB } from 'react-native-paper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { Dispatch } from 'redux';
|
||||
import { Platform, View, ViewStyle } from 'react-native';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import AccessibleWebMenu from '../accessibility/AccessibleModalMenu';
|
||||
import { AccessibilityActionEvent, AccessibilityActionInfo, View } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import BottomDrawer from '../BottomDrawer';
|
||||
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;
|
||||
interface ButtonSpec {
|
||||
icon: string;
|
||||
@@ -20,14 +17,18 @@ interface ButtonSpec {
|
||||
}
|
||||
|
||||
interface ActionButtonProps {
|
||||
buttons?: ButtonSpec[];
|
||||
|
||||
// If not given, an "add" button will be used.
|
||||
mainButton?: ButtonSpec;
|
||||
mainButton: ButtonSpec;
|
||||
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.
|
||||
const getIconRenderFunction = (iconName: string) => {
|
||||
@@ -43,95 +44,55 @@ const useIcon = (iconName: string) => {
|
||||
|
||||
const FloatingActionButton = (props: ActionButtonProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const onMenuToggled: FABGroupProps['onStateChange'] = useCallback(state => {
|
||||
const onMenuToggled = useCallback(() => {
|
||||
props.dispatch({
|
||||
type: 'SIDE_MENU_CLOSE',
|
||||
});
|
||||
setOpen(state.open);
|
||||
}, [setOpen, props.dispatch]);
|
||||
const newOpen = !open;
|
||||
setOpen(newOpen);
|
||||
}, [setOpen, open, props.dispatch]);
|
||||
|
||||
const actions = useMemo(() => (props.buttons ?? []).map(button => {
|
||||
return {
|
||||
...button,
|
||||
icon: getIconRenderFunction(button.icon),
|
||||
onPress: button.onPress ?? defaultOnPress,
|
||||
};
|
||||
}), [props.buttons]);
|
||||
const onDismiss = useCallback(() => {
|
||||
if (open) onMenuToggled();
|
||||
}, [open, onMenuToggled]);
|
||||
|
||||
const mainButtonRef = useRef<View>();
|
||||
|
||||
const closedIcon = useIcon(props.mainButton?.icon ?? 'add');
|
||||
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');
|
||||
|
||||
// On Web, FAB.Group can't be used at all with accessibility tools. Work around this
|
||||
// by hiding the FAB for accessibility, and providing a screen-reader-only custom menu.
|
||||
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}
|
||||
const menuButton = <FAB
|
||||
ref={mainButtonRef}
|
||||
icon={open ? openIcon : closedIcon}
|
||||
accessibilityLabel={label}
|
||||
style={marginStyles}
|
||||
icon={ open ? openIcon : closedIcon }
|
||||
fabStyle={{
|
||||
backgroundColor: props.mainButton?.color ?? 'rgba(231,76,60,1)',
|
||||
onPress={props.mainButton?.onPress ?? onMenuToggled}
|
||||
style={{
|
||||
alignSelf: 'flex-end',
|
||||
}}
|
||||
onStateChange={onMenuToggled}
|
||||
actions={actions}
|
||||
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}
|
||||
accessibilityActions={props.accessibilityActions}
|
||||
onAccessibilityAction={props.onAccessibilityAction}
|
||||
/>;
|
||||
const mainMenu = isWeb ? (
|
||||
<View
|
||||
aria-hidden={true}
|
||||
pointerEvents='box-none'
|
||||
tabIndex={-1}
|
||||
style={{ flex: 1 }}
|
||||
>{menuContent}</View>
|
||||
) : menuContent;
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
{mainMenu}
|
||||
{accessibleMenu}
|
||||
</Portal>
|
||||
);
|
||||
return <>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
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 { connect } from 'react-redux';
|
||||
import { AppState } from '../../utils/types';
|
||||
import { TextStyle, StyleSheet, ViewStyle, StyleProp } from 'react-native';
|
||||
|
||||
export enum ButtonType {
|
||||
Primary,
|
||||
@@ -12,9 +13,16 @@ export enum ButtonType {
|
||||
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;
|
||||
type: ButtonType;
|
||||
size?: ButtonSize;
|
||||
style?: TextStyle;
|
||||
onPress: ()=> void;
|
||||
children: ReactNode;
|
||||
}
|
||||
@@ -41,12 +49,25 @@ const useStyles = ({ themeId }: Props) => {
|
||||
primaryButton: { },
|
||||
};
|
||||
|
||||
return { themeOverride };
|
||||
return {
|
||||
themeOverride,
|
||||
styles: StyleSheet.create({
|
||||
largeContainer: {
|
||||
paddingVertical: 2,
|
||||
borderWidth: 2,
|
||||
borderRadius: 10,
|
||||
},
|
||||
largeLabel: {
|
||||
fontSize: theme.fontSize,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
}),
|
||||
};
|
||||
}, [themeId]);
|
||||
};
|
||||
|
||||
const TextButton: React.FC<Props> = props => {
|
||||
const { themeOverride } = useStyles(props);
|
||||
const { themeOverride, styles } = useStyles(props);
|
||||
|
||||
let mode: ButtonProps['mode'];
|
||||
let theme: ButtonProps['theme'];
|
||||
@@ -68,8 +89,19 @@ const TextButton: React.FC<Props> = props => {
|
||||
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
|
||||
labelStyle={labelStyle}
|
||||
{...props}
|
||||
style={containerStyle}
|
||||
theme={theme}
|
||||
mode={mode}
|
||||
onPress={props.onPress}
|
||||
|
@@ -8,6 +8,7 @@ const Color = require('color');
|
||||
const baseStyle = {
|
||||
appearance: 'light',
|
||||
fontSize: 16,
|
||||
fontSizeLarger: 18,
|
||||
fontSizeLarge: 20,
|
||||
margin: 15, // No text and no interactive component should be within this margin
|
||||
itemMarginTop: 10,
|
||||
|
@@ -1622,7 +1622,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
|
||||
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
|
||||
|
@@ -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 { stateUtils } from '@joplin/lib/reducer';
|
||||
import { connect } from 'react-redux';
|
||||
import NoteList from '../NoteList';
|
||||
import NoteList from '../../NoteList';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import Tag from '@joplin/lib/models/Tag';
|
||||
import Note, { PreviewsOrder } from '@joplin/lib/models/Note';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { themeStyle } from '../global-style';
|
||||
import { FolderPickerOptions, ScreenHeader } from '../ScreenHeader';
|
||||
import { themeStyle } from '../../global-style';
|
||||
import { FolderPickerOptions, ScreenHeader } from '../../ScreenHeader';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import ActionButton from '../buttons/FloatingActionButton';
|
||||
import { BaseScreenComponent } from '../base-screen';
|
||||
import { AppState } from '../../utils/types';
|
||||
import { BaseScreenComponent } from '../../base-screen';
|
||||
import { AppState } from '../../../utils/types';
|
||||
import { FolderEntity, NoteEntity, TagEntity } from '@joplin/lib/services/database/types';
|
||||
import { itemIsInTrash } from '@joplin/lib/services/trash';
|
||||
import AccessibleView from '../accessibility/AccessibleView';
|
||||
import AccessibleView from '../../accessibility/AccessibleView';
|
||||
import { Dispatch } from 'redux';
|
||||
import { DialogContext, DialogControl } from '../DialogManager';
|
||||
import { DialogContext, DialogControl } from '../../DialogManager';
|
||||
import { useContext } from 'react';
|
||||
import { MenuChoice } from '../DialogManager/types';
|
||||
import { MenuChoice } from '../../DialogManager/types';
|
||||
import NewNoteButton from './NewNoteButton';
|
||||
|
||||
interface Props {
|
||||
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 (addFolderNoteButtons && this.props.folders.length > 0) {
|
||||
const buttons = [];
|
||||
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 <NewNoteButton />;
|
||||
}
|
||||
return null;
|
||||
};
|
@@ -55,7 +55,7 @@ import Revision from '@joplin/lib/models/Revision';
|
||||
import RevisionService from '@joplin/lib/services/RevisionService';
|
||||
import JoplinDatabase from '@joplin/lib/JoplinDatabase';
|
||||
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 ConfigScreen from './components/screens/ConfigScreen/ConfigScreen';
|
||||
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',
|
||||
colorWarn2: '#ffcb81',
|
||||
colorWarn3: '#ff7626',
|
||||
backgroundColorTransparent2: 'rgba(0, 0, 0, 0.1)',
|
||||
|
||||
// Color scheme "3" is used for the config screens for example/
|
||||
// It's dark text over gray background.
|
||||
@@ -71,6 +72,7 @@ const expected = `
|
||||
--joplin-background-color4: #ffffff;
|
||||
--joplin-background-color-hover3: #CBDAF1;
|
||||
--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-code-background-color: rgb(243, 243, 243);
|
||||
--joplin-code-border-color: rgb(220, 220, 220);
|
||||
|
@@ -27,6 +27,7 @@ const theme: Theme = {
|
||||
colorError2: '#ff7070',
|
||||
colorWarn2: '#ffcb81',
|
||||
colorWarn3: '#ff7626',
|
||||
backgroundColorTransparent2: 'rgba(0, 0, 0, 0.1)',
|
||||
|
||||
// Color scheme "3" is used for the config screens for example/
|
||||
// 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
|
||||
// dark blue background.
|
||||
backgroundColor2: string;
|
||||
backgroundColorTransparent2: string; // Used for dimmed region outside modals
|
||||
color2: string;
|
||||
selectedColor2: string;
|
||||
colorError2: string;
|
||||
|
Reference in New Issue
Block a user