190 lines
5.6 KiB
TypeScript
190 lines
5.6 KiB
TypeScript
import React, { useCallback, useMemo, useRef } from 'react';
|
|
import { Platform, Pressable, StyleSheet, ViewStyle } from 'react-native';
|
|
import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu';
|
|
import { ToolTipMenuProps, Action } from './types';
|
|
import { useSettings } from '../hooks/context/useSettings';
|
|
|
|
const ToolTipMenu = (props: ToolTipMenuProps) => {
|
|
const {
|
|
title = '',
|
|
shouldOpenOnLongPress = true,
|
|
disabled = false,
|
|
onPress,
|
|
buttonStyle,
|
|
onPressMenuItem,
|
|
children,
|
|
isButton = false,
|
|
actions,
|
|
accessibilityLabel,
|
|
accessibilityHint,
|
|
accessibilityRole,
|
|
accessibilityState,
|
|
testID,
|
|
onMenuWillShow,
|
|
onMenuWillHide,
|
|
enableAndroidRipple = true,
|
|
} = props;
|
|
|
|
const { language } = useSettings();
|
|
const openedRef = useRef(false);
|
|
const menuRef = useRef<any>(null);
|
|
|
|
const normalizeMenuState = useCallback((menuState?: Action['menuState']): MenuAction['state'] | undefined => {
|
|
if (menuState === undefined) {
|
|
return undefined;
|
|
}
|
|
if (menuState === 'mixed') {
|
|
return 'mixed';
|
|
}
|
|
return menuState ? 'on' : 'off';
|
|
}, []);
|
|
|
|
const buildAttributes = useCallback((action: Action): MenuAction['attributes'] | undefined => {
|
|
const attributes = {
|
|
destructive: Boolean(action.destructive),
|
|
disabled: Boolean(action.disabled),
|
|
hidden: Boolean(action.hidden),
|
|
};
|
|
|
|
if (!attributes.destructive && !attributes.disabled && !attributes.hidden) {
|
|
return undefined;
|
|
}
|
|
|
|
return attributes;
|
|
}, []);
|
|
|
|
const mapMenuItemForMenuView = useCallback(
|
|
(action: Action): MenuAction | null => {
|
|
if (!action?.id) return null;
|
|
|
|
const mappedSubactions = (action.subactions || [])
|
|
.map(subaction => mapMenuItemForMenuView(subaction))
|
|
.filter((item): item is MenuAction => item !== null);
|
|
|
|
const menuItem: MenuAction = {
|
|
id: action.id.toString(),
|
|
title: action.text,
|
|
subtitle: action.subtitle,
|
|
image: action.icon?.iconValue ?? action.image,
|
|
imageColor: action.imageColor,
|
|
attributes: buildAttributes(action),
|
|
displayInline: Platform.OS === 'ios' ? action.displayInline : undefined,
|
|
};
|
|
|
|
const state = normalizeMenuState(action.menuState);
|
|
if (state) {
|
|
menuItem.state = state;
|
|
}
|
|
|
|
if (mappedSubactions.length > 0) {
|
|
menuItem.subactions = mappedSubactions;
|
|
}
|
|
|
|
return menuItem;
|
|
},
|
|
[buildAttributes, normalizeMenuState],
|
|
);
|
|
|
|
const menuViewItemsIOS = useMemo(() => {
|
|
return actions
|
|
.map(actionGroup => {
|
|
if (Array.isArray(actionGroup) && actionGroup.length > 0) {
|
|
const inlineActions = actionGroup.map(mapMenuItemForMenuView).filter((item): item is MenuAction => item !== null);
|
|
if (inlineActions.length === 0) return null;
|
|
const group: MenuAction = {
|
|
id: inlineActions[0].id,
|
|
title: '',
|
|
subactions: inlineActions,
|
|
displayInline: true,
|
|
};
|
|
return group;
|
|
}
|
|
|
|
if (!Array.isArray(actionGroup)) {
|
|
return mapMenuItemForMenuView(actionGroup);
|
|
}
|
|
|
|
return null;
|
|
})
|
|
.filter((item): item is MenuAction => item !== null);
|
|
}, [actions, mapMenuItemForMenuView]);
|
|
|
|
const menuViewItemsAndroid = useMemo(() => {
|
|
const mergedActions = actions.flat().filter(action => action.id);
|
|
return mergedActions.map(mapMenuItemForMenuView).filter((item): item is MenuAction => item !== null);
|
|
}, [actions, mapMenuItemForMenuView]);
|
|
|
|
const handlePressMenuItemForMenuView = ({ nativeEvent }: NativeActionEvent) => {
|
|
if (nativeEvent?.event) {
|
|
onPressMenuItem(nativeEvent.event);
|
|
}
|
|
};
|
|
|
|
const renderMenuView = () => {
|
|
if (disabled || (!isButton && !onPress)) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Pressable
|
|
android_ripple={enableAndroidRipple ? { color: '#d9d9d9', foreground: true } : undefined}
|
|
style={({ pressed }) => {
|
|
const base: ViewStyle[] = [styles.pressable];
|
|
if (buttonStyle) {
|
|
if (Array.isArray(buttonStyle)) {
|
|
base.push(...buttonStyle);
|
|
} else {
|
|
base.push(buttonStyle);
|
|
}
|
|
}
|
|
if (pressed && enableAndroidRipple) base.push(styles.pressed);
|
|
return base;
|
|
}}
|
|
disabled={disabled}
|
|
onPress={onPress}
|
|
onLongPress={shouldOpenOnLongPress ? () => {} : undefined}
|
|
accessibilityLabel={accessibilityLabel}
|
|
accessibilityHint={accessibilityHint}
|
|
accessibilityRole={accessibilityRole}
|
|
accessibilityState={accessibilityState}
|
|
accessibilityLanguage={language}
|
|
testID={testID}
|
|
hitSlop={8}
|
|
>
|
|
<MenuView
|
|
ref={menuRef}
|
|
title={title}
|
|
isAnchoredToRight
|
|
onOpenMenu={() => {
|
|
openedRef.current = true;
|
|
onMenuWillShow?.();
|
|
}}
|
|
onCloseMenu={() => {
|
|
if (!openedRef.current) {
|
|
return;
|
|
}
|
|
openedRef.current = false;
|
|
onMenuWillHide?.();
|
|
}}
|
|
onPressAction={handlePressMenuItemForMenuView}
|
|
actions={Platform.OS === 'ios' ? menuViewItemsIOS : menuViewItemsAndroid}
|
|
shouldOpenOnLongPress={shouldOpenOnLongPress}
|
|
style={buttonStyle ? styles.menuViewFlex : undefined}
|
|
>
|
|
{children}
|
|
</MenuView>
|
|
</Pressable>
|
|
);
|
|
};
|
|
|
|
return actions.length > 0 ? renderMenuView() : null;
|
|
};
|
|
|
|
export default ToolTipMenu;
|
|
|
|
const styles = StyleSheet.create({
|
|
menuViewFlex: { flex: 1 },
|
|
pressable: { alignSelf: 'center' },
|
|
pressed: { opacity: 0.6 },
|
|
});
|