文件
mynode/packages/app-mobile/components/buttons/FloatingActionButton.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

165 行
4.6 KiB
TypeScript

import * as React from 'react';
import { useState, useCallback, useMemo, useRef, RefObject, useLayoutEffect } from 'react';
import { FAB } from 'react-native-paper';
import { _ } from '@joplin/lib/locale';
import { Dispatch } from 'redux';
import { AccessibilityActionEvent, AccessibilityActionInfo, StyleSheet, useWindowDimensions, View } from 'react-native';
import { connect } from 'react-redux';
import { MenuAlignment, MenuType } from '../BottomDrawer';
import { Ionicons as Icon } from '@react-native-vector-icons/ionicons';
import BottomDrawerMenu, { MenuOption } from '../BottomDrawerMenu';
import { AppState } from '../../utils/types';
import { themeStyle } from '../global-style';
type OnButtonPress = ()=> void;
interface ButtonSpec {
icon: string;
label: string;
color?: string;
onPress?: OnButtonPress;
}
interface ActionButtonProps {
themeId: number;
// If not given, an "add" button will be used.
mainButton: ButtonSpec;
dispatch: Dispatch;
menuContent?: MenuOption[];
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) => {
type NameProp = React.ComponentProps<typeof Icon>['name'];
return (props: Omit<React.ComponentProps<typeof Icon>, 'name'>) => <Icon name={iconName as NameProp} {...props} />;
};
const useIcon = (iconName: string) => {
return useMemo(() => {
return getIconRenderFunction(iconName);
}, [iconName]);
};
const useMenuMarginBottom = (buttonContainerRef: RefObject<View>, themeId: number) => {
const { height: windowHeight } = useWindowDimensions();
const [menuMarginBottom, setMenuMarginBottom] = useState(0);
const recomputeMargin = useCallback(() => {
const theme = themeStyle(themeId);
buttonContainerRef.current?.measure((_x, _y, _width, _height, _pageX, pageY) => {
const extraMargin = theme.marginBottom;
setMenuMarginBottom(windowHeight - pageY + extraMargin);
});
}, [windowHeight, buttonContainerRef, themeId]);
useLayoutEffect(() => {
recomputeMargin();
}, [recomputeMargin]);
const recomputeMarginRef = useRef(recomputeMargin);
recomputeMarginRef.current = recomputeMargin;
const onMenuContainerLayout = useCallback(() => {
recomputeMarginRef.current();
}, []);
return { onMenuContainerLayout, menuMarginBottom };
};
interface StylesProps {
menuMarginBottom: number;
}
const useStyles = ({ menuMarginBottom }: StylesProps) => {
return useMemo(() => {
return StyleSheet.create({
menu: {
marginBottom: menuMarginBottom,
// Always float right:
alignSelf: 'flex-end',
},
buttonContainer: {
position: 'absolute',
bottom: 10,
right: 10,
},
button: {
alignSelf: 'flex-end',
},
});
}, [menuMarginBottom]);
};
const FloatingActionButton = (props: ActionButtonProps) => {
const [open, setOpen] = useState(false);
const onMenuToggled = useCallback(() => {
const newOpen = !open;
if (newOpen) {
props.dispatch({
type: 'SIDE_MENU_CLOSE',
});
}
setOpen(newOpen);
}, [setOpen, open, props.dispatch]);
const onDismiss = useCallback(() => {
if (open) onMenuToggled();
}, [open, onMenuToggled]);
const mainButtonRef = useRef<View>(null);
const closedIcon = useIcon(props.mainButton?.icon ?? 'add');
const openIcon = useIcon('close');
const label = props.mainButton?.label ?? _('Add new');
const buttonContainerRef = useRef<View|null>(null);
const { menuMarginBottom, onMenuContainerLayout } = useMenuMarginBottom(buttonContainerRef, props.themeId);
const styles = useStyles({ menuMarginBottom });
const menuButton = <FAB
ref={mainButtonRef}
icon={open ? openIcon : closedIcon}
accessibilityLabel={label}
onPress={props.mainButton?.onPress ?? onMenuToggled}
style={styles.button}
accessibilityActions={props.accessibilityActions}
onAccessibilityAction={props.onAccessibilityAction}
/>;
return <>
<View
style={styles.buttonContainer}
ref={buttonContainerRef}
onLayout={onMenuContainerLayout}
>
{menuButton}
</View>
{ props.menuContent && <BottomDrawerMenu
visible={open}
onDismiss={onDismiss}
alignment={MenuAlignment.Right}
style={styles.menu}
menuType={MenuType.Floating}
themeId={props.themeId}
options={props.menuContent}
autoScrollToEnd
/> }
</>;
};
const ConnectedComponent: React.FC<Omit<ActionButtonProps, 'themeId'|'dispatch'>> = (
connect((state: AppState) => ({ themeId: state.settings.theme }))(FloatingActionButton)
);
export default ConnectedComponent;