From 6e898597e614a97b4a820a6abf2cf9270e901bee Mon Sep 17 00:00:00 2001 From: Felix Habib Date: Mon, 9 Sep 2024 15:29:20 +1000 Subject: [PATCH] Improve snippets scrolling behaviour --- .changeset/wicked-carrots-watch.md | 6 ++ src/Playroom/Snippets/Snippets.css.ts | 8 +- src/Playroom/Snippets/Snippets.tsx | 87 +++++++--------------- src/Playroom/Snippets/useScrollIntoView.ts | 37 +++++++++ 4 files changed, 74 insertions(+), 64 deletions(-) create mode 100644 .changeset/wicked-carrots-watch.md create mode 100644 src/Playroom/Snippets/useScrollIntoView.ts diff --git a/.changeset/wicked-carrots-watch.md b/.changeset/wicked-carrots-watch.md new file mode 100644 index 00000000..cb34342b --- /dev/null +++ b/.changeset/wicked-carrots-watch.md @@ -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. diff --git a/src/Playroom/Snippets/Snippets.css.ts b/src/Playroom/Snippets/Snippets.css.ts index 51b49886..1b6b13ec 100644 --- a/src/Playroom/Snippets/Snippets.css.ts +++ b/src/Playroom/Snippets/Snippets.css.ts @@ -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, @@ -60,7 +65,6 @@ export const snippet = style([ backgroundColor: colorPaletteVars.background.selection, borderRadius: vars.radii.medium, opacity: 0, - transition: vars.transition.slow, pointerEvents: 'none', }, }, diff --git a/src/Playroom/Snippets/Snippets.tsx b/src/Playroom/Snippets/Snippets.tsx index 348ed793..424a94a2 100644 --- a/src/Playroom/Snippets/Snippets.tsx +++ b/src/Playroom/Snippets/Snippets.tsx @@ -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; @@ -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(''); const [highlightedIndex, setHighlightedIndex] = @@ -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' @@ -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 ( @@ -173,22 +131,27 @@ export default ({ snippets, onHighlight, onClose }: Props) => { return (
  • setHighlightedIndex(index) } onMouseDown={() => closeHandler(filteredSnippets[index])} title={getLabel(snippet)} > - - - {snippet.group} - {snippet.name} - - +
    + + + {snippet.group} + {snippet.name} + + +
  • ); })} diff --git a/src/Playroom/Snippets/useScrollIntoView.ts b/src/Playroom/Snippets/useScrollIntoView.ts new file mode 100644 index 00000000..40e5d03e --- /dev/null +++ b/src/Playroom/Snippets/useScrollIntoView.ts @@ -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]); +}