Skip to content

Commit

Permalink
Improve snippets scrolling behaviour (#364)
Browse files Browse the repository at this point in the history
  • Loading branch information
felixhabib authored Sep 13, 2024
1 parent ff68fad commit ee73b75
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 65 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': minor
---

Update snippets behaviour to instantly navigate and scroll to the currently selected snippet.
This eliminates sluggish feeling caused by smooth scroll.
36 changes: 32 additions & 4 deletions src/Playroom/Snippets/Snippets.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,62 @@ export const fieldContainer = style([
},
]);

const snippetsBorderSpace = 'small';

export const snippetsContainer = style([
sprinkles({
position: 'absolute',
left: 0,
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,
Expand All @@ -60,7 +89,6 @@ export const snippet = style([
backgroundColor: colorPaletteVars.background.selection,
borderRadius: vars.radii.medium,
opacity: 0,
transition: vars.transition.slow,
pointerEvents: 'none',
},
},
Expand Down
84 changes: 25 additions & 59 deletions src/Playroom/Snippets/Snippets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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
Expand All @@ -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<string>('');
const [highlightedIndex, setHighlightedIndex] =
Expand All @@ -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'
Expand All @@ -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 (
Expand Down Expand Up @@ -173,6 +136,7 @@ export default ({ snippets, onHighlight, onClose }: Props) => {
return (
<li
ref={isHighlighted ? highlightedEl : undefined}
id={getSnippetId(snippet, index)}
key={`${snippet.group}_${snippet.name}_${index}`}
className={classnames(styles.snippet, {
[styles.highlight]: isHighlighted,
Expand All @@ -183,12 +147,14 @@ export default ({ snippets, onHighlight, onClose }: Props) => {
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>
<Stack space="none">
<Text size="large" weight="strong">
{snippet.group}
</Text>
<Text size="large" tone="secondary">
{snippet.name}
</Text>
</span>
</Stack>
</li>
);
})}
Expand Down
4 changes: 4 additions & 0 deletions src/Playroom/Text/Text.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
Expand Down
2 changes: 1 addition & 1 deletion src/Playroom/Text/Text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/Playroom/vars.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit ee73b75

Please sign in to comment.