Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issues with swiping and overflow #71

Merged
merged 2 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/flashcard/main/answer-buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export default function AnswerButtons({
return (
<div
className={cn(
"grid h-full grid-cols-2 gap-x-2 gap-y-2 sm:h-12 sm:w-96 md:grid-cols-4",
"grid h-full grid-cols-2 gap-x-2 gap-y-2 shadow-sm sm:h-12 sm:w-96 md:grid-cols-4",
)}
>
{open ? (
Expand All @@ -85,7 +85,7 @@ export default function AnswerButtons({
) : (
<Button
variant="secondary"
className="col-span-2 h-32 text-2xl sm:h-full sm:text-lg md:col-span-4"
className="col-span-2 h-32 text-2xl transition duration-300 hover:scale-105 sm:h-full sm:text-lg md:col-span-4"
onClick={() => setOpen(true)}
>
Reveal
Expand Down
4 changes: 2 additions & 2 deletions src/components/flashcard/main/editable-flashcard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function EditableFlashcard({ form, setOpen, open, editing }: Props) {
<Form {...form}>
<div
className={cn(
"col-span-8 flex h-full min-h-80 w-full items-center justify-center overflow-y-auto border border-input sm:col-span-4 sm:min-h-96",
"col-span-8 flex h-full min-h-80 w-full items-center justify-center overflow-y-auto rounded-md border border-input sm:col-span-4 sm:min-h-96",
editing ? "bg-muted" : "",
)}
onClick={onContainerFocus}
Expand All @@ -35,7 +35,7 @@ export function EditableFlashcard({ form, setOpen, open, editing }: Props) {

<div
className={cn(
"relative col-span-8 flex h-full min-h-80 w-full items-center justify-center border border-input sm:col-span-4 sm:min-h-96",
"relative col-span-8 flex h-full min-h-80 w-full items-center justify-center rounded-md border border-input sm:col-span-4 sm:min-h-96",
editing ? "bg-muted" : "",
)}
onClick={onContainerFocus}
Expand Down
96 changes: 75 additions & 21 deletions src/components/flashcard/main/flashcard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type Props = {
onDelete: () => void;
};

const SWIPE_THRESHOLD = 120;
const SWIPE_THRESHOLD = 60;
const SWIPE_PADDING = 60;
const ANIMATION_DURATION = 200;
const SWIPE_DURATION = ANIMATION_DURATION + 500;
Expand All @@ -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}
*/
Expand All @@ -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<HTMLDivElement>(null);
const cardContainerRef = useRef<HTMLDivElement>(null);
const placeholderRef = useRef<HTMLDivElement>(null);

const answerButtonsContainerRef = useRef<HTMLDivElement>(null);
const isMobile = useMediaQuery("only screen and (max-width: 640px)");

Expand Down Expand Up @@ -91,30 +139,27 @@ 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) => {
const target = eventData.event.currentTarget;
if (!targetIsHTMLElement(target)) return;

const { deltaX: x, deltaY: y } = eventData;
const absX = Math.abs(x);
const transformAbsDistance =
Math.floor(absX / 4) + 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");
}
Expand All @@ -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: () => {
Expand All @@ -153,7 +202,7 @@ export default function Flashcard({

useKeydownRating(onRating, open && !editing, () => setOpen(true));
useClickOutside({
ref: cardRef,
ref: cardContainerRef,
enabled: editing,
callback: () => {
handleEdit();
Expand All @@ -170,8 +219,8 @@ export default function Flashcard({

return (
<div
className="relative col-span-12 flex flex-col gap-x-4 gap-y-2"
ref={cardRef}
className="relative col-span-12 flex flex-col gap-x-4 gap-y-2 overflow-hidden"
ref={cardContainerRef}
>
{currentlyFocusedRating === "Good" && (
<ThumbsUp className="absolute bottom-0 left-0 right-0 top-0 z-20 mx-auto my-auto h-12 w-12 animate-tada text-primary" />
Expand Down Expand Up @@ -214,8 +263,9 @@ export default function Flashcard({
onDelete={onDelete}
onUndo={() => history.undo()}
/>

<div
className="col-span-8 grid grid-cols-8 place-items-end gap-x-4 gap-y-4 bg-background"
className="col-span-8 grid grid-cols-8 place-items-end gap-x-4 gap-y-2 bg-background"
{...handlers}
onDoubleClick={() => {
if (!open || editing || currentlyFocusedRating || !isMobile) return;
Expand All @@ -233,6 +283,10 @@ export default function Flashcard({
editing={editing}
/>
</div>
<div
className="-z-40 col-span-12 hidden bg-muted opacity-60 shadow-inner"
ref={placeholderRef}
></div>

<div
className="z-20 mb-6 w-full sm:static sm:mx-auto sm:w-max"
Expand Down
2 changes: 1 addition & 1 deletion src/components/flashcard/main/swipe-action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function SwipeActionText({ direction, active, children }: Props) {
>
<div
className={cn(
"rounded-md bg-background px-8 py-4 text-muted transition duration-300",
"rounded-md px-8 py-4 text-muted transition duration-300",
active && "animate-wiggle text-primary",
)}
>
Expand Down