import * as React from 'react'; import { useCallback, useMemo, useRef } from 'react'; import { Platform, StyleSheet, View, useWindowDimensions, TextStyle, StyleProp, ViewStyle } from 'react-native'; import { themeStyle } from './global-style'; import BottomDrawer, { MenuAlignment, MenuType } from './BottomDrawer'; import { TouchableRipple, Text } from 'react-native-paper'; import Icon from './Icon'; import focusView from '../utils/focusView'; import debounce from '../utils/debounce'; interface MenuOptionDivider { isDivider: true; } export enum MenuOptionStyle { Normal = 'normal', Destructive = 'destructive', } export interface MenuOptionButton { key?: string; isDivider?: false|undefined; disabled?: boolean; style?: MenuOptionStyle; onPress: ()=> void; icon?: string; title: string; accessibilityHint?: string; autoFocus?: boolean; } export type MenuOption = MenuOptionDivider|MenuOptionButton; interface Props { themeId: number; visible: boolean; style?: StyleProp; menuType?: MenuType; alignment: MenuAlignment; autoScrollToEnd?: boolean; title?: string; onDismiss: ()=> void; options: MenuOption[]; } const useStyles = (themeId: number) => { const windowWidth = useWindowDimensions().width; return useMemo(() => { const theme = themeStyle(themeId); const dividerMargin = theme.marginSmall; return StyleSheet.create({ title: { paddingLeft: theme.marginLeft, paddingRight: theme.marginRight, // Match the space from button text to a neighbouring divider: paddingVertical: theme.marginMedium + dividerMargin, marginBottom: theme.marginSmall, lineHeight: theme.fontSizeLarger, fontSize: theme.fontSizeLarger, borderColor: theme.dividerColor, borderBottomWidth: 1, }, menu: { paddingHorizontal: 0, width: 350, maxWidth: windowWidth, }, menuContent: { flexDirection: 'column', width: '100%' }, menuItem: { alignItems: 'flex-start', padding: theme.marginMedium, paddingLeft: theme.marginLeft, paddingRight: theme.marginRight, }, menuItemContent: { flexDirection: 'row', gap: theme.marginSmall, alignItems: 'center', }, menuItemText: { color: theme.color, fontSize: theme.fontSize, }, menuItemTextDisabled: { opacity: theme.disabledOpacity, }, menuItemTextDestructive: { color: theme.destructiveColor, }, divider: { height: 0, borderBottomWidth: 1, borderColor: theme.dividerColor, marginVertical: dividerMargin, marginHorizontal: theme.margin, }, }); }, [themeId, windowWidth]); }; const useFocusView = (visible: boolean) => { const visibleRef = useRef(visible); visibleRef.current = visible; const autoFocusView = useMemo(() => { // Debounce: Auto-focus seems to need to occur after a delay return debounce((view: View|null) => { if (!view || !visibleRef.current) return; focusView('BottomDrawerMenu', view); }, 100); }, []); return autoFocusView; }; const BottomDrawerMenu: React.FC = props => { const styles = useStyles(props.themeId); const autoFocusView = useFocusView(props.visible); const pendingActionRef = useRef<(()=> void)|null>(null); const onDismissed = useCallback(() => { const action = pendingActionRef.current; pendingActionRef.current = null; action?.(); }, []); const menuOptionComponents: React.ReactNode[] = []; let keyCounter = 0; for (const option of props.options) { if (option.isDivider === true) { menuOptionComponents.push( , ); } else { const key = `menuOption_${option.key ?? keyCounter++}`; const textStyles: TextStyle[] = [styles.menuItemText]; if (option.disabled) { textStyles.push(styles.menuItemTextDisabled); } if (option.style === MenuOptionStyle.Destructive) { textStyles.push(styles.menuItemTextDestructive); } menuOptionComponents.push( { if (Platform.OS === 'ios') { // Wait for the menu to close because iOS cannot show other native views while it's closing. // See https://github.com/laurent22/joplin/issues/16551 pendingActionRef.current = option.onPress; } else { option.onPress(); } props.onDismiss(); }} accessibilityHint={option.accessibilityHint} ref={(option.autoFocus && props.visible) ? autoFocusView : undefined} key={key} disabled={!!option.disabled} > {option.icon && } {option.title} , ); } } const titleComponent = props.title ? <> {props.title} : null; return {titleComponent} {menuOptionComponents} ; }; export default BottomDrawerMenu;