文件
mynode/packages/app-mobile/components/BottomDrawerMenu.tsx
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

211 行
5.4 KiB
TypeScript

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<ViewStyle>;
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> = 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(
<View key={`menuOption_divider_${keyCounter++}`} style={styles.divider} />,
);
} 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(
<TouchableRipple
borderless={true}
role='button'
style={styles.menuItem}
onPress={() => {
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}
>
<View style={styles.menuItemContent}>
{option.icon && <Icon name={option.icon} style={textStyles} accessibilityLabel={null} />}
<Text
style={textStyles}
disabled={option.disabled}
>{option.title}</Text>
</View>
</TouchableRipple>,
);
}
}
const titleComponent = props.title ? <>
<Text
role='heading'
variant='titleMedium'
style={styles.title}
>{props.title}</Text>
</> : null;
return <BottomDrawer
visible={props.visible}
onDismiss={props.onDismiss}
onDismissed={onDismissed}
alignment={props.alignment}
autoScrollToEnd={props.autoScrollToEnd}
draggable={true}
menuType={props.menuType}
contentStyle={styles.menu}
style={props.style}
>
<View
style={styles.menuContent}
testID={`menu-content-${props.visible ? 'open' : 'closed'}`}
>
{titleComponent}
{menuOptionComponents}
</View>
</BottomDrawer>;
};
export default BottomDrawerMenu;