Skip to content

Commit 8eeb833

Browse files
fix: Suggestion menu scrolling to selected item (#838)
* Made suggestion menu components scroll to selected item * Added overflow detection and cleaned up ref merging
1 parent cc564bb commit 8eeb833

File tree

8 files changed

+86
-14
lines changed

8 files changed

+86
-14
lines changed

packages/ariakit/src/suggestionMenu/SuggestionMenuItem.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { assertEmpty, mergeCSSClasses } from "@blocknote/core";
2-
import { ComponentProps } from "@blocknote/react";
3-
import { forwardRef } from "react";
2+
import { ComponentProps, elementOverflow, mergeRefs } from "@blocknote/react";
3+
import { forwardRef, useEffect, useRef } from "react";
44

55
export const SuggestionMenuItem = forwardRef<
66
HTMLDivElement,
@@ -10,10 +10,26 @@ export const SuggestionMenuItem = forwardRef<
1010

1111
assertEmpty(rest);
1212

13+
const itemRef = useRef<HTMLDivElement>(null);
14+
15+
useEffect(() => {
16+
if (!itemRef.current || !isSelected) {
17+
return;
18+
}
19+
20+
const overflow = elementOverflow(itemRef.current);
21+
22+
if (overflow === "top") {
23+
itemRef.current.scrollIntoView(true);
24+
} else if (overflow === "bottom") {
25+
itemRef.current.scrollIntoView(false);
26+
}
27+
}, [isSelected]);
28+
1329
return (
1430
<div
1531
className={mergeCSSClasses("bn-ak-menu-item", className || "")}
16-
ref={ref}
32+
ref={mergeRefs([ref, itemRef])}
1733
id={id}
1834
onClick={onClick}
1935
role="option"

packages/mantine/src/style.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,6 @@
322322

323323
.bn-mt-suggestion-menu-item-body {
324324
align-items: stretch;
325-
color: var(--bn-colors-menu-text);
326325
display: flex;
327326
flex: 1;
328327
flex-direction: column;
@@ -331,6 +330,7 @@
331330
}
332331

333332
.bn-mt-suggestion-menu-item-title {
333+
color: var(--bn-colors-menu-text);
334334
line-height: 20px;
335335
font-weight: 500;
336336
font-size: 14px;
@@ -339,6 +339,7 @@
339339
}
340340

341341
.bn-mt-suggestion-menu-item-subtitle {
342+
color: var(--bn-colors-menu-text);
342343
line-height: 16px;
343344
font-size: 10px;
344345
margin: 0;

packages/mantine/src/suggestionMenu/SuggestionMenuEmptyItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const SuggestionMenuEmptyItem = forwardRef<
1414

1515
return (
1616
<MantineGroup className={className} ref={ref}>
17-
<MantineGroup className="bn-mt-suggestion-menu-item-label">
17+
<MantineGroup className="bn-mt-suggestion-menu-item-title">
1818
{children}
1919
</MantineGroup>
2020
</MantineGroup>

packages/mantine/src/suggestionMenu/SuggestionMenuItem.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import {
44
Stack as MantineStack,
55
Text as MantineText,
66
} from "@mantine/core";
7+
import { mergeRefs } from "@mantine/hooks";
78

89
import { assertEmpty } from "@blocknote/core";
9-
import { ComponentProps } from "@blocknote/react";
10-
import { forwardRef } from "react";
10+
import { ComponentProps, elementOverflow } from "@blocknote/react";
11+
import { forwardRef, useEffect, useRef } from "react";
1112

1213
export const SuggestionMenuItem = forwardRef<
1314
HTMLDivElement,
@@ -17,11 +18,27 @@ export const SuggestionMenuItem = forwardRef<
1718

1819
assertEmpty(rest);
1920

21+
const itemRef = useRef<HTMLDivElement>(null);
22+
23+
useEffect(() => {
24+
if (!itemRef.current || !isSelected) {
25+
return;
26+
}
27+
28+
const overflow = elementOverflow(itemRef.current);
29+
30+
if (overflow === "top") {
31+
itemRef.current.scrollIntoView(true);
32+
} else if (overflow === "bottom") {
33+
itemRef.current.scrollIntoView(false);
34+
}
35+
}, [isSelected]);
36+
2037
return (
2138
<MantineGroup
2239
gap={0}
2340
className={className}
24-
ref={ref}
41+
ref={mergeRefs(ref, itemRef)}
2542
id={id}
2643
role="option"
2744
onClick={onClick}

packages/react/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,6 @@ export * from "./hooks/useSelectedBlocks";
7979
export * from "./schema/ReactBlockSpec";
8080
export * from "./schema/ReactInlineContentSpec";
8181
export * from "./schema/ReactStyleSpec";
82+
83+
export * from "./util/mergeRefs";
84+
export * from "./util/elementOverflow";
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export function elementOverflow(element: HTMLElement) {
2+
if (!element.parentElement) {
3+
return "none";
4+
}
5+
6+
const elementRect = element.getBoundingClientRect();
7+
const parentRect = element.parentElement.getBoundingClientRect();
8+
9+
const topOverflow = elementRect.top < parentRect.top;
10+
const bottomOverflow = elementRect.bottom > parentRect.bottom;
11+
12+
return topOverflow && bottomOverflow
13+
? "both"
14+
: topOverflow
15+
? "top"
16+
: bottomOverflow
17+
? "bottom"
18+
: "none";
19+
}

packages/shadcn/src/lib/utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { type ClassValue, clsx } from "clsx"
2-
import { twMerge } from "tailwind-merge"
1+
import { type ClassValue, clsx } from "clsx";
2+
import { twMerge } from "tailwind-merge";
33

44
export function cn(...inputs: ClassValue[]) {
5-
return twMerge(clsx(inputs))
5+
return twMerge(clsx(inputs));
66
}

packages/shadcn/src/suggestionMenu/SuggestionMenuItem.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { assertEmpty } from "@blocknote/core";
2-
import { ComponentProps } from "@blocknote/react";
3-
import { forwardRef } from "react";
2+
import { ComponentProps, elementOverflow, mergeRefs } from "@blocknote/react";
3+
import { forwardRef, useEffect, useRef } from "react";
44

55
import { cn } from "../lib/utils";
66
import { useShadCNComponentsContext } from "../ShadCNComponentsContext";
@@ -15,14 +15,30 @@ export const SuggestionMenuItem = forwardRef<
1515

1616
assertEmpty(rest);
1717

18+
const itemRef = useRef<HTMLDivElement>(null);
19+
20+
useEffect(() => {
21+
if (!itemRef.current || !isSelected) {
22+
return;
23+
}
24+
25+
const overflow = elementOverflow(itemRef.current);
26+
27+
if (overflow === "top") {
28+
itemRef.current.scrollIntoView(true);
29+
} else if (overflow === "bottom") {
30+
itemRef.current.scrollIntoView(false);
31+
}
32+
}, [isSelected]);
33+
1834
return (
1935
<div
2036
// Styles from ShadCN DropdownMenuItem component
2137
className={cn(
2238
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
2339
className
2440
)}
25-
ref={ref}
41+
ref={mergeRefs([ref, itemRef])}
2642
id={id}
2743
onClick={onClick}
2844
role="option"

0 commit comments

Comments
 (0)