Skip to content

Commit

Permalink
Improve snippets scrolling behaviour
Browse files Browse the repository at this point in the history
  • Loading branch information
felixhabib committed Sep 9, 2024
1 parent ae7053f commit 6e89859
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 64 deletions.
6 changes: 6 additions & 0 deletions .changeset/wicked-carrots-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'playroom': patch
---

Update snippets behaviour to instantly navigate and scroll to the currently selected snippet.
This eliminates sluggish feeling caused by smooth scroll.
8 changes: 6 additions & 2 deletions src/Playroom/Snippets/Snippets.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,24 @@ export const snippetsContainer = style([
right: 0,
overflow: 'auto',
padding: 'none',
margin: 'small',
margin: 'none',
}),
{
top: toolbarItemSize,
},
]);

export const snippetPadding = sprinkles({
padding: 'small',
});

export const snippet = style([
sprinkles({
position: 'relative',
cursor: 'pointer',
paddingY: 'large',
paddingX: 'xlarge',
margin: 'none',
}),
{
color: colorPaletteVars.foreground.neutral,
Expand All @@ -60,7 +65,6 @@ export const snippet = style([
backgroundColor: colorPaletteVars.background.selection,
borderRadius: vars.radii.medium,
opacity: 0,
transition: vars.transition.slow,
pointerEvents: 'none',
},
},
Expand Down
87 changes: 25 additions & 62 deletions src/Playroom/Snippets/Snippets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Strong } from '../Strong/Strong';
import { Text } from '../Text/Text';

import * as styles from './Snippets.css';
import { useScrollIntoView } from './useScrollIntoView';

type HighlightIndex = number | null;
type ReturnedSnippet = Snippet | null;
Expand All @@ -29,52 +30,6 @@ const filterSnippetsForTerm = (snippets: Props['snippets'], term: string) =>
.map(({ original, score }) => ({ ...original, score }))
: snippets;

const scrollToHighlightedSnippet = (
listEl: HTMLUListElement | null,
highlightedEl: HTMLLIElement | null
) => {
if (highlightedEl && listEl) {
const scrollStep = Math.max(
Math.ceil(listEl.offsetHeight * 0.25),
highlightedEl.offsetHeight * 2
);
const currentListTop = listEl.scrollTop + scrollStep;
const currentListBottom =
listEl.offsetHeight + listEl.scrollTop - scrollStep;
let top = 0;

if (
highlightedEl === listEl.firstChild ||
highlightedEl === listEl.lastChild
) {
highlightedEl.scrollIntoView(false);
return;
}

if (highlightedEl.offsetTop >= currentListBottom) {
top =
highlightedEl.offsetTop -
listEl.offsetHeight +
highlightedEl.offsetHeight +
scrollStep;
} else if (highlightedEl.offsetTop <= currentListTop) {
top = highlightedEl.offsetTop - scrollStep;
} else {
return;
}

if ('scrollBehavior' in window.document.documentElement.style) {
listEl.scrollTo({
left: 0,
top,
behavior: 'smooth',
});
} else {
listEl.scrollTo(0, top);
}
}
};

export default ({ snippets, onHighlight, onClose }: Props) => {
const [searchTerm, setSearchTerm] = useState<string>('');
const [highlightedIndex, setHighlightedIndex] =
Expand All @@ -94,15 +49,21 @@ export default ({ snippets, onHighlight, onClose }: Props) => {
},
50
);
const debounceScrollToHighlighted = useDebouncedCallback(
scrollToHighlightedSnippet,
50
);

const filteredSnippets = useMemo(
() => filterSnippetsForTerm(snippets, searchTerm),
[searchTerm, snippets]
);

const highlightedItem =
typeof highlightedIndex === 'number'
? document.getElementById(
`${filteredSnippets[highlightedIndex].group}_${filteredSnippets[highlightedIndex].name}_${highlightedIndex}`
)
: null;

useScrollIntoView(highlightedItem, listEl.current);

useEffect(() => {
debouncedPreview(
typeof highlightedIndex === 'number'
Expand All @@ -125,9 +86,6 @@ export default ({ snippets, onHighlight, onClose }: Props) => {
onBlur={() => {
setHighlightedIndex(null);
}}
onKeyUp={() => {
debounceScrollToHighlighted(listEl.current, highlightedEl.current);
}}
onKeyDown={(event) => {
if (/^(?:Arrow)?Down$/.test(event.key)) {
if (
Expand Down Expand Up @@ -173,22 +131,27 @@ export default ({ snippets, onHighlight, onClose }: Props) => {
return (
<li
ref={isHighlighted ? highlightedEl : undefined}
id={`${snippet.group}_${snippet.name}_${index}`}
key={`${snippet.group}_${snippet.name}_${index}`}
className={classnames(styles.snippet, {
[styles.highlight]: isHighlighted,
})}
className={styles.snippetPadding}
onMouseMove={
isHighlighted ? undefined : () => setHighlightedIndex(index)
}
onMouseDown={() => closeHandler(filteredSnippets[index])}
title={getLabel(snippet)}
>
<span style={{ display: 'block', position: 'relative' }}>
<Text size="large">
<Strong>{snippet.group}</Strong>
<span className={styles.snippetName}>{snippet.name}</span>
</Text>
</span>
<div
className={classnames(styles.snippet, {
[styles.highlight]: isHighlighted,
})}
>
<span style={{ display: 'block', position: 'relative' }}>
<Text size="large">
<Strong>{snippet.group}</Strong>
<span className={styles.snippetName}>{snippet.name}</span>
</Text>
</span>
</div>
</li>
);
})}
Expand Down
37 changes: 37 additions & 0 deletions src/Playroom/Snippets/useScrollIntoView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useEffect } from 'react';

export function useScrollIntoView(
element: HTMLElement | null,
scrollContainer: HTMLElement | null
) {
useEffect(() => {
if (!scrollContainer || !element) {
return;
}

const itemOffsetRelativeToContainer =
element.offsetParent === scrollContainer
? element.offsetTop
: element.offsetTop - scrollContainer.offsetTop;

let { scrollTop } = scrollContainer; // Top of the visible area

if (itemOffsetRelativeToContainer < scrollTop) {
// Item is off the top of the visible area
scrollTop = itemOffsetRelativeToContainer;
} else if (
itemOffsetRelativeToContainer + element.offsetHeight >
scrollTop + scrollContainer.offsetHeight
) {
// Item is off the bottom of the visible area
scrollTop =
itemOffsetRelativeToContainer +
element.offsetHeight -
scrollContainer.offsetHeight;
}

if (scrollTop !== scrollContainer.scrollTop) {
scrollContainer.scrollTop = scrollTop;
}
}, [scrollContainer, element]);
}

0 comments on commit 6e89859

Please sign in to comment.