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; accessibilityHint?: string; } // Returns a render function compatible with React Native Paper. const getIconRenderFunction = (iconName: string) => { type NameProp = React.ComponentProps['name']; return (props: Omit, 'name'>) => ; }; const useIcon = (iconName: string) => { return useMemo(() => { return getIconRenderFunction(iconName); }, [iconName]); }; const useMenuMarginBottom = (buttonContainerRef: RefObject, 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(null); const closedIcon = useIcon(props.mainButton?.icon ?? 'add'); const openIcon = useIcon('close'); const label = props.mainButton?.label ?? _('Add new'); const buttonContainerRef = useRef(null); const { menuMarginBottom, onMenuContainerLayout } = useMenuMarginBottom(buttonContainerRef, props.themeId); const styles = useStyles({ menuMarginBottom }); const menuButton = ; return <> {menuButton} { props.menuContent && } ; }; const ConnectedComponent: React.FC> = ( connect((state: AppState) => ({ themeId: state.settings.theme }))(FloatingActionButton) ); export default ConnectedComponent;