Skip to content

Commit 4614f10

Browse files
author
Alexandre Machado
committed
Refactor code structure for improved readability and maintainability
1 parent 216ccbf commit 4614f10

File tree

7 files changed

+110
-8
lines changed

7 files changed

+110
-8
lines changed

app/components/cart/CartDrawer.tsx

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,69 @@ export function CartDrawer({
2222
onRemoveItem,
2323
}: CartDrawerProps) {
2424
const drawerRef = useRef<HTMLDivElement>(null);
25+
const previousActiveElement = useRef<HTMLElement | null>(null);
2526

26-
// Close on escape key
27+
// Close on escape key and focus trap
2728
useEffect(() => {
2829
const handleEscape = (e: KeyboardEvent) => {
2930
if (e.key === 'Escape') onClose();
3031
};
32+
3133
if (isOpen) {
34+
// Store the currently focused element
35+
previousActiveElement.current = document.activeElement as HTMLElement;
36+
37+
// Focus the drawer when it opens
38+
setTimeout(() => {
39+
drawerRef.current?.focus();
40+
}, 0);
41+
3242
document.addEventListener('keydown', handleEscape);
3343
document.body.style.overflow = 'hidden';
3444
}
45+
3546
return () => {
3647
document.removeEventListener('keydown', handleEscape);
3748
document.body.style.overflow = '';
49+
50+
// Restore focus to previous element when drawer closes
51+
if (previousActiveElement.current) {
52+
previousActiveElement.current.focus();
53+
}
3854
};
3955
}, [isOpen, onClose]);
4056

41-
// Focus trap
57+
// Focus trap for Tab key
4258
useEffect(() => {
43-
if (isOpen && drawerRef.current) {
44-
drawerRef.current.focus();
45-
}
59+
if (!isOpen) return;
60+
61+
const handleKeyDown = (e: KeyboardEvent) => {
62+
if (e.key !== 'Tab') return;
63+
if (!drawerRef.current) return;
64+
65+
const focusableElements = drawerRef.current.querySelectorAll<HTMLElement>(
66+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
67+
);
68+
69+
const firstElement = focusableElements[0];
70+
const lastElement = focusableElements[focusableElements.length - 1];
71+
72+
if (!firstElement || !lastElement) return;
73+
74+
// Shift + Tab on first element - trap to last
75+
if (e.shiftKey && document.activeElement === firstElement) {
76+
e.preventDefault();
77+
lastElement.focus();
78+
}
79+
// Tab on last element - trap to first
80+
else if (!e.shiftKey && document.activeElement === lastElement) {
81+
e.preventDefault();
82+
firstElement.focus();
83+
}
84+
};
85+
86+
document.addEventListener('keydown', handleKeyDown);
87+
return () => document.removeEventListener('keydown', handleKeyDown);
4688
}, [isOpen]);
4789

4890
const isEmpty = items.length === 0;

app/components/cart/CartProvider.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@ export function CartProvider({ children }: { children: ReactNode }) {
8484

8585
useEffect(() => {
8686
if (!isReady || typeof window === 'undefined') return;
87-
window.localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(items));
87+
try {
88+
window.localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(items));
89+
} catch (error) {
90+
console.warn('Failed to persist cart to localStorage:', error);
91+
}
8892
}, [items, isReady]);
8993

9094
const value = useMemo<CartContextValue>(() => {

app/lib/utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
import { type ClassValue, clsx } from 'clsx';
2+
import DOMPurify from 'dompurify';
23

34
// Classname utility
45
export function cn(...inputs: ClassValue[]) {
56
return clsx(inputs);
67
}
78

9+
// Sanitize HTML to prevent XSS attacks
10+
export function sanitizeHtml(html: string): string {
11+
if (typeof window === 'undefined') {
12+
return '';
13+
}
14+
15+
return DOMPurify.sanitize(html, {
16+
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'b', 'i', 'u', 'span', 'a', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
17+
ALLOWED_ATTR: ['href', 'target', 'rel', 'class'],
18+
});
19+
}
20+
821
// Currency formatter
922
export function formatCurrency(amount: string, currencyCode: string = 'USD'): string {
1023
return new Intl.NumberFormat('en-US', {

app/routes/products.$handle.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useLoaderData } from '@remix-run/react';
55
import { useMemo, useState } from 'react';
66
import { storefront } from '../lib/storefront';
77
import { PRODUCT_BY_HANDLE_QUERY, RELATED_PRODUCTS_QUERY } from '../lib/shopify/queries';
8-
import { formatCurrency } from '../lib/utils';
8+
import { formatCurrency, sanitizeHtml } from '../lib/utils';
99
import { Badge } from '../components/ui/Badge';
1010
import { Button } from '../components/ui/Button';
1111
import { AnimateIn } from '../components/motion/AnimateIn';
@@ -203,7 +203,7 @@ export default function ProductPage() {
203203
{/* Description */}
204204
<div className="text-text-secondary leading-relaxed">
205205
{product.descriptionHtml ? (
206-
<div dangerouslySetInnerHTML={{ __html: product.descriptionHtml }} />
206+
<div dangerouslySetInnerHTML={{ __html: sanitizeHtml(product.descriptionHtml) }} />
207207
) : (
208208
<p>{product.description}</p>
209209
)}

package-lock.json

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

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
"@remix-run/node": "^2.15.0",
1616
"@remix-run/react": "^2.15.0",
1717
"@remix-run/serve": "^2.15.0",
18+
"@types/dompurify": "^3.0.5",
1819
"clsx": "^2.1.0",
20+
"dompurify": "^3.3.3",
1921
"isbot": "^4.1.0",
2022
"react": "^18.2.0",
2123
"react-dom": "^18.2.0"

tsconfig.tsbuildinfo

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)