Skip to content

Commit

Permalink
refactor(playlist): add scroll throttle to prevent stutter
Browse files Browse the repository at this point in the history
  • Loading branch information
royschut committed Jan 29, 2024
1 parent 43d0a8b commit af567e8
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 7 deletions.
24 changes: 24 additions & 0 deletions packages/common/src/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@ export function debounce<T extends (...args: any[]) => void>(callback: T, wait =
timeout = setTimeout(() => callback(...args), wait);
};
}
export function throttle<T extends (...args: any[]) => unknown>(func: T, limit: number): (...args: Parameters<T>) => void {
let lastFunc: NodeJS.Timeout | undefined;
let lastRan: number | undefined;

return function (this: ThisParameterType<T>, ...args: Parameters<T>): void {
const timeSinceLastRan = lastRan ? Date.now() - lastRan : limit;

if (timeSinceLastRan >= limit) {
func.apply(this, args);
lastRan = Date.now();
} else if (!lastFunc) {
lastFunc = setTimeout(() => {
if (lastRan) {
const timeSinceLastRan = Date.now() - lastRan;
if (timeSinceLastRan >= limit) {
func.apply(this, args);
lastRan = Date.now();
}
}
lastFunc = undefined;
}, limit - timeSinceLastRan);
}
};
}

/**
* Parse hex color and return the RGB colors
Expand Down
34 changes: 27 additions & 7 deletions packages/ui-react/src/components/LayoutGrid/LayoutGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Keyboard-accessible grid layout, with focus management

import { throttle } from '@jwp/ott-common/src/utils/common';
import useEventCallback from '@jwp/ott-hooks-react/src/useEventCallback';
import { useEffect, useLayoutEffect, useRef, useState } from 'react';

Expand All @@ -12,16 +13,26 @@ type Props<Item> = {
renderCell: (item: Item, tabIndex: number) => JSX.Element;
};

const scrollIntoViewThrottled = throttle(function (focusedElement: HTMLElement) {
focusedElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 300);

const LayoutGrid = <Item extends object>({ className, columnCount, data, renderCell }: Props<Item>) => {
const [focused, setFocused] = useState(false);
const [currentRowIndex, setCurrentRowIndex] = useState(0);
const [currentColumnIndex, setCurrentColumnIndex] = useState(0);

const gridRef = useRef<HTMLDivElement>(null);
const rowCount = Math.ceil(data.length / columnCount);

const handleKeyDown = useEventCallback(({ key, ctrlKey }: KeyboardEvent) => {
const handleKeyDown = useEventCallback((event: KeyboardEvent) => {
if (event instanceof KeyboardEvent === false) return;

const { key, ctrlKey } = event;

if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown'].includes(key)) return;

event.preventDefault();

const isOnFirstColumn = currentColumnIndex === 0;
const isOnLastColumn = currentColumnIndex === columnCount - 1;
const isOnFirstRow = currentRowIndex === 0;
Expand Down Expand Up @@ -74,16 +85,23 @@ const LayoutGrid = <Item extends object>({ className, columnCount, data, renderC
}
});

const originalScrollBehavior = useRef<string | null>(null);

useEffect(() => {
if (focused) {
document.addEventListener('keydown', handleKeyDown);

// Prevent immediate page scrolling when out-of-viewport element gets focus
originalScrollBehavior.current = document.documentElement.style.scrollBehavior;
document.documentElement.style.scrollBehavior = 'smooth';
}

return () => document.removeEventListener('keydown', handleKeyDown);
return () => {
document.documentElement.style.scrollBehavior = originalScrollBehavior.current || '';
document.removeEventListener('keydown', handleKeyDown);
};
}, [focused, handleKeyDown, columnCount, rowCount]);

const gridRef = useRef<HTMLDivElement>(null);

// Set DOM focus to a focusable element within the currently focusable grid cell
useLayoutEffect(() => {
if (!focused) return;
Expand All @@ -92,7 +110,10 @@ const LayoutGrid = <Item extends object>({ className, columnCount, data, renderC
const focusableElement = gridCell?.querySelector('button, a, input, [tabindex]:not([tabindex="-1"])') as HTMLElement | null;
const elementToFocus = focusableElement || gridCell;

elementToFocus?.focus();
if (!elementToFocus) return;

elementToFocus.focus();
scrollIntoViewThrottled(elementToFocus);
}, [focused, currentRowIndex, currentColumnIndex]);

// When the window size changes, correct indexes if necessary
Expand All @@ -114,7 +135,6 @@ const LayoutGrid = <Item extends object>({ className, columnCount, data, renderC
{data.slice(rowIndex * columnCount, rowIndex * columnCount + columnCount).map((item, columnIndex) => (
<div
role="gridcell"
onFocus={(event) => event.target.scrollIntoView?.({ behavior: 'smooth', block: 'center' })}
id={`layout_grid_${rowIndex}-${columnIndex}`}
key={columnIndex}
aria-colindex={columnIndex}
Expand Down

0 comments on commit af567e8

Please sign in to comment.