diff --git a/.changeset/wicked-carrots-watch.md b/.changeset/wicked-carrots-watch.md new file mode 100644 index 00000000..424a4960 --- /dev/null +++ b/.changeset/wicked-carrots-watch.md @@ -0,0 +1,6 @@ +--- +'playroom': minor +--- + +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..39887030 100644 --- a/src/Playroom/Snippets/Snippets.css.ts +++ b/src/Playroom/Snippets/Snippets.css.ts @@ -25,6 +25,8 @@ export const fieldContainer = style([ }, ]); +const snippetsBorderSpace = 'small'; + export const snippetsContainer = style([ sprinkles({ position: 'absolute', @@ -32,26 +34,53 @@ export const snippetsContainer = style([ bottom: 0, right: 0, overflow: 'auto', - padding: 'none', - margin: 'small', + paddingX: 'none', + paddingY: snippetsBorderSpace, + margin: 'none', }), { + listStyle: 'none', top: toolbarItemSize, + /* + These pseudo-elements create a buffer area at the top and bottom of the list, the same size as the scroll margin. + This prevents auto-scrolling when the cursor enters a snippet in the scroll margin, by preventing the element from being selected. + */ + '::before': { + content: '', + position: 'fixed', + top: toolbarItemSize, + left: 0, + right: 0, + height: vars.space[snippetsBorderSpace], + zIndex: 1, + }, + '::after': { + content: '', + position: 'fixed', + bottom: 0, + left: 0, + right: 0, + height: vars.space[snippetsBorderSpace], + zIndex: 1, + }, }, ]); export const snippet = style([ sprinkles({ position: 'relative', + display: 'block', cursor: 'pointer', paddingY: 'large', paddingX: 'xlarge', + marginX: snippetsBorderSpace, }), { + scrollMarginBlock: vars.space[snippetsBorderSpace], color: colorPaletteVars.foreground.neutral, backgroundColor: colorPaletteVars.background.surface, '::before': { - content: '""', + content: '', position: 'absolute', top: 0, bottom: 0, @@ -60,7 +89,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..ea9cc567 100644 --- a/src/Playroom/Snippets/Snippets.tsx +++ b/src/Playroom/Snippets/Snippets.tsx @@ -5,8 +5,8 @@ import { useDebouncedCallback } from 'use-debounce'; import type { PlayroomProps } from '../Playroom'; import type { Snippet } from '../../../utils'; import SearchField from './SearchField/SearchField'; -import { Strong } from '../Strong/Strong'; import { Text } from '../Text/Text'; +import { Stack } from '../Stack/Stack'; import * as styles from './Snippets.css'; @@ -20,6 +20,10 @@ interface Props { const getLabel = (snippet: Snippet) => `${snippet.group}\n${snippet.name}`; +function getSnippetId(snippet: Snippet, index: number) { + return `${snippet.group}_${snippet.name}_${index}`; +} + const filterSnippetsForTerm = (snippets: Props['snippets'], term: string) => term ? fuzzy @@ -29,52 +33,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 +52,23 @@ export default ({ snippets, onHighlight, onClose }: Props) => { }, 50 ); - const debounceScrollToHighlighted = useDebouncedCallback( - scrollToHighlightedSnippet, - 50 - ); + const filteredSnippets = useMemo( () => filterSnippetsForTerm(snippets, searchTerm), [searchTerm, snippets] ); + if ( + typeof highlightedIndex === 'number' && + filteredSnippets[highlightedIndex] + ) { + const highlightedItem = document.getElementById( + getSnippetId(filteredSnippets[highlightedIndex], highlightedIndex) + ); + + highlightedItem?.scrollIntoView({ block: 'nearest' }); + } + useEffect(() => { debouncedPreview( typeof highlightedIndex === 'number' @@ -125,9 +91,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,6 +136,7 @@ export default ({ snippets, onHighlight, onClose }: Props) => { return (
  • { onMouseDown={() => closeHandler(filteredSnippets[index])} title={getLabel(snippet)} > - - - {snippet.group} - {snippet.name} + + + {snippet.group} + + + {snippet.name} - +
  • ); })} diff --git a/src/Playroom/Text/Text.css.ts b/src/Playroom/Text/Text.css.ts index 81cf1271..0713b4c1 100644 --- a/src/Playroom/Text/Text.css.ts +++ b/src/Playroom/Text/Text.css.ts @@ -13,6 +13,10 @@ export const critical = style({ color: colorPaletteVars.foreground.critical, }); +export const secondary = style({ + color: colorPaletteVars.foreground.secondary, +}); + export const xsmall = sprinkles({ font: 'xsmall', }); diff --git a/src/Playroom/Text/Text.tsx b/src/Playroom/Text/Text.tsx index 0f27e5a9..7b48e69e 100644 --- a/src/Playroom/Text/Text.tsx +++ b/src/Playroom/Text/Text.tsx @@ -6,7 +6,7 @@ import * as styles from './Text.css'; interface Props { size?: 'xsmall' | 'small' | 'standard' | 'large'; weight?: 'regular' | 'strong'; - tone?: 'neutral' | 'critical'; + tone?: 'neutral' | 'secondary' | 'critical'; as?: ElementType; truncate?: boolean; children: ReactNode; diff --git a/src/Playroom/vars.css.ts b/src/Playroom/vars.css.ts index 35292a48..f30180c7 100644 --- a/src/Playroom/vars.css.ts +++ b/src/Playroom/vars.css.ts @@ -11,7 +11,7 @@ export const vars = createGlobalTheme(':root', { xsmall: `normal 10px ${fontFamily}`, small: `normal 12px ${fontFamily}`, standard: `normal 14px ${fontFamily}`, - large: `normal 16px/1.3em ${fontFamily}`, + large: `normal 16px/20px ${fontFamily}`, }, weight: { weak: '100',