Skip to content

Commit f4cb272

Browse files
Merge branch 'main' into main
2 parents 438de62 + 6db694d commit f4cb272

File tree

5 files changed

+118
-64
lines changed

5 files changed

+118
-64
lines changed

package-lock.json

Lines changed: 32 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"snippets:consolidate": "node ./utils/consolidateSnippets.js"
1717
},
1818
"dependencies": {
19-
"framer-motion": "^11.15.0",
19+
"motion": "^11.15.0",
2020
"prismjs": "^1.29.0",
2121
"react": "^18.3.1",
2222
"react-dom": "^18.3.1",

src/components/SnippetList.tsx

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { motion, AnimatePresence } from "framer-motion";
1+
import { motion, AnimatePresence, useReducedMotion } from "motion/react";
22
import { useState } from "react";
33

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

16+
const shouldReduceMotion = useReducedMotion();
17+
1618
if (!fetchedSnippets)
1719
return (
1820
<div>
@@ -34,41 +36,49 @@ const SnippetList = () => {
3436
<>
3537
<motion.ul role="list" className="snippets">
3638
<AnimatePresence mode="popLayout">
37-
{fetchedSnippets.map((snippet, idx) => (
38-
<motion.li
39-
key={idx}
40-
initial={{ opacity: 0, y: 20 }}
41-
animate={{
42-
opacity: 1,
43-
y: 0,
44-
transition: {
45-
delay: idx * 0.05,
46-
duration: 0.2,
47-
},
48-
}}
49-
exit={{
50-
opacity: 0,
51-
y: -20,
52-
transition: {
53-
delay: (fetchedSnippets.length - 1 - idx) * 0.01,
54-
duration: 0.09,
55-
},
56-
}}
57-
>
58-
<motion.button
59-
className="snippet | flow"
60-
data-flow-space="sm"
61-
onClick={() => handleOpenModal(snippet)}
62-
whileHover={{ scale: 1.01 }}
63-
whileTap={{ scale: 0.98 }}
39+
{fetchedSnippets.map((snippet, idx) => {
40+
const uniqueId = `${language.name}-${snippet.title}`;
41+
return (
42+
<motion.li
43+
key={uniqueId}
44+
layoutId={uniqueId}
45+
initial={{ opacity: 0, y: 20 }}
46+
animate={{
47+
opacity: 1,
48+
y: 0,
49+
transition: {
50+
delay: shouldReduceMotion ? 0 : 0.09 + idx * 0.05,
51+
duration: shouldReduceMotion ? 0 : 0.2,
52+
},
53+
}}
54+
exit={{
55+
opacity: 0,
56+
y: -20,
57+
transition: {
58+
delay: idx * 0.01,
59+
duration: shouldReduceMotion ? 0 : 0.09,
60+
},
61+
}}
62+
transition={{
63+
ease: [0, 0.75, 0.25, 1],
64+
duration: shouldReduceMotion ? 0 : 0.25,
65+
}}
6466
>
65-
<div className="snippet__preview">
66-
<img src={language.icon} alt={language.name} />
67-
</div>
68-
<h3 className="snippet__title">{snippet.title}</h3>
69-
</motion.button>
70-
</motion.li>
71-
))}
67+
<motion.button
68+
className="snippet | flow"
69+
data-flow-space="sm"
70+
onClick={() => handleOpenModal(snippet)}
71+
whileHover={{ scale: 1.01 }}
72+
whileTap={{ scale: 0.98 }}
73+
>
74+
<div className="snippet__preview">
75+
<img src={language.icon} alt={language.name} />
76+
</div>
77+
<h3 className="snippet__title">{snippet.title}</h3>
78+
</motion.button>
79+
</motion.li>
80+
);
81+
})}
7282
</AnimatePresence>
7383
</motion.ul>
7484

src/components/SnippetModal.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { motion } from "framer-motion";
1+
import { motion, useReducedMotion } from "motion/react";
22
import React from "react";
33
import ReactDOM from "react-dom";
44

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

26+
const shouldReduceMotion = useReducedMotion();
27+
2628
useEscapeKey(handleCloseModal);
2729

2830
if (!modalRoot) {
@@ -41,16 +43,17 @@ const SnippetModal: React.FC<Props> = ({
4143
initial={{ opacity: 0 }}
4244
animate={{ opacity: 1 }}
4345
exit={{ opacity: 0 }}
44-
transition={{ duration: 0.2 }}
46+
transition={{ duration: shouldReduceMotion ? 0 : 0.2 }}
4547
>
4648
<motion.div
4749
key="modal-content"
4850
className="modal | flow"
4951
data-flow-space="lg"
50-
initial={{ scale: 0.8, opacity: 0, y: 20 }}
51-
animate={{ scale: 1, opacity: 1, y: 0 }}
52-
exit={{ scale: 0.8, opacity: 0, y: 20 }}
53-
transition={{ type: "spring", duration: 0.5 }}
52+
layoutId={`${language}-${snippet.title}`}
53+
transition={{
54+
ease: [0, 0.75, 0.25, 1],
55+
duration: shouldReduceMotion ? 0 : 0.3,
56+
}}
5457
>
5558
<div className="modal__header">
5659
<h2 className="section-title">{snippet.title}</h2>

src/hooks/useKeyboardNavigation.ts

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ interface UseKeyboardNavigationProps {
99
onClose: () => void;
1010
}
1111

12+
const keyboardEventKeys = {
13+
arrowDown: "ArrowDown",
14+
arrowUp: "ArrowUp",
15+
enter: "Enter",
16+
escape: "Escape",
17+
} as const;
18+
19+
type KeyboardEventKeys =
20+
(typeof keyboardEventKeys)[keyof typeof keyboardEventKeys];
21+
1222
export const useKeyboardNavigation = ({
1323
items,
1424
isOpen,
@@ -20,25 +30,27 @@ export const useKeyboardNavigation = ({
2030
const handleKeyDown = (event: React.KeyboardEvent) => {
2131
if (!isOpen) return;
2232

23-
switch (event.key) {
24-
case "ArrowDown":
25-
event.preventDefault();
26-
setFocusedIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0));
27-
break;
28-
case "ArrowUp":
29-
event.preventDefault();
30-
setFocusedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1));
31-
break;
32-
case "Enter":
33-
event.preventDefault();
34-
if (focusedIndex >= 0) {
35-
onSelect(items[focusedIndex]);
36-
}
37-
break;
38-
case "Escape":
39-
event.preventDefault();
40-
onClose();
41-
break;
33+
const key = event.key as KeyboardEventKeys;
34+
35+
if (Object.values(keyboardEventKeys).includes(key)) {
36+
event.preventDefault();
37+
38+
switch (key) {
39+
case "ArrowDown":
40+
setFocusedIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0));
41+
break;
42+
case "ArrowUp":
43+
setFocusedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1));
44+
break;
45+
case "Enter":
46+
if (focusedIndex >= 0) {
47+
onSelect(items[focusedIndex]);
48+
}
49+
break;
50+
case "Escape":
51+
onClose();
52+
break;
53+
}
4254
}
4355
};
4456

0 commit comments

Comments
 (0)