Skip to content

Commit

Permalink
feat: add more swipe directions (#544)
Browse files Browse the repository at this point in the history
* Add more swipe directions

* tweaks

* Maintain correct y position

* fix tests
  • Loading branch information
emilkowalski authored Jan 15, 2025
1 parent baa7b47 commit be10e49
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 18 deletions.
83 changes: 74 additions & 9 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { toast, ToastState } from './state';
import './styles.css';
import {
isAction,
SwipeDirection,
type ExternalToast,
type HeightT,
type ToasterProps,
Expand Down Expand Up @@ -45,6 +46,21 @@ function cn(...classes: (string | undefined)[]) {
return classes.filter(Boolean).join(' ');
}

function getDefaultSwipeDirections(position: string): Array<SwipeDirection> {
const [y, x] = position.split('-');
const directions: Array<SwipeDirection> = [];

if (y) {
directions.push(y as SwipeDirection);
}

if (x) {
directions.push(x as SwipeDirection);
}

return directions;
}

const Toast = (props: ToastProps) => {
const {
invert: ToasterInvert,
Expand Down Expand Up @@ -75,6 +91,8 @@ const Toast = (props: ToastProps) => {
closeButtonAriaLabel = 'Close toast',
pauseWhenPageIsHidden,
} = props;
const [swipeDirection, setSwipeDirection] = React.useState<'x' | 'y' | null>(null);
const [swipeOutDirection, setSwipeOutDirection] = React.useState<'left' | 'right' | 'up' | 'down' | null>(null);
const [mounted, setMounted] = React.useState(false);
const [removed, setRemoved] = React.useState(false);
const [swiping, setSwiping] = React.useState(false);
Expand Down Expand Up @@ -278,6 +296,7 @@ const Toast = (props: ToastProps) => {
data-type={toastType}
data-invert={invert}
data-swipe-out={swipeOut}
data-swipe-direction={swipeOutDirection}
data-expanded={Boolean(expanded || (expandByDefault && mounted))}
style={
{
Expand All @@ -304,37 +323,82 @@ const Toast = (props: ToastProps) => {
if (swipeOut || !dismissible) return;

pointerStartRef.current = null;
const swipeAmount = Number(toastRef.current?.style.getPropertyValue('--swipe-amount').replace('px', '') || 0);
const swipeAmountX = Number(
toastRef.current?.style.getPropertyValue('--swipe-amount-x').replace('px', '') || 0,
);
const swipeAmountY = Number(
toastRef.current?.style.getPropertyValue('--swipe-amount-y').replace('px', '') || 0,
);
const timeTaken = new Date().getTime() - dragStartTime.current?.getTime();

const swipeAmount = swipeDirection === 'x' ? swipeAmountX : swipeAmountY;
const velocity = Math.abs(swipeAmount) / timeTaken;

// Remove only if threshold is met
if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) {
setOffsetBeforeRemove(offset.current);
toast.onDismiss?.(toast);

if (swipeDirection === 'x') {
setSwipeOutDirection(swipeAmountX > 0 ? 'right' : 'left');
} else {
setSwipeOutDirection(swipeAmountY > 0 ? 'down' : 'up');
}

deleteToast();
setSwipeOut(true);
setIsSwiped(false);
return;
}

toastRef.current?.style.setProperty('--swipe-amount', '0px');
setSwiping(false);
setSwipeDirection(null);
}}
onPointerMove={(event) => {
if (!pointerStartRef.current || !dismissible) return;

const yPosition = event.clientY - pointerStartRef.current.y;
const isHighlighted = window.getSelection()?.toString().length > 0;
const swipeAmount = y === 'top' ? Math.min(0, yPosition) : Math.max(0, yPosition);
if (isHighlighted) return;

if (Math.abs(swipeAmount) > 0) {
setIsSwiped(true);
const yDelta = event.clientY - pointerStartRef.current.y;
const xDelta = event.clientX - pointerStartRef.current.x;

const swipeDirections = props.swipeDirections ?? getDefaultSwipeDirections(position);

// Determine swipe direction if not already locked
if (!swipeDirection && (Math.abs(xDelta) > 1 || Math.abs(yDelta) > 1)) {
setSwipeDirection(Math.abs(xDelta) > Math.abs(yDelta) ? 'x' : 'y');
}

if (isHighlighted) return;
let swipeAmount = { x: 0, y: 0 };

// Only apply swipe in the locked direction
if (swipeDirection === 'y') {
// Handle vertical swipes
if (swipeDirections.includes('top') || swipeDirections.includes('bottom')) {
if (swipeDirections.includes('top') && yDelta < 0) {
swipeAmount.y = yDelta;
} else if (swipeDirections.includes('bottom') && yDelta > 0) {
swipeAmount.y = yDelta;
}
}
} else if (swipeDirection === 'x') {
// Handle horizontal swipes
if (swipeDirections.includes('left') || swipeDirections.includes('right')) {
if (swipeDirections.includes('left') && xDelta < 0) {
swipeAmount.x = xDelta;
} else if (swipeDirections.includes('right') && xDelta > 0) {
swipeAmount.x = xDelta;
}
}
}

if (Math.abs(swipeAmount.x) > 0 || Math.abs(swipeAmount.y) > 0) {
setIsSwiped(true);
}

toastRef.current?.style.setProperty('--swipe-amount', `${swipeAmount}px`);
// Apply transform using both x and y values
toastRef.current?.style.setProperty('--swipe-amount-x', `${swipeAmount.x}px`);
toastRef.current?.style.setProperty('--swipe-amount-y', `${swipeAmount.y}px`);
}}
>
{closeButton && !toast.jsx ? (
Expand Down Expand Up @@ -783,6 +847,7 @@ const Toaster = forwardRef<HTMLElement, ToasterProps>(function Toaster(props, re
loadingIcon={loadingIcon}
expanded={expanded}
pauseWhenPageIsHidden={pauseWhenPageIsHidden}
swipeDirections={props.swipeDirections}
/>
))}
</ol>
Expand Down
68 changes: 61 additions & 7 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,8 @@
:where([data-sonner-toast][data-swiping='true'])::before {
content: '';
position: absolute;
left: 0;
right: 0;
left: -50%;
right: -50%;
height: 100%;
z-index: -1;
}
Expand Down Expand Up @@ -341,7 +341,7 @@
}

[data-sonner-toast][data-swiping='true'] {
transform: var(--y) translateY(var(--swipe-amount, 0px));
transform: var(--y) translateY(var(--swipe-amount-y, 0px)) translateX(var(--swipe-amount-x, 0px));
transition: none;
}

Expand All @@ -351,17 +351,71 @@

[data-sonner-toast][data-swipe-out='true'][data-y-position='bottom'],
[data-sonner-toast][data-swipe-out='true'][data-y-position='top'] {
animation: swipe-out 200ms ease-out forwards;
animation-duration: 200ms;
animation-timing-function: ease-out;
animation-fill-mode: forwards;
}

[data-sonner-toast][data-swipe-out='true'][data-swipe-direction='left'] {
animation-name: swipe-out-left;
}

[data-sonner-toast][data-swipe-out='true'][data-swipe-direction='right'] {
animation-name: swipe-out-right;
}

[data-sonner-toast][data-swipe-out='true'][data-swipe-direction='up'] {
animation-name: swipe-out-up;
}

[data-sonner-toast][data-swipe-out='true'][data-swipe-direction='down'] {
animation-name: swipe-out-down;
}

@keyframes swipe-out-left {
from {
transform: var(--y) translateX(var(--swipe-amount-x));
opacity: 1;
}

to {
transform: var(--y) translateX(calc(var(--swipe-amount-x) - 100%));
opacity: 0;
}
}

@keyframes swipe-out-right {
from {
transform: var(--y) translateX(var(--swipe-amount-x));
opacity: 1;
}

to {
transform: var(--y) translateX(calc(var(--swipe-amount-x) + 100%));
opacity: 0;
}
}

@keyframes swipe-out-up {
from {
transform: var(--y) translateY(var(--swipe-amount-y));
opacity: 1;
}

to {
transform: var(--y) translateY(calc(var(--swipe-amount-y) - 100%));
opacity: 0;
}
}

@keyframes swipe-out {
@keyframes swipe-out-down {
from {
transform: translateY(calc(var(--lift) * var(--offset) + var(--swipe-amount)));
transform: var(--y) translateY(var(--swipe-amount-y));
opacity: 1;
}

to {
transform: translateY(calc(var(--lift) * var(--offset) + var(--swipe-amount) + var(--lift) * -100%));
transform: var(--y) translateY(calc(var(--swipe-amount-y) + 100%));
opacity: 0;
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export interface ToasterProps {
offset?: Offset;
mobileOffset?: Offset;
dir?: 'rtl' | 'ltr' | 'auto';
swipeDirections?: SwipeDirection[];
/**
* @deprecated Please use the `icons` prop instead:
* ```jsx
Expand All @@ -144,10 +145,13 @@ export interface ToasterProps {
pauseWhenPageIsHidden?: boolean;
}

export type SwipeDirection = 'top' | 'right' | 'bottom' | 'left';

export interface ToastProps {
toast: ToastT;
toasts: ToastT[];
index: number;
swipeDirections?: SwipeDirection[];
expanded: boolean;
invert: boolean;
heights: HeightT[];
Expand Down
10 changes: 8 additions & 2 deletions test/tests/basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,18 @@ test.describe('Basic functionality', () => {
const dragBoundingBox = await toast.boundingBox();

if (!dragBoundingBox) return;
await page.mouse.move(dragBoundingBox.x + dragBoundingBox.width / 2, dragBoundingBox.y);

// Initial touch point
await page.mouse.move(dragBoundingBox.x + dragBoundingBox.width / 2, dragBoundingBox.y);
await page.mouse.down();
await page.mouse.move(0, dragBoundingBox.y + 300);

// Move mouse slightly to determine swipe direction
await page.mouse.move(dragBoundingBox.x + dragBoundingBox.width / 2, dragBoundingBox.y + 10);

// Complete the swipe
await page.mouse.move(0, dragBoundingBox.y + 300);
await page.mouse.up();

await expect(page.getByTestId('dismiss-el')).toHaveCount(1);
});

Expand Down

0 comments on commit be10e49

Please sign in to comment.