Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
Mathys-Gasnier authored Jan 5, 2025
2 parents 438de62 + 6db694d commit f4cb272
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 64 deletions.
35 changes: 32 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"snippets:consolidate": "node ./utils/consolidateSnippets.js"
},
"dependencies": {
"framer-motion": "^11.15.0",
"motion": "^11.15.0",
"prismjs": "^1.29.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
80 changes: 45 additions & 35 deletions src/components/SnippetList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { motion, AnimatePresence } from "framer-motion";
import { motion, AnimatePresence, useReducedMotion } from "motion/react";
import { useState } from "react";

import { useAppContext } from "@contexts/AppContext";
Expand All @@ -13,6 +13,8 @@ const SnippetList = () => {
const { fetchedSnippets } = useSnippets();
const [isModalOpen, setIsModalOpen] = useState(false);

const shouldReduceMotion = useReducedMotion();

if (!fetchedSnippets)
return (
<div>
Expand All @@ -34,41 +36,49 @@ const SnippetList = () => {
<>
<motion.ul role="list" className="snippets">
<AnimatePresence mode="popLayout">
{fetchedSnippets.map((snippet, idx) => (
<motion.li
key={idx}
initial={{ opacity: 0, y: 20 }}
animate={{
opacity: 1,
y: 0,
transition: {
delay: idx * 0.05,
duration: 0.2,
},
}}
exit={{
opacity: 0,
y: -20,
transition: {
delay: (fetchedSnippets.length - 1 - idx) * 0.01,
duration: 0.09,
},
}}
>
<motion.button
className="snippet | flow"
data-flow-space="sm"
onClick={() => handleOpenModal(snippet)}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.98 }}
{fetchedSnippets.map((snippet, idx) => {
const uniqueId = `${language.name}-${snippet.title}`;
return (
<motion.li
key={uniqueId}
layoutId={uniqueId}
initial={{ opacity: 0, y: 20 }}
animate={{
opacity: 1,
y: 0,
transition: {
delay: shouldReduceMotion ? 0 : 0.09 + idx * 0.05,
duration: shouldReduceMotion ? 0 : 0.2,
},
}}
exit={{
opacity: 0,
y: -20,
transition: {
delay: idx * 0.01,
duration: shouldReduceMotion ? 0 : 0.09,
},
}}
transition={{
ease: [0, 0.75, 0.25, 1],
duration: shouldReduceMotion ? 0 : 0.25,
}}
>
<div className="snippet__preview">
<img src={language.icon} alt={language.name} />
</div>
<h3 className="snippet__title">{snippet.title}</h3>
</motion.button>
</motion.li>
))}
<motion.button
className="snippet | flow"
data-flow-space="sm"
onClick={() => handleOpenModal(snippet)}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.98 }}
>
<div className="snippet__preview">
<img src={language.icon} alt={language.name} />
</div>
<h3 className="snippet__title">{snippet.title}</h3>
</motion.button>
</motion.li>
);
})}
</AnimatePresence>
</motion.ul>

Expand Down
15 changes: 9 additions & 6 deletions src/components/SnippetModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { motion } from "framer-motion";
import { motion, useReducedMotion } from "motion/react";
import React from "react";
import ReactDOM from "react-dom";

Expand All @@ -23,6 +23,8 @@ const SnippetModal: React.FC<Props> = ({
}) => {
const modalRoot = document.getElementById("modal-root");

const shouldReduceMotion = useReducedMotion();

useEscapeKey(handleCloseModal);

if (!modalRoot) {
Expand All @@ -41,16 +43,17 @@ const SnippetModal: React.FC<Props> = ({
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
transition={{ duration: shouldReduceMotion ? 0 : 0.2 }}
>
<motion.div
key="modal-content"
className="modal | flow"
data-flow-space="lg"
initial={{ scale: 0.8, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.8, opacity: 0, y: 20 }}
transition={{ type: "spring", duration: 0.5 }}
layoutId={`${language}-${snippet.title}`}
transition={{
ease: [0, 0.75, 0.25, 1],
duration: shouldReduceMotion ? 0 : 0.3,
}}
>
<div className="modal__header">
<h2 className="section-title">{snippet.title}</h2>
Expand Down
50 changes: 31 additions & 19 deletions src/hooks/useKeyboardNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ interface UseKeyboardNavigationProps {
onClose: () => void;
}

const keyboardEventKeys = {
arrowDown: "ArrowDown",
arrowUp: "ArrowUp",
enter: "Enter",
escape: "Escape",
} as const;

type KeyboardEventKeys =
(typeof keyboardEventKeys)[keyof typeof keyboardEventKeys];

export const useKeyboardNavigation = ({
items,
isOpen,
Expand All @@ -20,25 +30,27 @@ export const useKeyboardNavigation = ({
const handleKeyDown = (event: React.KeyboardEvent) => {
if (!isOpen) return;

switch (event.key) {
case "ArrowDown":
event.preventDefault();
setFocusedIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0));
break;
case "ArrowUp":
event.preventDefault();
setFocusedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1));
break;
case "Enter":
event.preventDefault();
if (focusedIndex >= 0) {
onSelect(items[focusedIndex]);
}
break;
case "Escape":
event.preventDefault();
onClose();
break;
const key = event.key as KeyboardEventKeys;

if (Object.values(keyboardEventKeys).includes(key)) {
event.preventDefault();

switch (key) {
case "ArrowDown":
setFocusedIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0));
break;
case "ArrowUp":
setFocusedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1));
break;
case "Enter":
if (focusedIndex >= 0) {
onSelect(items[focusedIndex]);
}
break;
case "Escape":
onClose();
break;
}
}
};

Expand Down

0 comments on commit f4cb272

Please sign in to comment.