Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion examples/07-collaboration/05-comments/src/SettingsSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ComponentProps, useComponentsContext } from "@blocknote/react";
import {
ComponentProps,
useComponentsContext,
useEditorPortalElement,
} from "@blocknote/react";

// This component is used to display a selection dropdown with a label. By using
// the useComponentsContext hook, we can create it out of existing components
Expand All @@ -9,6 +13,10 @@ export const SettingsSelect = (props: {
items: ComponentProps["FormattingToolbar"]["Select"]["items"];
}) => {
const Components = useComponentsContext()!;
// The select's dropdown portals into the editor's portal element, which keeps
// it themed and clear of any overflow clipping. The prop is required, so it
// can't be left out by accident.
const editorPortalElement = useEditorPortalElement();

return (
<div className={"settings-select"}>
Expand All @@ -17,6 +25,7 @@ export const SettingsSelect = (props: {
<Components.Generic.Toolbar.Select
className={"bn-select"}
items={props.items}
portalRoot={editorPortalElement}
/>
</Components.Generic.Toolbar.Root>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ComponentProps, useComponentsContext } from "@blocknote/react";
import {
ComponentProps,
useComponentsContext,
useEditorPortalElement,
} from "@blocknote/react";

// This component is used to display a selection dropdown with a label. By using
// the useComponentsContext hook, we can create it out of existing components
Expand All @@ -9,6 +13,10 @@ export const SettingsSelect = (props: {
items: ComponentProps["FormattingToolbar"]["Select"]["items"];
}) => {
const Components = useComponentsContext()!;
// The select's dropdown portals into the editor's portal element, which keeps
// it themed and clear of any overflow clipping. The prop is required, so it
// can't be left out by accident.
const editorPortalElement = useEditorPortalElement();

return (
<div className={"settings-select"}>
Expand All @@ -17,6 +25,7 @@ export const SettingsSelect = (props: {
<Components.Generic.Toolbar.Select
className={"bn-select"}
items={props.items}
portalRoot={editorPortalElement}
/>
</Components.Generic.Toolbar.Root>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ComponentProps, useComponentsContext } from "@blocknote/react";
import {
ComponentProps,
useComponentsContext,
useEditorPortalElement,
} from "@blocknote/react";

// This component is used to display a selection dropdown with a label. By using
// the useComponentsContext hook, we can create it out of existing components
Expand All @@ -9,6 +13,10 @@ export const SettingsSelect = (props: {
items: ComponentProps["FormattingToolbar"]["Select"]["items"];
}) => {
const Components = useComponentsContext()!;
// The select's dropdown portals into the editor's portal element, which keeps
// it themed and clear of any overflow clipping. The prop is required, so it
// can't be left out by accident.
const editorPortalElement = useEditorPortalElement();

return (
<div className={"settings-select"}>
Expand All @@ -17,6 +25,7 @@ export const SettingsSelect = (props: {
<Components.Generic.Toolbar.Select
className={"bn-select"}
items={props.items}
portalRoot={editorPortalElement}
/>
</Components.Generic.Toolbar.Root>
</div>
Expand Down
12 changes: 8 additions & 4 deletions packages/ariakit/src/menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@ import { createContext, forwardRef, useContext } from "react";

// Threads the `portalRoot` override from `Menu` (the provider) down to
// `MenuDropdown`, where ariakit's `portalElement` prop actually lives.
const PortalRootContext = createContext<HTMLElement | null | undefined>(
undefined,
);
const PortalRootContext = createContext<HTMLElement | null>(null);

export const Menu = (props: ComponentProps["Generic"]["Menu"]["Root"]) => {
const {
children,
onOpenChange,
position,
portalRoot,
// ariakit's `virtualFocus` keeps DOM focus on the editor (roving via
// `aria-activedescendant`), so there is no focus to suppress here.
preventFocusOnOpen: _preventFocusOnOpen,
sub: _sub, // unused
...rest
} = props;
Expand Down Expand Up @@ -63,7 +64,10 @@ export const MenuDropdown = forwardRef<
<AriakitMenu
unmountOnHide={true}
className={mergeCSSClasses("bn-ak-menu", className || "")}
portalElement={portalRoot ?? undefined}
// Ariakit falls back to a body-appended div for a missing element, so
// don't portal at all until there is one (editor not mounted yet).
portal={portalRoot !== null}
portalElement={portalRoot}
ref={ref}
>
{children}
Expand Down
19 changes: 14 additions & 5 deletions packages/ariakit/src/popover/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import { assertEmpty, mergeCSSClasses } from "@blocknote/core";
import { ComponentProps } from "@blocknote/react";
import { createContext, forwardRef, useContext } from "react";

const PortalRootContext = createContext<HTMLElement | null | undefined>(
undefined,
);
const PortalRootContext = createContext<HTMLElement | null>(null);

export const PopoverTrigger = forwardRef<
HTMLButtonElement,
Expand Down Expand Up @@ -40,7 +38,10 @@ export const PopoverContent = forwardRef<
className || "",
variant === "panel-popover" ? "bn-ak-panel-popover" : "",
)}
portalElement={portalRoot ?? undefined}
// Ariakit falls back to a body-appended div for a missing element, so
// don't portal at all until there is one (editor not mounted yet).
portal={portalRoot !== null}
portalElement={portalRoot}
ref={ref}
>
{children}
Expand All @@ -51,7 +52,15 @@ export const PopoverContent = forwardRef<
export const Popover = (
props: ComponentProps["Generic"]["Popover"]["Root"],
) => {
const { children, open, onOpenChange, position, portalRoot, ...rest } = props;
const {
children,
open,
onOpenChange,
position,
portalRoot,
preventFocusOnOpen: _preventFocusOnOpen, // unused; see Menu.tsx
...rest
} = props;

assertEmpty(rest);

Expand Down
14 changes: 12 additions & 2 deletions packages/ariakit/src/toolbar/ToolbarSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ export const ToolbarSelect = forwardRef<
HTMLDivElement,
ComponentProps["FormattingToolbar"]["Select"]
>((props, ref) => {
const { className, items, isDisabled, portalRoot, ...rest } = props;
const {
className,
items,
isDisabled,
portalRoot,
preventFocusOnOpen: _preventFocusOnOpen, // unused; see Menu.tsx
...rest
} = props;

assertEmpty(rest);

Expand All @@ -40,7 +47,10 @@ export const ToolbarSelect = forwardRef<
className={mergeCSSClasses("bn-ak-popover", className || "")}
ref={ref}
gutter={4}
portalElement={portalRoot ?? undefined}
// Ariakit falls back to a body-appended div for a missing element,
// so don't portal at all until there is one (editor not mounted yet).
portal={portalRoot !== null}
portalElement={portalRoot}
>
{items.map((option) => (
<AriakitSelectItem
Expand Down
16 changes: 12 additions & 4 deletions packages/mantine/src/menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ const SubMenuContext = createContext<
>(undefined);

export const Menu = (props: ComponentProps["Generic"]["Menu"]["Root"]) => {
const { children, onOpenChange, position, portalRoot, sub, ...rest } = props;
const {
children,
onOpenChange,
position,
portalRoot,
preventFocusOnOpen,
sub,
...rest
} = props;

assertEmpty(rest);

Expand All @@ -38,9 +46,9 @@ export const Menu = (props: ComponentProps["Generic"]["Menu"]["Root"]) => {
<MantineMenu
withinPortal={!!portalRoot}
portalProps={portalRoot ? { target: portalRoot } : undefined}
// Do not move focus to dropdown when portaled (mobile), as it blurs the
// editor's contentEditable and dismisses the on-screen keyboard.
trapFocus={portalRoot ? false : undefined}
// Do not move focus to the dropdown when requested (mobile), as it blurs
// the editor's contentEditable and dismisses the on-screen keyboard.
trapFocus={preventFocusOnOpen ? false : undefined}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice improvement, so much better to have explicit naming like this

middlewares={{ flip: true, shift: true, inline: false, size: true }}
onChange={onOpenChange}
position={position}
Expand Down
16 changes: 12 additions & 4 deletions packages/mantine/src/popover/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ import { forwardRef } from "react";
export const Popover = (
props: ComponentProps["Generic"]["Popover"]["Root"],
) => {
const { open, onOpenChange, position, portalRoot, children, ...rest } = props;
const {
open,
onOpenChange,
position,
portalRoot,
preventFocusOnOpen,
children,
...rest
} = props;

assertEmpty(rest);

Expand All @@ -20,9 +28,9 @@ export const Popover = (
middlewares={{ size: { padding: 20 } }}
withinPortal={!!portalRoot}
portalProps={portalRoot ? { target: portalRoot } : undefined}
// Do not move focus to the dropdown on mobile, as it blurs the editor's
// contentEditable and dismisses the on-screen keyboard.
trapFocus={portalRoot ? false : undefined}
// Do not move focus to the dropdown when requested (mobile), as it blurs
// the editor's contentEditable and dismisses the on-screen keyboard.
trapFocus={preventFocusOnOpen ? false : undefined}
opened={open}
onChange={onOpenChange}
position={position}
Expand Down
15 changes: 11 additions & 4 deletions packages/mantine/src/toolbar/ToolbarSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@ export const ToolbarSelect = forwardRef<
HTMLDivElement,
ComponentProps["FormattingToolbar"]["Select"]
>((props, ref) => {
const { className, items, isDisabled, portalRoot, ...rest } = props;
const {
className,
items,
isDisabled,
portalRoot,
preventFocusOnOpen,
...rest
} = props;

assertEmpty(rest);

Expand All @@ -32,9 +39,9 @@ export const ToolbarSelect = forwardRef<
exitDuration: 0,
}}
disabled={isDisabled}
// Do not move focus to the dropdown on mobile, as it blurs the editor's
// contentEditable and dismisses the on-screen keyboard.
trapFocus={portalRoot ? false : undefined}
// Do not move focus to the dropdown when requested (mobile), as it blurs
// the editor's contentEditable and dismisses the on-screen keyboard.
trapFocus={preventFocusOnOpen ? false : undefined}
middlewares={{
flip: true,
shift: true,
Expand Down
7 changes: 6 additions & 1 deletion packages/react/src/components/Comments/Comment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
Components,
useComponentsContext,
} from "../../editor/ComponentsContext.js";
import { useEditorPortalElement } from "../../editor/EditorPortalProvider.js";
import { useCreateBlockNote } from "../../hooks/useCreateBlockNote.js";
import { useExtension } from "../../hooks/useExtension.js";
import { useDictionary } from "../../i18n/dictionary.js";
Expand Down Expand Up @@ -161,6 +162,7 @@ export const Comment = ({
});

const Components = useComponentsContext()!;
const editorPortalElement = useEditorPortalElement();

const [isEditing, setEditing] = useState(false);
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
Expand Down Expand Up @@ -289,7 +291,10 @@ export const Comment = ({
</Components.Generic.Toolbar.Button>
))}
{(canDeleteComment || canEditComment) && (
<Components.Generic.Menu.Root position={"bottom-start"}>
<Components.Generic.Menu.Root
position={"bottom-start"}
portalRoot={editorPortalElement}
>
<Components.Generic.Menu.Trigger>
<Components.Generic.Toolbar.Button
key={"more-actions"}
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/components/Comments/EmojiPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const EmojiPicker = (props: {
return (
<Components.Generic.Popover.Root
open={open}
portalRoot={editorPortalElement ?? undefined}
portalRoot={editorPortalElement}
>
<Components.Generic.Popover.Trigger>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,6 @@ export const ColorStyleButton = () => {
const dict = useDictionary();
const uiMode = useUIMode();
const editorPortalElement = useEditorPortalElement();
// Only portal (and suppress dropdown focus) in the mobile toolbar; desktop
// renders inline with default focus behavior.
const portalRoot =
uiMode === "mobile" ? (editorPortalElement ?? undefined) : undefined;
const editor = useBlockNoteEditor<
BlockSchema,
InlineContentSchema,
Expand Down Expand Up @@ -145,14 +141,14 @@ export const ColorStyleButton = () => {

return (
<Components.Generic.Menu.Root
// On mobile, portal the dropdown into the toolbar's themed body-level
// container (see `MobileFormattingToolbarController`) so it escapes the
// editor's scroll container overflow instead of being clipped, while
// staying styled. A set `portalRoot` also stops focus moving into the
// dropdown, which would blur the editor and dismiss the on-screen
// keyboard. On desktop it's `undefined`, keeping the default inline
// rendering.
portalRoot={portalRoot}
// Portal the dropdown into the editor's themed portal target so it
// inherits styling and escapes any scroll-container overflow clipping.
// On mobile that target is the toolbar's body-level container (see
// `MobileFormattingToolbarController`), and `preventFocusOnOpen` stops
// focus moving into the dropdown, which would blur the editor and dismiss
// the on-screen keyboard.
portalRoot={editorPortalElement}
preventFocusOnOpen={uiMode === "mobile"}
>
<Components.Generic.Menu.Trigger>
<Components.FormattingToolbar.Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@ export const CreateLinkButton = () => {
const dict = useDictionary();
const uiMode = useUIMode();
const editorPortalElement = useEditorPortalElement();
// Only portal (and suppress dropdown focus) in the mobile toolbar; desktop
// renders inline with default focus behavior.
const portalRoot =
uiMode === "mobile" ? (editorPortalElement ?? undefined) : undefined;

const formattingToolbar = useExtension(FormattingToolbarExtension);
// eslint-disable-next-line @typescript-eslint/unbound-method -- showSelection is a plain object method, not a class method
Expand Down Expand Up @@ -136,13 +132,14 @@ export const CreateLinkButton = () => {
<Components.Generic.Popover.Root
open={showPopover}
onOpenChange={setPopoverOpen}
// On mobile, portal the popover into the toolbar's themed body-level
// container (see `MobileFormattingToolbarController`) so it escapes the
// editor's scroll container overflow instead of being clipped, while
// staying styled. A set `portalRoot` also stops focus moving into the
// popover, which would blur the editor and dismiss the on-screen keyboard.
// On desktop it's `undefined`, keeping the default inline rendering.
portalRoot={portalRoot}
// Portal the popover into the editor's themed portal target so it
// inherits styling and escapes any scroll-container overflow clipping.
// On mobile that target is the toolbar's body-level container (see
// `MobileFormattingToolbarController`), and `preventFocusOnOpen` stops
// focus moving into the popover, which would blur the editor and dismiss
// the on-screen keyboard.
portalRoot={editorPortalElement}
preventFocusOnOpen={uiMode === "mobile"}
>
<Components.Generic.Popover.Trigger>
{/* TODO: hide tooltip on click */}
Expand Down
Loading
Loading