一些检查失败了
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
165 行
4.6 KiB
TypeScript
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;
|