Skip to content

Commit

Permalink
Anchor link bubble menu to link button when opening via button
Browse files Browse the repository at this point in the history
Resolves #53
  • Loading branch information
sjdemartini committed Jun 23, 2023
1 parent 3f3b395 commit 597163f
Show file tree
Hide file tree
Showing 4 changed files with 36 additions and 7 deletions.
1 change: 1 addition & 0 deletions src/LinkBubbleMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export default function LinkBubbleMenu({
<ControlledBubbleMenu
editor={editor}
open={menuState !== LinkMenuState.HIDDEN}
anchorEl={handlerStorage.anchorEl}
{...controlledBubbleMenuProps}
>
<div className={classes.content}>{linkMenuContent}</div>
Expand Down
17 changes: 13 additions & 4 deletions src/controls/MenuButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ToggleButton, type ToggleButtonProps } from "@mui/material";
import type { RefObject } from "react";
import { makeStyles } from "tss-react/mui";
import type { SetOptional } from "type-fest";
import type { Except, SetOptional } from "type-fest";
import MenuButtonTooltip, {
type MenuButtonTooltipProps,
} from "./MenuButtonTooltip";
Expand Down Expand Up @@ -29,9 +30,11 @@ export type MenuButtonProps = {
tooltipShortcutKeys?: MenuButtonTooltipProps["shortcutKeys"];
/** The icon component to use for the button. Must accept a className. */
IconComponent: React.ElementType<{ className: string }>;
} & SetOptional<ToggleButtonProps, "value">;
/** Attaches a `ref` to the ToggleButton's root button element. */
buttonRef?: RefObject<HTMLButtonElement>;
} & SetOptional<Except<ToggleButtonProps, "ref">, "value">;

const useStyles = makeStyles({ name: { MenuButton } })({
const useStyles = makeStyles({ name: "MenuButton" })({
root: {
// Use && for additional specificity, since MUI's conditional "disabled"
// styles also set the border
Expand All @@ -54,6 +57,7 @@ export default function MenuButton({
tooltipLabel,
tooltipShortcutKeys,
IconComponent,
buttonRef,
...toggleButtonProps
}: MenuButtonProps) {
const { classes } = useStyles();
Expand All @@ -63,7 +67,12 @@ export default function MenuButton({
label={tooltipLabel}
shortcutKeys={tooltipShortcutKeys}
>
<ToggleButton size="small" value={tooltipLabel} {...toggleButtonProps}>
<ToggleButton
ref={buttonRef}
size="small"
value={tooltipLabel}
{...toggleButtonProps}
>
<IconComponent className={classes.menuButtonIcon} />
</ToggleButton>
</MenuButtonTooltip>
Expand Down
9 changes: 8 additions & 1 deletion src/controls/MenuButtonEditLink.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import { Link } from "@mui/icons-material";
import { useRef } from "react";
import { useRichTextEditorContext } from "../context";
import MenuButton, { type MenuButtonProps } from "./MenuButton";

export type MenuButtonEditLinkProps = Partial<MenuButtonProps>;

export default function MenuButtonEditLink(props: MenuButtonEditLinkProps) {
const editor = useRichTextEditorContext();
const buttonRef = useRef<HTMLButtonElement | null>(null);
return (
<MenuButton
buttonRef={buttonRef}
tooltipLabel="Link"
tooltipShortcutKeys={["mod", "Shift", "U"]}
IconComponent={Link}
selected={editor?.isActive("link")}
disabled={!editor?.isEditable}
onClick={editor?.commands.openLinkBubbleMenu}
onClick={() =>
// Anchor the link bubble menu to the button, when clicking the button
// to open it
editor?.commands.openLinkBubbleMenu({ anchorEl: buttonRef.current })
}
{...props}
/>
);
Expand Down
16 changes: 14 additions & 2 deletions src/extensions/LinkBubbleMenuHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/// <reference types="@tiptap/extension-link" />
import type { PopperProps } from "@mui/material";
import { Extension, getAttributes } from "@tiptap/core";
import { Plugin, PluginKey } from "@tiptap/pm/state";

Expand All @@ -9,8 +10,16 @@ declare module "@tiptap/core" {
* Open/show the link bubble menu. Create a link if one doesn't exist at
* the current cursor selection, or edit the existing link if there is
* already a link at the current selection.
*
* If the anchorEl is provided, it's used to override the anchor point for
* positioning the bubble menu. Each call to `openLinkBubbleMenu` will
* reset the anchor based on the provided argument. If not provided, the
* bubble menu will be anchored to the location in the editor content
* where the link appears or will appear.
*/
openLinkBubbleMenu: () => ReturnType;
openLinkBubbleMenu: (options?: {
anchorEl?: PopperProps["anchorEl"];
}) => ReturnType;
/**
* Edit an existing link in the bubble menu, to be used when currently
* viewing a link in the already-opened bubble menu.
Expand All @@ -30,6 +39,7 @@ export enum LinkMenuState {

export type LinkBubbleMenuHandlerStorage = {
state: LinkMenuState;
anchorEl?: PopperProps["anchorEl"];
};

const LinkBubbleMenuHandler = Extension.create<
Expand All @@ -41,13 +51,14 @@ const LinkBubbleMenuHandler = Extension.create<
addStorage() {
return {
state: LinkMenuState.HIDDEN,
anchorEl: undefined,
};
},

addCommands() {
return {
openLinkBubbleMenu:
() =>
({ anchorEl } = {}) =>
({ editor, chain, dispatch }) => {
const currentMenuState = this.storage.state;

Expand Down Expand Up @@ -84,6 +95,7 @@ const LinkBubbleMenuHandler = Extension.create<
// this happens automatically for the Tiptap built-in commands
// called with `chain()` above.
this.storage.state = newMenuState;
this.storage.anchorEl = anchorEl;
}

return true;
Expand Down

0 comments on commit 597163f

Please sign in to comment.