void;
};
-const SWIPE_THRESHOLD = 40;
+const SWIPE_THRESHOLD = 60;
const SWIPE_PADDING = 60;
const ANIMATION_DURATION = 200;
const SWIPE_DURATION = ANIMATION_DURATION + 500;
@@ -45,6 +45,52 @@ const targetIsHTMLElement = (
return target instanceof HTMLElement;
};
+function replaceCardWithPlaceholder(
+ card: HTMLElement,
+ placeholder: HTMLElement,
+) {
+ const rect = card.getBoundingClientRect();
+ const parentNode = card.parentNode as HTMLElement;
+ const parentRect = parentNode.getBoundingClientRect();
+
+ // Position the target element correctly
+ card.style.left = `${rect.left - parentRect.left}px`;
+ card.style.top = `${rect.top - parentRect.top}px`;
+ // Set up the placeholder element to be same size as the target element
+ placeholder.style.height = `${rect.height}px`;
+ placeholder.style.display = "block";
+ card.parentNode?.insertBefore(placeholder, card);
+ // Set up the target element
+ card.style.transition = "transform 0.05s";
+ card.style.position = "absolute";
+ // Ensure that the target element has the same size
+ card.style.width = `${rect.width}px`;
+ card.style.height = `${rect.height}px`;
+}
+
+function revertCardFromPlaceholder(
+ card: HTMLElement,
+ placeholder: HTMLElement,
+) {
+ placeholder.style.height = "0px";
+ placeholder.style.display = "none";
+ card.style.position = "static";
+}
+
+function translateCardToThreshold(card: HTMLElement, x: number, y: number) {
+ const absX = Math.abs(x);
+ const transformAbsDistance =
+ Math.floor(absX) + absX > SWIPE_THRESHOLD ? SWIPE_PADDING : 0;
+ const transformDistance =
+ x > 0 ? transformAbsDistance : -transformAbsDistance;
+ card.style.transform = `translateX(${transformDistance}px)`;
+}
+
+function revertCardFromTranslation(card: HTMLElement) {
+ card.style.transition = `transform ${ANIMATION_DURATION}ms cubic-bezier(0.4, 0, 0.2, 1)`;
+ card.style.transform = "translateX(0)";
+}
+
/**
* Flashcard is the component that displays a {@link Card}
*/
@@ -60,7 +106,9 @@ export default function Flashcard({
const { card_contents: initialCardContent } = sessionCard;
const [open, setOpen] = useState(false);
const [editing, setEditing] = useState(false);
- const cardRef = useRef
(null);
+ const cardContainerRef = useRef(null);
+ const placeholderRef = useRef(null);
+
const answerButtonsContainerRef = useRef(null);
const isMobile = useMediaQuery("only screen and (max-width: 640px)");
@@ -91,12 +139,15 @@ export default function Flashcard({
const handlers = useSwipeable({
onSwipeStart: (eventData) => {
const target = eventData.event.currentTarget;
- if (!targetIsHTMLElement(target)) return;
- target.style.transition = "transform 0.05s";
+ const placeholderElement = placeholderRef.current;
+ if (!targetIsHTMLElement(target) || !placeholderElement) return;
+
+ replaceCardWithPlaceholder(target, placeholderElement);
- const id = setTimeout(() => {
- setCurrentlyFocusedRating(undefined);
- }, SWIPE_DURATION - 100);
+ const id = setTimeout(
+ () => setCurrentlyFocusedRating(undefined),
+ SWIPE_DURATION - 100,
+ );
setTimeoutId(id);
},
onSwiping: (eventData) => {
@@ -104,17 +155,11 @@ export default function Flashcard({
if (!targetIsHTMLElement(target)) return;
const { deltaX: x, deltaY: y } = eventData;
- const absX = Math.abs(x);
- const transformAbsDistance =
- Math.floor(absX) + absX > SWIPE_THRESHOLD ? SWIPE_PADDING : 0;
- const transformDistance =
- x > 0 ? transformAbsDistance : -transformAbsDistance;
- target.style.transform = `translateX(${transformDistance}px)`;
+ translateCardToThreshold(target, x, y);
if (x > SWIPE_THRESHOLD) {
setCurrentlyFocusedRating("Easy");
}
-
if (x < -SWIPE_THRESHOLD) {
setCurrentlyFocusedRating("Hard");
}
@@ -126,10 +171,14 @@ export default function Flashcard({
}
const target = eventData.event.currentTarget;
- if (!targetIsHTMLElement(target)) return;
+ const placeholderElement = placeholderRef.current;
+ if (!targetIsHTMLElement(target) || !placeholderElement) return;
+ revertCardFromTranslation(target);
- target.style.transition = `transform ${ANIMATION_DURATION}ms cubic-bezier(0.4, 0, 0.2, 1)`;
- target.style.transform = "translateX(0)";
+ setTimeout(
+ () => revertCardFromPlaceholder(target, placeholderElement),
+ ANIMATION_DURATION,
+ );
currentlyFocusedRating !== "Good" && setCurrentlyFocusedRating(undefined);
},
onSwipedRight: () => {
@@ -153,7 +202,7 @@ export default function Flashcard({
useKeydownRating(onRating, open && !editing, () => setOpen(true));
useClickOutside({
- ref: cardRef,
+ ref: cardContainerRef,
enabled: editing,
callback: () => {
handleEdit();
@@ -170,8 +219,8 @@ export default function Flashcard({
return (
{currentlyFocusedRating === "Good" && (
@@ -214,8 +263,9 @@ export default function Flashcard({
onDelete={onDelete}
onUndo={() => history.undo()}
/>
+
{
if (!open || editing || currentlyFocusedRating || !isMobile) return;
@@ -233,6 +283,10 @@ export default function Flashcard({
editing={editing}
/>
+