Skip to content

Commit

Permalink
implement group menu as a submenu of node bubble menu
Browse files Browse the repository at this point in the history
  • Loading branch information
buckhalt committed Nov 19, 2024
1 parent 7f32037 commit 7bc01c6
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 111 deletions.
4 changes: 2 additions & 2 deletions components/DropdownMenu.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const Default: StoryFn = () => (
Choose an Option
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.DropdownMenuContent>
<DropdownMenu.Content>
<DropdownMenu.Label>Input Control</DropdownMenu.Label>
<DropdownMenu.RadioGroup>
<DropdownMenu.Item
Expand All @@ -36,6 +36,6 @@ export const Default: StoryFn = () => (
Toggle Button Group
</DropdownMenu.Item>
</DropdownMenu.RadioGroup>
</DropdownMenu.DropdownMenuContent>
</DropdownMenu.Content>
</DropdownMenu.Root>
);
12 changes: 8 additions & 4 deletions components/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,21 @@ const DropdownTrigger = ({ children }: PropsWithChildren) => (
</DropdownMenuPrimitive.Trigger>
);

const DropdownMenuContent = ({ children }: PropsWithChildren) => {
// defer getting portal container until the component mounts
const DropdownMenuContent = ({
children,
side = 'bottom',
}: PropsWithChildren<{ side?: 'top' | 'right' | 'bottom' | 'left' }>) => {
const portalContainer =
typeof window !== 'undefined'
? document.getElementById('dialog-portal')
: null;

return (
<DropdownMenuPrimitive.Portal container={portalContainer}>
<DropdownMenuPrimitive.Content
side={side} // Pass the side prop to the content
sideOffset={5}
className="z-50 rounded-small border bg-surface-0 p-2"
className="rounded-small border bg-surface-0 p-2"
>
{children}
</DropdownMenuPrimitive.Content>
Expand Down Expand Up @@ -59,7 +63,7 @@ const DropdownMenu = {
Root: DropdownMenuPrimitive.Root,
RadioGroup: DropdownMenuPrimitive.RadioGroup,
Trigger: DropdownTrigger,
DropdownMenuContent: DropdownMenuContent,
Content: DropdownMenuContent,
Label: DropdownLabel,
Item: DropdownItem,
};
Expand Down
1 change: 1 addition & 0 deletions components/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const ToolbarMenu = {
Button: ToolbarButton,
ToggleGroup: ToolbarToggleGroup,
ToggleItem: ToolbarToggleItem,
Separator: ToolbarPrimitive.Separator,
};

export default ToolbarMenu;
2 changes: 0 additions & 2 deletions components/block-editor/BlockEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import { EditorContent } from '@tiptap/react';
import { useRef } from 'react';
import GroupMenu from '~/lib/block-editor/extensions/Group/GroupMenu';
import { NodeBubbleMenu } from '~/lib/block-editor/extensions/NodeBubbleMenu';
import { useBlockEditor } from '~/lib/block-editor/useBlockEditor';
import SidePanel from './SidePanel';
Expand All @@ -19,7 +18,6 @@ const BlockEditor = () => {
<SidePanel />
<EditorContent editor={editor} />
<NodeBubbleMenu editor={editor} />
<GroupMenu editor={editor} appendTo={menuContainerRef} />
</div>
);
};
Expand Down
172 changes: 72 additions & 100 deletions lib/block-editor/extensions/Group/GroupMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,22 @@
import { useEditorState, type Editor } from '@tiptap/react';
import { ChevronDown, CircleAlert, Trash } from 'lucide-react';
import { useCallback } from 'react';
import { sticky } from 'tippy.js';
import {
CircleAlert,
Columns2,
Columns3,
Columns4,
Group,
RectangleVertical,
} from 'lucide-react';
import DropdownMenu from '~/components/DropdownMenu';
import Toolbar from '~/components/Toolbar';
import BubbleMenu from '~/components/block-editor/BubbleMenu';
import getRenderContainer from '../../utils';
import { toggleGroupRequired } from './utils';

type GroupEditorState = {
columns: number;
groupRequired: boolean;
};

export default function GroupMenu({
editor,
appendTo,
}: {
editor: Editor | null;
appendTo: React.RefObject<HTMLDivElement>;
}) {
const shouldShow = useCallback(() => {
const isGroup = editor?.isActive('group');
return !!isGroup;
}, [editor]);

const getReferenceClientRect = useCallback(() => {
if (!editor) return new DOMRect(-1000, -1000, 0, 0);
const renderContainer = getRenderContainer(editor, 'group');

const rect =
renderContainer?.getBoundingClientRect() ??
new DOMRect(-1000, -1000, 0, 0);

return rect;
}, [editor]);

export default function GroupMenu({ editor }: { editor: Editor | null }) {
const { columns, groupRequired } = useEditorState({
editor,
selector: (ctx) => {
Expand All @@ -52,79 +33,70 @@ export default function GroupMenu({
}

return (
<BubbleMenu
editor={editor}
shouldShow={shouldShow}
tippyOptions={{
offset: [0, 20],
popperOptions: {
modifiers: [{ name: 'flip', enabled: false }],
},
plugins: [sticky],
getReferenceClientRect,
appendTo: () => appendTo?.current ?? document.body,
}}
>
<Toolbar.Root>
<Toolbar.Button
onClick={() => {
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Group />
</DropdownMenu.Trigger>

<DropdownMenu.Content side="top">
<DropdownMenu.Label>Group Settings</DropdownMenu.Label>

<DropdownMenu.Item
onSelect={() => {
editor?.commands.deleteNode('group');
}}
textValue="Delete Group"
>
<Trash className="h-4 w-4" />
</Toolbar.Button>
<DropdownMenu.Root>
<DropdownMenu.DropdownMenuContent>
<DropdownMenu.RadioGroup>
<DropdownMenu.Label>Select number of columns</DropdownMenu.Label>
<DropdownMenu.Item
active={columns === 1}
textValue="1"
onSelect={() =>
editor?.commands.updateAttributes('group', { columns: 1 })
}
>
1
</DropdownMenu.Item>
<DropdownMenu.Item
active={columns === 2}
textValue="2"
onSelect={() =>
editor?.commands.updateAttributes('group', { columns: 2 })
}
>
2
</DropdownMenu.Item>
<DropdownMenu.Item
active={columns === 3}
textValue="3"
onSelect={() =>
editor?.commands.updateAttributes('group', { columns: 3 })
}
>
3
</DropdownMenu.Item>
<DropdownMenu.Item
active={columns === 4}
textValue="4"
onSelect={() =>
editor?.commands.updateAttributes('group', { columns: 4 })
}
>
4
</DropdownMenu.Item>
</DropdownMenu.RadioGroup>
</DropdownMenu.DropdownMenuContent>
<Toolbar.Button>
<DropdownMenu.Trigger>
<div className="flex items-center gap-1">
{columns} Columns
<ChevronDown className="h-4 w-4" />
</div>
</DropdownMenu.Trigger>
</Toolbar.Button>
</DropdownMenu.Root>
{/* required toggle */}
Delete Group
</DropdownMenu.Item>
<Toolbar.ToggleGroup type="multiple">
<Toolbar.ToggleItem
active={columns === 1}
value="1"
onClick={() => {
editor?.commands.updateAttributes('group', {
columns: 1,
});
}}
>
<RectangleVertical className="h-4 w-4" />
</Toolbar.ToggleItem>
<Toolbar.ToggleItem
active={columns === 2}
value="2"
onClick={() => {
editor?.commands.updateAttributes('group', {
columns: 2,
});
}}
>
<Columns2 className="h-4 w-4" />
</Toolbar.ToggleItem>
<Toolbar.ToggleItem
active={columns === 3}
value="3"
onClick={() => {
editor?.commands.updateAttributes('group', {
columns: 3,
});
}}
>
<Columns3 className="h-4 w-4" />
</Toolbar.ToggleItem>
<Toolbar.ToggleItem
active={columns === 4}
value="4"
onClick={() => {
editor?.commands.updateAttributes('group', {
columns: 4,
});
}}
>
<Columns4 className="h-4 w-4" />
</Toolbar.ToggleItem>
</Toolbar.ToggleGroup>

{/* Required toggle */}
<Toolbar.ToggleGroup
type="single"
onValueChange={() => toggleGroupRequired(editor)}
Expand All @@ -137,7 +109,7 @@ export default function GroupMenu({
<CircleAlert className="h-4 w-4" /> Group Required
</Toolbar.ToggleItem>
</Toolbar.ToggleGroup>
</Toolbar.Root>
</BubbleMenu>
</DropdownMenu.Content>
</DropdownMenu.Root>
);
}
8 changes: 8 additions & 0 deletions lib/block-editor/extensions/NodeBubbleMenu/NodeBubbleMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import BubbleMenu from '~/components/block-editor/BubbleMenu';
import { Button } from '~/components/Button';
import Popover from '~/components/Popover';
import Toolbar from '~/components/Toolbar';
import GroupMenu from '../Group/GroupMenu';
import VariableMenu from '../Variable/VariableMenu';

export const NodeBubbleMenu = ({ editor }: { editor: Editor | null }) => {
Expand Down Expand Up @@ -34,6 +35,7 @@ export const NodeBubbleMenu = ({ editor }: { editor: Editor | null }) => {
!!editor?.isActive('control') ||
!!editor?.isActive('label') ||
!!editor?.isActive('hint'),
isGroup: !!editor?.isActive('group'),
};
},
});
Expand Down Expand Up @@ -125,6 +127,12 @@ export const NodeBubbleMenu = ({ editor }: { editor: Editor | null }) => {
</Toolbar.ToggleItem>
</Toolbar.ToggleGroup>
{editorState?.isVariable && <VariableMenu editor={editor} />}
{editorState?.isGroup && (
<>
<Toolbar.Separator className="mx-2.5 w-px bg-muted" />
<GroupMenu editor={editor} />
</>
)}
</Toolbar.Root>
</BubbleMenu>
);
Expand Down
6 changes: 3 additions & 3 deletions lib/block-editor/extensions/Variable/VariableMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default function VariableMenu({ editor }: { editor: Editor | null }) {
return (
<>
<DropdownMenu.Root>
<DropdownMenu.DropdownMenuContent>
<DropdownMenu.Content>
<DropdownMenu.RadioGroup>
<DropdownMenu.Label>Select an input control</DropdownMenu.Label>
{type === 'categorical' && (
Expand Down Expand Up @@ -92,7 +92,7 @@ export default function VariableMenu({ editor }: { editor: Editor | null }) {
</>
)}
</DropdownMenu.RadioGroup>
</DropdownMenu.DropdownMenuContent>
</DropdownMenu.Content>
<Toolbar.Button>
<DropdownMenu.Trigger>
<div className="flex items-center gap-1">
Expand Down Expand Up @@ -123,7 +123,7 @@ export default function VariableMenu({ editor }: { editor: Editor | null }) {
editor?.commands.deleteNode('variable');
}}
>
<Trash />
<Trash className="h-4 w-4" />
</Toolbar.Button>
</>
);
Expand Down

0 comments on commit 7bc01c6

Please sign in to comment.