@fto-consult/expo-ui
Version:
Bibliothèque de composants UI Expo,react-native
383 lines (365 loc) • 11.5 kB
JavaScript
import * as React from 'react';
import {
StyleSheet,
Animated,
SafeAreaView,
TouchableWithoutFeedback
} from 'react-native';
import View from "$ecomponents/View";
import {Text,Card,withTheme} from "react-native-paper";
import PropTypes from "prop-types";
import Action from "$ecomponents/Form/Action";
import theme,{ disabledStyle,StylePropTypes,cursorPointer,Colors,cursorNotAllowed } from '$theme';
import {defaultStr} from "$cutils";
import CustomFabItem from "./FabItem";
const FABGroup = ({
actions,
isFormAction,
icon,
open,
onPress,
label,
style,
screenName,
fabStyle,
visible,
onStateChange,
color: colorProp,
testID,
...rest
}) => {
testID = defaultStr(testID,"RN_FabGroupComponent")
const { current: backdrop } = React.useRef(
new Animated.Value(0)
);
const animations = React.useRef(
actions.map(() => new Animated.Value(open ? 1 : 0))
);
const [prevActions, setPrevActions] = React.useState(null);
const { scale } = theme.animation;
React.useEffect(() => {
if (open) {
Animated.parallel([
Animated.timing(backdrop, {
toValue: 1,
duration: 250 * scale,
useNativeDriver: true,
}),
Animated.stagger(
50 * scale,
animations.current
.map((animation) =>
Animated.timing(animation, {
toValue: 1,
duration: 150 * scale,
useNativeDriver: true,
})
)
.reverse()
),
]).start();
} else {
Animated.parallel([
Animated.timing(backdrop, {
toValue: 0,
duration: 200 * scale,
useNativeDriver: true,
}),
...animations.current.map((animation) =>
Animated.timing(animation, {
toValue: 0,
duration: 150 * scale,
useNativeDriver: true,
})
),
]).start();
}
}, [open, actions, backdrop, scale]);
const close = () => onStateChange({ open: false });
const toggle = () => onStateChange({ open: !open });
const colors = theme.colors;
const backdropOpacity = open
? backdrop.interpolate({
inputRange: [0, 0.5, 1],
outputRange: [0, 1, 1],
})
: backdrop;
const opacities = animations.current;
const scales = opacities.map((opacity) =>
open
? opacity.interpolate({
inputRange: [0, 1],
outputRange: [0.8, 1],
})
: 1
);
if (actions.length !== prevActions?.length) {
animations.current = actions.map(
(_, i) => animations.current[i] || new Animated.Value(open ? 1 : 0)
);
setPrevActions(actions);
}
const Item = isFormAction ? Action : FabItem;
const itemComponentProps = isFormAction ? {Component : FabItem} : {};
return (
<View testID={testID+"_Container"} style={[styles.container, style]}>
<TouchableWithoutFeedback testID={testID+"_TouchableOpacity"} onPress={close}>
<Animated.View
testID={testID+"_AnimatedView"}
style={[
styles.backdrop,
{
opacity: backdropOpacity,
backgroundColor: colors.backdrop,
pointerEvents : open ? 'auto' : 'none',
},
]}
/>
</TouchableWithoutFeedback>
<SafeAreaView testID={testID+"_SafeAreaView"} style={styles.safeArea}>
<View testID={testID+"_ItemsContainer"} style={[styles.itemsContainer,{pointerEvents:open ? 'box-none' : 'none'}]}>
{actions.map((it, i) => {
const itemProps = {
...it,
open,
scale:scales[i],
opacity : opacities[i],
close,
}
return (
<View
testID={testID+"_Item_"+i}
key={i} // eslint-disable-line react/no-array-index-key
style={[
styles.item,
{
marginHorizontal:
typeof it.small || it.small ? 24 : 16,
pointerEvents : open ? 'box-none' : 'none',
},
]}
>
<Item
{...itemProps}
{...itemComponentProps}
/>
</View>
)
})}
</View>
<CustomFabItem
size = 'medium'
{...defaultObj(rest)}
onPress={() => {
onPress?.();
toggle();
}}
label = {label}
icon={icon}
color={colorProp}
// @ts-expect-error We keep old a11y props for backwards compat with old RN versions
accessibilityTraits="button"
accessibilityComponentType="button"
//role="button"
accessibilityState={{ expanded: open }}
style={StyleSheet.flatten([styles.fab, fabStyle])}
visible={visible}
testID={testID}
/>
</SafeAreaView>
</View>
);
};
FABGroup.displayName = 'FAB.Group';
export default withTheme(FABGroup);
// @component-docs ignore-next-line
const FABGroupWithTheme = withTheme(FABGroup);
// @component-docs ignore-next-line
export { FABGroupWithTheme as FABGroup };
FABGroup.propTypes = {
isFormAction : PropTypes.bool,
/**
* Action items to display in the form of a speed dial.
* An action item should contain the following properties:
* - `icon`: icon to display (required)
* - `label`: optional label text
* - `color`: custom icon color of the action item
* - `labelTextColor`: custom label text color of the action item
* - `style`: pass additional styles for the fab item, for example, `backgroundColor`
* - `labelStyle`: pass additional styles for the fab item label, for example, `backgroundColor`
* - `onPress`: callback that is called when `FAB` is pressed (required)
*/
actions: PropTypes.arrayOf(PropTypes.shape({
icon: PropTypes.any,
label: PropTypes.string,
color: PropTypes.string,
labelTextColor: PropTypes.string,
style: StylePropTypes,
labelStyle: StylePropTypes,
onPress: PropTypes.func,
testID: PropTypes.string,
})),
/**
* Icon to display for the `FAB`.
* You can toggle it based on whether the speed dial is open to display a different icon.
*/
icon: PropTypes.any,
/**
* Accessibility label for the FAB. This is read by the screen reader when the user taps the FAB.
*/
"aria-label" : PropTypes.string,
/**
* Custom color for the `FAB`.
*/
color: PropTypes.string,
/**
* Function to execute on pressing the `FAB`.
*/
onPress : PropTypes.func,
/**
* Whether the speed dial is open.
*/
open: PropTypes.bool,
/**
* Callback which is called on opening and closing the speed dial.
* The open state needs to be updated when it's called, otherwise the change is dropped.
*/
onStateChange: PropTypes.func,
/**
* Whether `FAB` is currently visible.
*/
visible: PropTypes.bool,
/**
* Style for the group. You can use it to pass additional styles if you need.
* For example, you can set an additional padding if you have a tab bar at the bottom.
*/
style: StylePropTypes,
/**
* Style for the FAB. It allows to pass the FAB button styles, such as backgroundColor.
*/
fabStyle: StylePropTypes,
/**
* @optional
*/
theme: PropTypes.object,
/**
* Pass down testID from Group props to FAB.
*/
testID: PropTypes.string,
}
const _FabItem = function({children,label,disabled:customDisabled,pointerEvents,open,close,testID:customTestID,labelStyle,icon,backgroundColor,scale,opacity,color,style,small,onPress,...rest}){
const disabled = typeof customDisabled =='boolean'? customDisabled : false;
const testID = defaultStr(customTestID,"RN_FabItemComponent")
style = StyleSheet.flatten(style) || {};
color = Colors.isValid(color)? color : style.color;
backgroundColor = Colors.isValid(backgroundColor)? backgroundColor : style.backgroundColor;
const cursorStyle = disabled? cursorNotAllowed : cursorPointer;
const _onPress = ()=>{
if(onPress){
onPress();
}
close();
}
const dStyle = disabled ? disabledStyle : null;
return <>
{label ? (
<View testID = {testID+"_LabelContainer"} style={[dStyle,{pointerEvents}]}>
<Card
testID={testID+"_Card"}
style={
[
styles.label,
labelStyle,
{
transform: [{ scale }],
opacity,
backgroundColor,
},
]
}
onPress={_onPress}
aria-label ={
rest["aria-label"] !== 'undefined'
? rest["aria-label"]
: label
}
accessibilityTraits="button"
accessibilityComponentType="button"
//role="button"
>
<Text testID={testID+"_Label"} style={StyleSheet.flatten([{ color},cursorStyle])}>
{label}
</Text>
</Card>
</View>
) : null}
<CustomFabItem
size={typeof small =='boolean' && small ? "small":"medium"}
icon={icon}
color={color}
disabled = {disabled}
style={
[
style,
dStyle,
cursorStyle,
{
transform: [{ scale}],
opacity,
backgroundColor,
pointerEvents,
},
]
}
onPress={_onPress}
aria-label={
typeof rest["aria-label"] !== 'undefined'
? rest["aria-label"]
: label
}
// @ts-expect-error We keep old a11y props for backwards compat with old RN versions
accessibilityTraits="button"
accessibilityComponentType="button"
//role="button"
testID={testID}
visible={open}
/>
</>
}
export const FabItem = theme.withStyles(_FabItem,{displayName:"FabItemComponent",mode:"contained"});
const styles = StyleSheet.create({
safeArea: {
alignItems: 'flex-end',
pointerEvents : "box-none",
},
itemsContainer : {
marginBottom : 70,
},
container: {
...StyleSheet.absoluteFillObject,
justifyContent: 'flex-end',
pointerEvents : "box-none",
},
fab: {
marginHorizontal: 16,
marginBottom: 16,
marginTop: 0,
},
backdrop: {
...StyleSheet.absoluteFillObject,
},
label: {
borderRadius: 5,
paddingHorizontal: 12,
paddingVertical: 6,
marginVertical: 8,
marginHorizontal: 16,
elevation: 2,
},
item: {
marginBottom: 16,
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
},
});