sonner-native
Version:
An opinionated toast component for React Native. A port of @emilkowalski's sonner.
398 lines (394 loc) • 14.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ToastIcon = exports.Toast = void 0;
var React = _interopRequireWildcard(require("react"));
var _reactNative = require("react-native");
var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated"));
var _animations = require("./animations.js");
var _constants = require("./constants.js");
var _context = require("./context.js");
var _gestures = require("./gestures.js");
var _icons = require("./icons.js");
var _types = require("./types.js");
var _useAppState = require("./use-app-state.js");
var _useDefaultStyles = require("./use-default-styles.js");
var _jsxRuntime = require("react/jsx-runtime");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
const Toast = exports.Toast = /*#__PURE__*/React.forwardRef(({
id,
title,
jsx,
description,
icon,
duration: durationProps,
variant,
action,
cancel,
close,
onDismiss,
onAutoClose,
dismissible = _constants.toastDefaultValues.dismissible,
closeButton: closeButtonProps,
actionButtonStyle,
actionButtonTextStyle,
cancelButtonStyle,
cancelButtonTextStyle,
style,
styles,
promiseOptions,
position,
unstyled: unstyledProps,
important,
invert: invertProps,
richColors: richColorsProps,
onPress,
backgroundComponent: backgroundComponentProps
}, ref) => {
const {
duration: durationCtx,
addToast,
closeButton: closeButtonCtx,
icons,
pauseWhenPageIsHidden,
invert: invertCtx,
richColors: richColorsCtx,
toastOptions: {
unstyled: unstyledCtx,
toastContainerStyle: toastContainerStyleCtx,
actionButtonStyle: actionButtonStyleCtx,
actionButtonTextStyle: actionButtonTextStyleCtx,
cancelButtonStyle: cancelButtonStyleCtx,
cancelButtonTextStyle: cancelButtonTextStyleCtx,
style: toastStyleCtx,
toastContentStyle: toastContentStyleCtx,
titleStyle: titleStyleCtx,
descriptionStyle: descriptionStyleCtx,
buttonsStyle: buttonsStyleCtx,
closeButtonStyle: closeButtonStyleCtx,
closeButtonIconStyle: closeButtonIconStyleCtx,
backgroundComponent: backgroundComponentCtx,
success: successStyleCtx,
error: errorStyleCtx,
warning: warningStyleCtx,
info: infoStyleCtx,
loading: loadingStyleCtx
}
} = (0, _context.useToastContext)();
const invert = invertProps ?? invertCtx;
const richColors = richColorsProps ?? richColorsCtx;
const unstyled = unstyledProps ?? unstyledCtx;
const duration = durationProps ?? durationCtx;
const closeButton = closeButtonProps ?? closeButtonCtx;
const backgroundComponent = backgroundComponentProps ?? backgroundComponentCtx;
const {
entering,
exiting
} = (0, _animations.useToastLayoutAnimations)(position);
const isDragging = React.useRef(false);
const timer = React.useRef();
const timerStart = React.useRef();
const timeLeftOnceBackgrounded = React.useRef();
const isResolvingPromise = React.useRef(false);
const wiggleSharedValue = (0, _reactNativeReanimated.useSharedValue)(1);
const wiggleAnimationStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => {
return {
transform: [{
scale: wiggleSharedValue.value
}]
};
}, [wiggleSharedValue]);
const startTimer = React.useCallback(() => {
clearTimeout(timer.current);
timer.current = setTimeout(() => {
if (!isDragging.current) {
onAutoClose?.(id);
}
}, _animations.ANIMATION_DURATION + duration);
}, [duration, id, onAutoClose]);
const wiggle = React.useCallback(() => {
'worklet';
wiggleSharedValue.value = (0, _reactNativeReanimated.withRepeat)((0, _reactNativeReanimated.withTiming)(Math.min(wiggleSharedValue.value * 1.035, 1.035), {
duration: 150
}), 4, true);
}, [wiggleSharedValue]);
const wiggleHandler = React.useCallback(() => {
// we can't send Infinity over to the native layer.
if (duration === Infinity) {
return;
}
// reset the duration
timerStart.current = Date.now();
startTimer();
if (wiggleSharedValue.value !== 1) {
// we should animate back to 1 and then wiggle
wiggleSharedValue.value = (0, _reactNativeReanimated.withTiming)(1, {
duration: 150
}, wiggle);
} else {
wiggle();
}
}, [wiggle, wiggleSharedValue, startTimer, duration]);
React.useImperativeHandle(ref, () => ({
wiggle: wiggleHandler
}));
const onBackground = React.useCallback(() => {
if (!pauseWhenPageIsHidden) {
return;
}
if (timer.current) {
timeLeftOnceBackgrounded.current = duration - (Date.now() - timerStart.current);
clearTimeout(timer.current);
timer.current = undefined;
timerStart.current = undefined;
}
}, [duration, pauseWhenPageIsHidden]);
const onForeground = React.useCallback(() => {
if (!pauseWhenPageIsHidden) {
return;
}
if (timeLeftOnceBackgrounded.current && timeLeftOnceBackgrounded.current > 0) {
timer.current = setTimeout(() => {
if (!isDragging.current) {
onAutoClose?.(id);
}
}, Math.max(timeLeftOnceBackgrounded.current, 1000) // minimum 1 second to avoid weird behavior
);
} else {
onAutoClose?.(id);
}
}, [id, onAutoClose, pauseWhenPageIsHidden]);
(0, _useAppState.useAppStateListener)(React.useMemo(() => ({
onBackground,
onForeground
}), [onBackground, onForeground]));
React.useEffect(() => {
const handlePromise = async () => {
if (isResolvingPromise.current) {
return;
}
if (promiseOptions?.promise) {
isResolvingPromise.current = true;
try {
const data = await promiseOptions.promise;
addToast({
title: promiseOptions.success(data) ?? 'Success',
id,
variant: 'success',
promiseOptions: undefined
});
} catch (error) {
addToast({
title: typeof promiseOptions.error === 'function' ? promiseOptions.error(error) : promiseOptions.error ?? 'Error',
id,
variant: 'error',
promiseOptions: undefined
});
} finally {
isResolvingPromise.current = false;
}
return;
}
if (duration === Infinity) {
return;
}
// Start the timer only if it hasn't been started yet
if (!timerStart.current) {
timerStart.current = Date.now();
startTimer();
}
};
handlePromise();
}, [duration, id, onDismiss, promiseOptions, addToast, onAutoClose, startTimer]);
React.useEffect(() => {
// Cleanup function to clear the timer if it's still the same timer
return () => {
clearTimeout(timer.current);
timer.current = undefined;
timerStart.current = undefined;
};
}, [id]);
const defaultStyles = (0, _useDefaultStyles.useDefaultStyles)({
invert,
richColors,
unstyled,
description,
variant
});
const variantStyles = {
success: successStyleCtx,
error: errorStyleCtx,
warning: warningStyleCtx,
info: infoStyleCtx,
loading: loadingStyleCtx
};
const variantStyle = variantStyles[variant];
const renderCloseButton = React.useMemo(() => {
if (!dismissible) {
return null;
}
if (close) {
return close;
}
if (closeButton) {
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
onPress: () => onDismiss?.(id),
hitSlop: 10,
style: [closeButtonStyleCtx, styles?.closeButton],
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_icons.X, {
size: 20,
color: defaultStyles.closeButtonColor,
style: [closeButtonIconStyleCtx, styles?.closeButtonIcon]
})
});
}
return null;
}, [close, closeButton, closeButtonIconStyleCtx, closeButtonStyleCtx, defaultStyles.closeButtonColor, dismissible, id, onDismiss, styles?.closeButton, styles?.closeButtonIcon]);
const toastSwipeHandlerProps = React.useMemo(() => ({
onRemove: () => {
onDismiss?.(id);
},
onBegin: () => {
isDragging.current = true;
},
onFinalize: () => {
isDragging.current = false;
const timeElapsed = Date.now() - timerStart.current;
if (timeElapsed < duration) {
timer.current = setTimeout(() => {
onDismiss?.(id);
}, duration - timeElapsed);
} else {
onDismiss?.(id);
}
},
onPress: () => onPress?.(),
enabled: !promiseOptions && dismissible,
style: [toastContainerStyleCtx, styles?.toastContainer],
unstyled: unstyled,
important: important,
position: position
}), [onDismiss, id, duration, dismissible, promiseOptions, onPress, toastContainerStyleCtx, styles?.toastContainer, unstyled, important, position]);
if (jsx) {
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_gestures.ToastSwipeHandler, {
...toastSwipeHandlerProps,
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
entering: entering,
exiting: exiting,
children: jsx
})
});
}
const backgroundComponentStyle = backgroundComponent ? {
overflow: 'hidden',
backgroundColor: 'transparent'
} : undefined;
const contentContainerStyle = backgroundComponent ? {
position: 'relative',
zIndex: 1
} : undefined;
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_gestures.ToastSwipeHandler, {
...toastSwipeHandlerProps,
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
style: wiggleAnimationStyle,
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
style: [unstyled ? undefined : elevationStyle, defaultStyles.toast, toastStyleCtx, variantStyle, styles?.toast, style, backgroundComponentStyle],
entering: entering,
exiting: exiting,
children: [backgroundComponent, /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
style: [defaultStyles.toastContent, toastContentStyleCtx, styles?.toastContent, contentContainerStyle],
children: [promiseOptions || variant === 'loading' ? 'loading' in icons ? icons.loading : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, {}) : icon ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
children: icon
}) : variant in icons ? icons[variant] : /*#__PURE__*/(0, _jsxRuntime.jsx)(ToastIcon, {
variant: variant,
invert: invert,
richColors: richColors
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
style: {
flex: 1
},
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
style: [defaultStyles.title, titleStyleCtx, styles?.title],
children: title
}), description ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
style: [defaultStyles.description, descriptionStyleCtx, styles?.description],
children: description
}) : null, /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
style: [unstyled || !action && !cancel ? undefined : defaultStyles.buttons, buttonsStyleCtx, styles?.buttons],
children: [(0, _types.isToastAction)(action) ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
onPress: action.onClick,
style: [defaultStyles.actionButton, actionButtonStyleCtx, actionButtonStyle],
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
numberOfLines: 1,
style: [defaultStyles.actionButtonText, actionButtonTextStyleCtx, actionButtonTextStyle],
children: action.label
})
}) : action || undefined, (0, _types.isToastAction)(cancel) ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
onPress: () => {
cancel.onClick();
onDismiss?.(id);
},
style: [defaultStyles.cancelButton, cancelButtonStyleCtx, cancelButtonStyle],
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
numberOfLines: 1,
style: [defaultStyles.cancelButtonText, cancelButtonTextStyleCtx, cancelButtonTextStyle],
children: cancel.label
})
}) : cancel || undefined]
})]
}), renderCloseButton]
})]
})
})
});
});
Toast.displayName = 'Toast';
const ToastIcon = ({
variant,
invert,
richColors
}) => {
const color = (0, _useDefaultStyles.useDefaultStyles)({
variant,
invert,
richColors,
unstyled: false,
description: undefined
}).iconColor;
switch (variant) {
case 'success':
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_icons.CircleCheck, {
size: 20,
color: color
});
case 'error':
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_icons.CircleX, {
size: 20,
color: color
});
case 'warning':
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_icons.TriangleAlert, {
size: 20,
color: color
});
default:
case 'info':
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_icons.Info, {
size: 20,
color: color
});
}
};
exports.ToastIcon = ToastIcon;
const elevationStyle = {
shadowOpacity: 0.0015 * 4 + 0.1,
shadowRadius: 3 * 4,
shadowOffset: {
height: 4,
width: 0
},
elevation: 4
};
//# sourceMappingURL=toast.js.map