@fto-consult/expo-ui
Version:
Bibliothèque de composants UI Expo,react-native
474 lines (457 loc) • 16.5 kB
JavaScript
import React from '$react';
import {
Animated,
StyleSheet,
} from 'react-native';
import View from "$ecomponents/View";
import Tooltip from "$ecomponents/Tooltip";
import theme,{DISABLED_OPACITY,Colors,colorsAlias,cursorNotAllowed,StylePropsTypes} from "$theme";
import Icon from "$ecomponents/Icon";
import {TouchableRipple} from "react-native-paper";
import ActivityIndicator from "$ecomponents/ActivityIndicator";
import Surface from "$ecomponents/Surface";
import Label from "$ecomponents/Label";
import {defaultObj,defaultVal,defaultDecimal,defaultStr} from "$cutils";
import {default as Status} from "./Status";
import PropTypes from "prop-types";
const white = "white";
const ButtonComponent = React.forwardRef((prs,ref) => {
let {
disabled:customDisabled,
compact,
mode = 'text',
dark,
loading,
icon,
color: buttonColor,
children,
text,
label,
isCancelButton,
error,
upperCase = true,
onPress,
iconPosition,
iconBefore,
onLongPress,
style:customStyle,
contentStyle,
contentProps,
labelStyle,
labelProps,
testID,
iconSize,
iconProps,
left,
elevation : customElevation,
right,
accessible,
backgroundColor,
borderColor,
role,
contentContainerProps,
loadingProps,
rounded,
containerProps,
disableRipple,
noPadding,
noMargin,
isAlert,
borderRadius,
forceWhiteColorOnDarkMode,
...rest
} = prs;
isCancelButton = isCancelButton || error && true || false;
children = defaultVal(children,label,text);
testID = defaultStr(testID,'RN_ButtonComponent');
upperCase = upperCase ? true : false;
const isElevationNumber = typeof customElevation =='number' && customElevation ? true : false;
const hasElevation = mode ==='contained' || isElevationNumber ?true : false;
const elev = hasElevation ? (isElevationNumber && customElevation || 5) : 0;
const {roundness } = theme;
const { current: elevation } = React.useRef(
new Animated.Value(elev)
);
React.useEffect(() => {
elevation.setValue(elev);
}, [mode, elevation,customElevation]);
const handlePressIn = () => {
if (hasElevation) {
const { scale } = theme.animation;
Animated.timing(elevation, {
toValue: 8,
duration: 200 * scale,
useNativeDriver: true,
}).start();
}
};
const [state,setState] = React.useState({
isLoading : typeof loading =='boolean'? loading : false,
isDisabled : typeof customDisabled =='boolean'? customDisabled : false,
});
const {isLoading,isDisabled} = state;
const setIsLoading = (loading)=>{
return toggleState({isLoading:loading});
}
const setIsDisabled = (disabled)=>{
return toggleState({isDisabled:disabled});
}
const toggleState = (s)=>{
if(isObj(s) && (typeof s.isLoading =='boolean' || typeof s.isDisabled =='boolean')){
const loading = typeof s.isLoading =='boolean' && s.isLoading !== isLoading ? s.isLoading : typeof s.loading =='boolean' && s.loading !== isLoading ? s.loading : undefined;
const disabled = typeof s.isDisabled =='boolean' && s.isDisabled !== isDisabled ? s.isDisabled : typeof s.disabled =='boolean' && s.disabled !== isDisabled ? s.disabled : undefined;
if(loading !== undefined || disabled !== undefined){
const nState = {};
if(loading !== undefined){
nState.isLoading = loading;
}
if(disabled !== undefined){
nState.isDisabled = disabled;
}
setState({...state,...nState});
return true;
}
return false;
}
}
React.useEffect(()=>{
if(typeof customDisabled !=='boolean' && typeof loading !== 'boolean') return;
if(typeof loading =='boolean' && loading == isLoading && typeof customDisabled =='boolean' && customDisabled === isDisabled){
return;
}
setState({
...state,
isLoading : typeof loading =='boolean' ? loading : state.isLoading,
isDisabled : typeof customDisabled =='boolean' ? customDisabled : state.isDisabled,
});
},[loading,customDisabled]);
const handlePressOut = () => {
if (hasElevation) {
const { scale } = theme.animation;
Animated.timing(elevation, {
toValue: elev,
duration: 150 * scale,
useNativeDriver: true,
}).start();
}
};
contentContainerProps = defaultObj(contentContainerProps);
containerProps = defaultObj(containerProps);
const style = StyleSheet.flatten(customStyle) || {};
labelStyle = StyleSheet.flatten([labelStyle]);
const disabled = isDisabled || isLoading;
let textColor = Colors.isValid(style.color)? style.color : Colors.isValid(buttonColor)?buttonColor : isCancelButton? theme.colors.onError : Colors.isValid(labelStyle.color) ? labelStyle.color : theme.colors.primary,
borderWidth;
const restButtonStyle = {
opacity : disabled ? DISABLED_OPACITY : undefined
};
iconProps = defaultObj(iconProps);
labelProps = defaultObj(labelProps);
contentProps = defaultObj(contentProps);
if (!disabled && hasElevation) {
backgroundColor = Colors.isValid(style.backgroundColor)?style.backgroundColor : Colors.isValid(backgroundColor)? backgroundColor : isCancelButton ? style.backgroundColor = theme.colors.error : undefined;
borderColor = Colors.isValid(style.borderColor)? style.borderColor : isCancelButton ? theme.styles.onError : Colors.isValid(borderColor)? borderColor : undefined;
}
if(theme.isDark() && forceWhiteColorOnDarkMode !== false && !hasElevation){
textColor = white;
}
const rippleColor = Colors.setAlpha(textColor,0.32);
const buttonStyle = {
backgroundColor,
borderColor,
borderWidth,
borderRadius: typeof borderRadius =='number' ? borderRadius : typeof style.borderRadius ==='number'? style.borderRadius : rounded ? roundness : 0,
};
loadingProps = defaultObj(loadingProps);
const elevationRes = disabled || !hasElevation ? 0 : elevation?.__getValue();
contentStyle = Object.assign({},StyleSheet.flatten([hasElevation ? (icon?styles.elevation2icon:(isAlert?styles.elevationAlert:styles.elevationOnly)):null,contentProps.style,contentStyle]));
if(iconPosition == 'top'){
contentStyle.flexDirection = 'column';
} else if(iconPosition =='bottom'){
contentStyle.flexDirection = 'column-reverse';
} else if(iconPosition =='right' || iconBefore === false){
contentStyle.flexDirection = "row-reverse";
}
const iconStyle =
contentStyle.flexDirection?.contains('reverse')
? styles.iconReverse
: styles.icon;
const leftRProps = {color:textColor,iconSize};
const notAllowedStyle = disabled?cursorNotAllowed:undefined
const textStyle = { color: textColor};
iconSize = defaultDecimal(iconSize,iconProps.size,24);
const surfaceStyle = StyleSheet.flatten([
styles.button,
compact && styles.compact,
buttonStyle,
isAlert && styles.surfaceAlert,
style,
restButtonStyle,
]);
if(!hasElevation && !Colors.isValid(surfaceStyle.backgroundColor)){
surfaceStyle.backgroundColor = "transparent";
}
return <Tooltip ref={ref} {...rest}>
{(tProps,tRef)=>{
return <View
testID = {testID+"_Container"} {...containerProps} ref={(el)=>{
if(el){
el.setIsLoading = setIsLoading;
el.setIsDisabled = setIsDisabled;
el.isDisabled = ()=>{
return state.isDisabled || state.isLoading;
};
el.isEnabled = ()=>{
return !state.isDisabled && !state.isLoading;
};
el.disable = ()=>{
return toggleState({isDisabled:true});
};
el.toggleState = toggleState;
el.enable = ()=>{
return toggleState({isDisabled:false});
}
}
React.setRef(tRef,el);
React.setRef(ref,el);
}}
>
<Surface
testID = {testID+"_Surface"}
{...tProps}
elevation = {elevationRes}
style={surfaceStyle}
>
<TouchableRipple
delayPressIn={0}
onPress={isLoading ? undefined : onPress}
onLongPress={onLongPress}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
// @ts-expect-error We keep old a11y props for backwards compat with old RN versions
accessibilityTraits={disabled ? ['button', 'disabled'] : 'button'}
accessibilityComponentType="button"
//role={disabled?role ||undefined}
accessibilityState={{ disabled }}
accessible={accessible}
disabled={disabled}
rippleColor={disableRipple?'transparent':rippleColor}
{...contentContainerProps}
style={[/*touchableStyle*/,contentContainerProps.style,notAllowedStyle]}
testID={testID}
>
<View testID={testID+"_Content"} {...contentProps} style={[styles.content, contentStyle,noPadding && theme.styles.noPadding,noMargin && theme.styles.noMargin]}>
{React.isValidElement(left)? left : typeof left =='function' ?left(leftRProps):null}
{icon && isLoading !== true ? (
<View style={[iconStyle,{color:textColor},iconProps.style]} testID={testID+"_IconContainer"}>
{React.isValidElement(icon)?icon : <Icon
testID={testID+"_Icon"}
source={icon}
{...iconProps}
size={iconSize}
disabled = {disabled}
onPress = {(e)=>{
if(iconProps.onPress && iconProps.onPress(e) === false){
return;
}
if(onPress){
onPress(e);
}
}}
color={textColor}
style = {[
iconProps.style,
styles.icon,
,notAllowedStyle
]}
/>}
</View>
) : null}
{isLoading ? (
<ActivityIndicator
testID={testID+"_ActivityIndicator"}
{...loadingProps}
size={iconSize}
color={textColor}
style={[iconStyle,{marginRight:10},iconProps.style,loadingProps.style]}
/>
) : null}
<Label
userSelect={false}
numberOfLines={1}
testID = {testID+"_Label"}
{...labelProps}
disabled = {disabled}
style={[
styles.label,
compact && styles.compactLabel,
upperCase && styles.upperCaseLabel,
textStyle,
labelProps.style,
labelStyle,
{color:textColor},
]}
>
{children}
</Label>
{React.isValidElement(right)? right : typeof right === 'function'?right(leftRProps):null}
</View>
</TouchableRipple>
</Surface>
</View>
}}
</Tooltip>
});
const styles = StyleSheet.create({
button: {
//minWidth: 64,
borderStyle: 'solid',
},
surfaceAlert : {
marginLeft : 0,
marginRight : 0,
},
elevationOnly : {
padding : 10,
},
elevationAlert : {
paddingHorizontal : 10,
paddingVertical : 7,
marginRight : 0,
},
elevation2icon : {
paddingHorizontal : 5,
paddingVertical : 1,
},
compact: {
//minWidth: 'auto',
},
content: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
icon: {
marginLeft: 0,
marginRight: 0,
paddingVertical : 0,
marginVertical : 0,
},
iconReverse: {
marginRight: 0,
marginLeft: 0,
},
label: {
textAlign: 'center',
fontWeight : '500',
letterSpacing: 1,
marginVertical: 0,
marginHorizontal: 0,
},
compactLabel: {
marginHorizontal: 0,
},
upperCaseLabel: {
textTransform: 'uppercase',
},
});
ButtonComponent.displayName = "ButtonComponent";
export default theme.withStyles(ButtonComponent,{
shouldForwardProp : (prop)=>{
if(prop && colorsAlias.includes(prop)) return true;
return undefined;
}
});
ButtonComponent.propTypes = {
left : PropTypes.oneOfType([
PropTypes.func,
PropTypes.node,
]),
right : PropTypes.oneOfType([
PropTypes.func,
PropTypes.node,
]),
disableRipple : PropTypes.bool,//wheater ripple will be disabled
mode : PropTypes.oneOf([
'text','outlined', 'contained'
]),
/**
* Whether the color is a dark color. A dark button will render light text and vice-versa. Only applicable for `contained` mode.
*/
dark : PropTypes.bool,
/**
* Use a compact look, useful for `text` buttons in a row.
*/
compact:PropTypes.bool,
/**
* Custom text color for flat button, or background color for contained button.
*/
color:PropTypes.string,
/**
* Whether to show a loading indicator.
*/
loading:PropTypes.bool,
/**
* Icon to display for the `Button`.
*/
icon : PropTypes.any,
/**
* Whether the button is disabled. A disabled button is greyed out and `onPress` is not called on touch.
*/
disabled:PropTypes.bool,
/**
* Label text of the button.
*/
children: PropTypes.oneOfType([
PropTypes.node,
PropTypes.string,
]),
/**
* Make the label text upperCased. Note that this won't work if you pass React elements as children.
*/
upperCase:PropTypes.bool,
/**
* Function to execute on press.
*/
onPress:PropTypes.func,
/**
* Function to execute on long press.
*/
onLongPress:PropTypes.func,
/**
* Style of button's inner content.
* Use this prop to apply custom height and width and to set the icon on the right with `flexDirection: 'row-reverse'`.
*/
contentStyle:StylePropsTypes,
style : StylePropsTypes,
/**
* Style for the button text.
*/
labelStyle:StylePropsTypes,
/**
* @optional
*/
theme: PropTypes.object,
/**
* testID to be used on tests.
*/
testID:PropTypes.string,
iconPosition : PropTypes.oneOf([
'top','left','bottom','right'
]),
forceWhiteColorOnDarkMode: PropTypes.bool,//spécifie si la couleur white sera établie comme par défaut eu mode dark
}
export const setIsLoading = (buttonRef,loading)=>{
if(typeof buttonRef =='boolean'){
const t = loading;
loading = buttonRef;
buttonRef = t;
}
if(typeof loading =='boolean' && buttonRef && buttonRef.current && typeof buttonRef.current.setIsLoading =='function'){
buttonRef.current.setIsLoading(loading);
return true;
}
return false;
}
ButtonComponent.Status = Status;
export {Status};