Skip to content

Commit d95a85b

Browse files
committed
Add flexible toast positioning and alignment
Introduces new config options for vertical and horizontal toast stack alignment, including `horizontalPosition` and a combined `position` object. Updates components and types to support left/center/right alignment, adds runtime controls to the example app, and improves documentation. Also updates `react-native-safe-area-context` to v5.6.0.
1 parent 644bb30 commit d95a85b

File tree

10 files changed

+281
-39
lines changed

10 files changed

+281
-39
lines changed

README.md

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export default function App() {
9191

9292
- 🌈 **Themes & fonts** – override per-variant colours and fonts.
9393
- 🪄 **Enter/exit animations** – configure easing, duration, and offset.
94-
- 🧭 **Placement control**render from the top or bottom and tweak spacing/off-sets.
94+
- 🧭 **Flexible positioning**align the stack top/bottom and left/center/right with safe-area awareness.
9595
- 🔔 **Per-toast behaviour** – set custom duration, icons, and press handlers.
9696
- 🧱 **Render override** – supply `renderToast` to swap the default card while keeping stack logic.
9797
- 🧪 **Strict TypeScript** – rich typings with `ToastMessage`, `ToastConfig`, and context helpers.
@@ -103,6 +103,8 @@ export default function App() {
103103
| `bgColor` | `Record<'success' \| 'error' \| 'warning' \| 'info', string>` | vibrant palette | Background colours per toast type. |
104104
| `timeToDismiss` | `number` | `3000` | Global auto-dismiss duration (ms). Set `0` to keep toasts until manual removal. |
105105
| `placement` | `'top' \| 'bottom'` | `'bottom'` | Screen edge for the stack. |
106+
| `horizontalPosition` | `'left' \| 'center' \| 'right'` | `'center'` | Horizontal alignment for the toast stack. |
107+
| `position` | `{ vertical?: 'top' \| 'bottom'; horizontal?: 'left' \| 'center' \| 'right' }` | `undefined` | Override both axes at once; takes precedence over `placement`/`horizontalPosition`. |
106108
| `spacing` | `number` | `12` | Gap between stacked toasts. |
107109
| `offset` | `number` | `20` | Additional inset from the safe area edge. |
108110
| `font.fontFamilyRegular` | `string` | `undefined` | Custom family for message text. |
@@ -138,7 +140,7 @@ export default function App() {
138140
```
139141

140142
> [!TIP]
141-
> Call `setToastConfig` from anywhere to merge runtime overrides, such as toggling placement when a modal opens.
143+
> Call `setToastConfig` from anywhere to merge runtime overrides, such as moving toasts to the top-right while a modal is open.
142144
143145
## 🧰 Composable API
144146

@@ -152,6 +154,15 @@ setToastConfig(config: Partial<ToastConfig>): void
152154
- `removeToast()` with no arguments pops the most recent toast; supply an `id` to remove a specific one.
153155
- `setToastConfig` merges deeply with the current configuration – safe for incremental updates.
154156

157+
> [!EXAMPLE]
158+
> To snap the stack to the top-right at runtime:
159+
>
160+
> ```tsx
161+
> setToastConfig({ position: { vertical: 'top', horizontal: 'right' } });
162+
> ```
163+
>
164+
> The provider stays mounted and existing toasts animate to their new home.
165+
155166
## 🎨 Designing Beautiful Toasts
156167
157168
Use the render override for complete control:
@@ -186,7 +197,7 @@ yarn example start
186197
```
187198

188199
> [!NOTE]
189-
> The example mirrors the library source via workspaces. Any local edits appear immediately inside Expo.
200+
> The example mirrors the library source via workspaces and now includes live vertical/horizontal toggle controls.
190201
191202
## 🤝 Contributing
192203

@@ -197,16 +208,4 @@ yarn example start
197208
> [!TIP]
198209
> Small fixes are welcome! If you are planning a bigger change, open an issue first so we can discuss the approach.
199210
200-
## 🗣️ Community & Support
201-
202-
- File issues on [GitHub](https://github.com/mCodex/react-native-rooster/issues).
203-
- Follow [@mCodex](https://github.com/mCodex) for release updates.
204-
- Got a success story? Share it through a discussion or tweet with #ReactNativeRooster.
205-
206-
## 📄 License
207-
208-
MIT © [Mateus Andrade](https://github.com/mCodex)
209-
210-
---
211-
212211
Made with ❤️ using [create-react-native-library](https://github.com/callstack/react-native-builder-bob)

example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"react": "19.1.0",
1616
"react-dom": "19.1.0",
1717
"react-native": "0.81.4",
18-
"react-native-safe-area-context": "^4.12.5",
18+
"react-native-safe-area-context": "^5.6.0",
1919
"react-native-web": "~0.21.2"
2020
},
2121
"private": true,

example/src/App.tsx

Lines changed: 137 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback } from 'react';
1+
import React, { useCallback, useState } from 'react';
22
import {
33
Pressable,
44
ScrollView,
@@ -8,16 +8,21 @@ import {
88
View,
99
useWindowDimensions,
1010
} from 'react-native';
11+
import { SafeAreaView } from 'react-native-safe-area-context';
1112
import { ToastProvider, useToast } from 'react-native-rooster';
1213
import type { ToastType } from 'react-native-rooster';
13-
import { SafeAreaView } from 'react-native-safe-area-context';
1414

1515
type ButtonConfig = {
1616
label: string;
1717
type: ToastType;
1818
persistent?: boolean;
1919
};
2020

21+
type ToggleOption<T> = {
22+
label: string;
23+
value: T;
24+
};
25+
2126
const BUTTONS: ButtonConfig[] = [
2227
{ label: 'Success toast', type: 'success' },
2328
{ label: 'Warning toast', type: 'warning' },
@@ -26,10 +31,42 @@ const BUTTONS: ButtonConfig[] = [
2631
{ label: 'Persistent toast', type: 'info', persistent: true },
2732
];
2833

34+
const VERTICAL_OPTIONS: ToggleOption<'top' | 'bottom'>[] = [
35+
{ label: 'Top', value: 'top' },
36+
{ label: 'Bottom', value: 'bottom' },
37+
];
38+
39+
const HORIZONTAL_OPTIONS: ToggleOption<'left' | 'center' | 'right'>[] = [
40+
{ label: 'Left', value: 'left' },
41+
{ label: 'Center', value: 'center' },
42+
{ label: 'Right', value: 'right' },
43+
];
44+
2945
const ToastDemo: React.FC = () => {
30-
const { addToast } = useToast();
46+
const { addToast, setToastConfig } = useToast();
3147
const { width } = useWindowDimensions();
3248
const isWide = width >= 768;
49+
const [verticalPosition, setVerticalPosition] = useState<'top' | 'bottom'>(
50+
'bottom'
51+
);
52+
const [horizontalPosition, setHorizontalPosition] = useState<
53+
'left' | 'center' | 'right'
54+
>('center');
55+
56+
const updatePosition = useCallback(
57+
(
58+
nextVertical: 'top' | 'bottom',
59+
nextHorizontal: 'left' | 'center' | 'right'
60+
) => {
61+
setToastConfig({
62+
position: {
63+
vertical: nextVertical,
64+
horizontal: nextHorizontal,
65+
},
66+
});
67+
},
68+
[setToastConfig]
69+
);
3370

3471
const handlePress = useCallback(
3572
(config: ButtonConfig) => {
@@ -52,6 +89,22 @@ const ToastDemo: React.FC = () => {
5289
[addToast]
5390
);
5491

92+
const handleVerticalChange = useCallback(
93+
(option: ToggleOption<'top' | 'bottom'>) => {
94+
setVerticalPosition(option.value);
95+
updatePosition(option.value, horizontalPosition);
96+
},
97+
[horizontalPosition, updatePosition]
98+
);
99+
100+
const handleHorizontalChange = useCallback(
101+
(option: ToggleOption<'left' | 'center' | 'right'>) => {
102+
setHorizontalPosition(option.value);
103+
updatePosition(verticalPosition, option.value);
104+
},
105+
[updatePosition, verticalPosition]
106+
);
107+
55108
return (
56109
<SafeAreaView style={styles.screen}>
57110
<StatusBar barStyle="light-content" />
@@ -76,6 +129,44 @@ const ToastDemo: React.FC = () => {
76129
Trigger different variants to see Rooster in action.
77130
</Text>
78131

132+
<View style={styles.settingsGroup}>
133+
<Text style={styles.settingsLabel}>Vertical placement</Text>
134+
<View style={styles.toggleRow}>
135+
{VERTICAL_OPTIONS.map((option) => (
136+
<Pressable
137+
key={option.value}
138+
accessibilityRole="button"
139+
style={({ pressed }) => [
140+
styles.toggle,
141+
option.value === verticalPosition && styles.toggleActive,
142+
pressed && styles.togglePressed,
143+
]}
144+
onPress={() => handleVerticalChange(option)}
145+
>
146+
<Text style={styles.toggleText}>{option.label}</Text>
147+
</Pressable>
148+
))}
149+
</View>
150+
151+
<Text style={styles.settingsLabel}>Horizontal alignment</Text>
152+
<View style={styles.toggleRow}>
153+
{HORIZONTAL_OPTIONS.map((option) => (
154+
<Pressable
155+
key={option.value}
156+
accessibilityRole="button"
157+
style={({ pressed }) => [
158+
styles.toggle,
159+
option.value === horizontalPosition && styles.toggleActive,
160+
pressed && styles.togglePressed,
161+
]}
162+
onPress={() => handleHorizontalChange(option)}
163+
>
164+
<Text style={styles.toggleText}>{option.label}</Text>
165+
</Pressable>
166+
))}
167+
</View>
168+
</View>
169+
79170
<View style={[styles.buttonGroup, isWide && styles.buttonGroupWide]}>
80171
{BUTTONS.map((button) => (
81172
<Pressable
@@ -108,14 +199,6 @@ const ToastDemo: React.FC = () => {
108199
);
109200
};
110201

111-
export default function App() {
112-
return (
113-
<ToastProvider>
114-
<ToastDemo />
115-
</ToastProvider>
116-
);
117-
}
118-
119202
const styles = StyleSheet.create({
120203
screen: {
121204
flex: 1,
@@ -186,6 +269,41 @@ const styles = StyleSheet.create({
186269
fontSize: 15,
187270
textAlign: 'center',
188271
},
272+
settingsGroup: {
273+
gap: 12,
274+
},
275+
settingsLabel: {
276+
color: '#d1d5db',
277+
fontSize: 14,
278+
fontWeight: '600',
279+
},
280+
toggleRow: {
281+
flexDirection: 'row',
282+
gap: 8,
283+
},
284+
toggle: {
285+
flex: 1,
286+
borderRadius: 999,
287+
borderWidth: StyleSheet.hairlineWidth,
288+
borderColor: 'rgba(255,255,255,0.08)',
289+
paddingVertical: 10,
290+
alignItems: 'center',
291+
backgroundColor: '#202020',
292+
},
293+
toggleActive: {
294+
backgroundColor: '#4338ca',
295+
borderColor: '#6366f1',
296+
},
297+
togglePressed: {
298+
opacity: 0.85,
299+
},
300+
toggleText: {
301+
color: '#f9fafb',
302+
fontSize: 13,
303+
fontWeight: '600',
304+
textTransform: 'uppercase',
305+
letterSpacing: 0.5,
306+
},
189307
buttonGroup: {
190308
gap: 12,
191309
},
@@ -240,3 +358,11 @@ const styles = StyleSheet.create({
240358
textAlign: 'center',
241359
},
242360
});
361+
362+
const App: React.FC = () => (
363+
<ToastProvider>
364+
<ToastDemo />
365+
</ToastProvider>
366+
);
367+
368+
export default App;

src/components/Toast.tsx

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
22
import { Animated, Pressable, StyleSheet, Text, View } from 'react-native';
33
import type { LayoutChangeEvent } from 'react-native';
44

5-
import type { ToastConfig, ToastMessage, ToastPlacement } from '../types';
5+
import type {
6+
ToastConfig,
7+
ToastHorizontalPosition,
8+
ToastMessage,
9+
ToastPlacement,
10+
} from '../types';
611

712
interface ToastProps {
813
message: ToastMessage;
914
config: ToastConfig;
1015
placement: ToastPlacement;
16+
horizontalPosition: ToastHorizontalPosition;
1117
offset: number;
1218
index: number;
1319
onRemove: (id: string) => void;
@@ -20,6 +26,7 @@ const Toast: React.FC<ToastProps> = ({
2026
message,
2127
config,
2228
placement,
29+
horizontalPosition,
2330
offset,
2431
index,
2532
onRemove,
@@ -143,16 +150,37 @@ const Toast: React.FC<ToastProps> = ({
143150
opacity,
144151
]);
145152

153+
const horizontalStyle = useMemo(() => {
154+
switch (horizontalPosition) {
155+
case 'left':
156+
return styles.horizontalLeft;
157+
case 'right':
158+
return styles.horizontalRight;
159+
default:
160+
return styles.horizontalCenter;
161+
}
162+
}, [horizontalPosition]);
163+
146164
const animatedStyle = useMemo(
147165
() => [
148166
styles.toast,
167+
horizontalStyle,
149168
{ backgroundColor },
150169
placement === 'top' ? { top: offset } : { bottom: offset },
151170
{ opacity, transform: [{ translateY: translate }] },
152171
toastStyle,
153172
{ zIndex: 1000 - index },
154173
],
155-
[backgroundColor, index, offset, opacity, placement, toastStyle, translate]
174+
[
175+
backgroundColor,
176+
horizontalStyle,
177+
index,
178+
offset,
179+
opacity,
180+
placement,
181+
toastStyle,
182+
translate,
183+
]
156184
);
157185

158186
return (
@@ -184,8 +212,7 @@ export default React.memo(Toast);
184212
const styles = StyleSheet.create({
185213
toast: {
186214
position: 'absolute',
187-
left: 16,
188-
right: 16,
215+
maxWidth: 420,
189216
paddingVertical: 14,
190217
paddingHorizontal: 16,
191218
borderRadius: 12,
@@ -195,6 +222,21 @@ const styles = StyleSheet.create({
195222
shadowRadius: 16,
196223
elevation: 8,
197224
},
225+
horizontalCenter: {
226+
left: 16,
227+
right: 16,
228+
alignSelf: 'center',
229+
},
230+
horizontalLeft: {
231+
left: 16,
232+
right: undefined,
233+
alignSelf: 'flex-start',
234+
},
235+
horizontalRight: {
236+
left: undefined,
237+
right: 16,
238+
alignSelf: 'flex-end',
239+
},
198240
pressable: {
199241
flexDirection: 'row',
200242
alignItems: 'flex-start',

0 commit comments

Comments
 (0)