Merge pull request #24210 from overleaf/mj-nested-menu-bar

[web] Editor redesign: Make menu bar nestable

GitOrigin-RevId: 5c08126499ff96494d6af9adcbd75126ddd596af
This commit is contained in:
David 2025-03-12 09:33:55 +00:00 committed by Copybot
parent 10b0d6333f
commit 542a52c510
10 changed files with 221 additions and 43 deletions

View file

@ -1,6 +1,12 @@
import { DropdownDivider } from '@/features/ui/components/bootstrap-5/dropdown-menu'
import {
DropdownDivider,
DropdownHeader,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import { MenuBar } from '@/shared/components/menu-bar/menu-bar'
import { MenuBarDropdown } from '@/shared/components/menu-bar/menu-bar-dropdown'
import {
MenuBarDropdown,
NestedMenuBarDropdown,
} from '@/shared/components/menu-bar/menu-bar-dropdown'
import { MenuBarOption } from '@/shared/components/menu-bar/menu-bar-option'
import { useTranslation } from 'react-i18next'
import ChangeLayoutOptions from './change-layout-options'
@ -17,8 +23,16 @@ export const ToolbarMenuBar = () => {
id="file"
className="ide-redesign-toolbar-dropdown-toggle-subdued ide-redesign-toolbar-button-subdued"
>
<MenuBarOption title="New File" />
<MenuBarOption title="New Project" />
<MenuBarOption title="New file" />
<MenuBarOption title="New folder" />
<MenuBarOption title="Upload file" />
<DropdownDivider />
<MenuBarOption title="Show version history" />
<DropdownDivider />
<MenuBarOption title="Download as source (.zip)" />
<MenuBarOption title="Download as PDF" />
<DropdownDivider />
<MenuBarOption title="New project" />
</MenuBarDropdown>
<MenuBarDropdown
title={t('edit')}
@ -30,7 +44,11 @@ export const ToolbarMenuBar = () => {
<DropdownDivider />
<MenuBarOption title="Cut" />
<MenuBarOption title="Copy" />
<MenuBarOption title="Pate" />
<MenuBarOption title="Paste" />
<MenuBarOption title="Paste without formatting" />
<DropdownDivider />
<MenuBarOption title="Find" />
<MenuBarOption title="Select all" />
</MenuBarDropdown>
<MenuBarDropdown
title={t('view')}
@ -38,23 +56,61 @@ export const ToolbarMenuBar = () => {
className="ide-redesign-toolbar-dropdown-toggle-subdued ide-redesign-toolbar-button-subdued"
>
<ChangeLayoutOptions />
<DropdownHeader>Editor settings</DropdownHeader>
<MenuBarOption title="Show breadcrumbs" />
<MenuBarOption title="Show equation preview" />
<DropdownHeader>PDF preview</DropdownHeader>
<MenuBarOption title="Presentation mode" />
<MenuBarOption title="Zoom in" />
<MenuBarOption title="Zoom out" />
<MenuBarOption title="Fit to width" />
<MenuBarOption title="Fit to height" />
</MenuBarDropdown>
<MenuBarDropdown
title={t('insert')}
id="insert"
className="ide-redesign-toolbar-dropdown-toggle-subdued ide-redesign-toolbar-button-subdued"
>
<MenuBarOption title="Insert figure" />
<MenuBarOption title="Insert table" />
<MenuBarOption title="Insert link" />
<MenuBarOption title="Add comment" />
<NestedMenuBarDropdown title="Math" id="math">
<MenuBarOption title="Generate from text or image" />
<DropdownDivider />
<MenuBarOption title="Inline math" />
<MenuBarOption title="Display math" />
</NestedMenuBarDropdown>
<MenuBarOption title="Symbol" />
<NestedMenuBarDropdown title="Figure" id="figure">
<MenuBarOption title="Upload from computer" />
<MenuBarOption title="From project files" />
<MenuBarOption title="From another project" />
<MenuBarOption title="From URL" />
</NestedMenuBarDropdown>
<MenuBarOption title="Table" />
<MenuBarOption title="Citation" />
<MenuBarOption title="Link" />
<MenuBarOption title="Cross-reference" />
<DropdownDivider />
<MenuBarOption title="Comment" />
</MenuBarDropdown>
<MenuBarDropdown
title={t('format')}
id="format"
className="ide-redesign-toolbar-dropdown-toggle-subdued ide-redesign-toolbar-button-subdued"
>
<MenuBarOption title="Bold text" />
<MenuBarOption title="Bold" />
<MenuBarOption title="Italics" />
<DropdownDivider />
<MenuBarOption title="Bullet list" />
<MenuBarOption title="Numbered list" />
<MenuBarOption title="Increase indentation" />
<MenuBarOption title="Decrease indentation" />
<DropdownDivider />
<DropdownHeader>Paragraph styles</DropdownHeader>
<MenuBarOption title="Normal text" />
<MenuBarOption title="Section" />
<MenuBarOption title="Subsection" />
<MenuBarOption title="Subsubsection" />
<MenuBarOption title="Paragraph" />
<MenuBarOption title="Subparagraph" />
</MenuBarDropdown>
<MenuBarDropdown
title={t('help')}

View file

@ -36,6 +36,7 @@ export type DropdownItemProps = PropsWithChildren<{
href?: string
leadingIcon?: string | React.ReactNode
onClick?: React.MouseEventHandler
onMouseEnter?: React.MouseEventHandler
trailingIcon?: string | React.ReactNode
variant?: 'default' | 'danger'
className?: string

View file

@ -3,14 +3,19 @@ import {
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import { FC, useCallback } from 'react'
import { FC, forwardRef, useCallback } from 'react'
import classNames from 'classnames'
import { useMenuBar } from '@/shared/hooks/use-menu-bar'
import { useNestableDropdown } from '@/shared/hooks/use-nestable-dropdown'
import { NestableDropdownContextProvider } from '@/shared/context/nestable-dropdown-context'
import { AnchorProps } from 'react-bootstrap-5'
import MaterialIcon from '../material-icon'
import { DropdownMenuProps } from '@/features/ui/components/types/dropdown-menu-props'
type MenuBarDropdownProps = {
title: string
id: string
className?: string
align?: 'start' | 'end'
}
export const MenuBarDropdown: FC<MenuBarDropdownProps> = ({
@ -18,8 +23,9 @@ export const MenuBarDropdown: FC<MenuBarDropdownProps> = ({
children,
id,
className,
align = 'start',
}) => {
const { menuId, selected, setSelected } = useMenuBar()
const { menuId, selected, setSelected } = useNestableDropdown()
const onToggle = useCallback(
show => {
@ -38,7 +44,12 @@ export const MenuBarDropdown: FC<MenuBarDropdownProps> = ({
}, [id, setSelected])
return (
<Dropdown show={selected === id} align="start" onToggle={onToggle}>
<Dropdown
show={selected === id}
align={align}
onToggle={onToggle}
autoClose
>
<DropdownToggle
id={`${menuId}-${id}`}
variant="secondary"
@ -47,7 +58,88 @@ export const MenuBarDropdown: FC<MenuBarDropdownProps> = ({
>
{title}
</DropdownToggle>
<DropdownMenu renderOnMount>{children}</DropdownMenu>
<NestableDropdownMenu renderOnMount id={`${menuId}-${id}`}>
{children}
</NestableDropdownMenu>
</Dropdown>
)
}
const NestableDropdownMenu: FC<DropdownMenuProps & { id: string }> = ({
children,
id,
...props
}) => {
return (
<DropdownMenu {...props}>
<NestableDropdownContextProvider id={id}>
{children}
</NestableDropdownContextProvider>
</DropdownMenu>
)
}
const NestedDropdownToggle: FC = forwardRef<HTMLAnchorElement, AnchorProps>(
function NestedDropdownToggle(
{ children, className, onMouseEnter, id },
ref
) {
return (
// eslint-disable-next-line jsx-a11y/anchor-is-valid
<a
id={id}
href="#"
ref={ref}
onMouseEnter={onMouseEnter}
onClick={onMouseEnter}
className={classNames(
className,
'nested-dropdown-toggle',
'dropdown-item'
)}
>
{children}
<MaterialIcon type="chevron_right" />
</a>
)
}
)
export const NestedMenuBarDropdown: FC<{ id: string; title: string }> = ({
children,
id,
title,
}) => {
const { menuId, selected, setSelected } = useNestableDropdown()
const select = useCallback(() => {
setSelected(id)
}, [id, setSelected])
const onToggle = useCallback(
show => {
setSelected(show ? id : null)
},
[setSelected, id]
)
const active = selected === id
return (
<Dropdown
align="start"
drop="end"
show={active}
autoClose
onToggle={onToggle}
>
<DropdownToggle
id={`${menuId}-${id}`}
onMouseEnter={select}
className={classNames({ 'nested-dropdown-toggle-shown': active })}
as={NestedDropdownToggle}
>
{title}
</DropdownToggle>
<NestableDropdownMenu renderOnMount id={`${menuId}-${id}`}>
{children}
</NestableDropdownMenu>
</Dropdown>
)
}

View file

@ -1,5 +1,6 @@
import DropdownListItem from '@/features/ui/components/bootstrap-5/dropdown-list-item'
import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
import { useNestableDropdown } from '@/shared/hooks/use-nestable-dropdown'
type MenuBarOptionProps = {
title: string
@ -7,9 +8,12 @@ type MenuBarOptionProps = {
}
export const MenuBarOption = ({ title, onClick }: MenuBarOptionProps) => {
const { setSelected } = useNestableDropdown()
return (
<DropdownListItem>
<DropdownItem onClick={onClick}>{title}</DropdownItem>
<DropdownItem onMouseEnter={() => setSelected(null)} onClick={onClick}>
{title}
</DropdownItem>
</DropdownListItem>
)
}

View file

@ -1,17 +1,16 @@
import { MenuBarContext } from '@/shared/context/menu-bar-context'
import { FC, HTMLProps, useState } from 'react'
import { NestableDropdownContextProvider } from '@/shared/context/nestable-dropdown-context'
import { FC, HTMLProps } from 'react'
export const MenuBar: FC<HTMLProps<HTMLDivElement> & { id: string }> = ({
children,
id,
...props
}) => {
const [selected, setSelected] = useState<string | null>(null)
return (
<div {...props}>
<MenuBarContext.Provider value={{ selected, setSelected, menuId: id }}>
<NestableDropdownContextProvider id={id}>
{children}
</MenuBarContext.Provider>
</NestableDropdownContextProvider>
</div>
)
}

View file

@ -1,11 +0,0 @@
import { createContext, Dispatch, SetStateAction } from 'react'
export type MenuBarContextType = {
selected: string | null
setSelected: Dispatch<SetStateAction<string | null>>
menuId: string
}
export const MenuBarContext = createContext<MenuBarContextType | undefined>(
undefined
)

View file

@ -0,0 +1,25 @@
import { createContext, Dispatch, FC, SetStateAction, useState } from 'react'
export type NestableDropdownContextType = {
selected: string | null
setSelected: Dispatch<SetStateAction<string | null>>
menuId: string
}
export const NestableDropdownContext = createContext<
NestableDropdownContextType | undefined
>(undefined)
export const NestableDropdownContextProvider: FC<{ id: string }> = ({
id,
children,
}) => {
const [selected, setSelected] = useState<string | null>(null)
return (
<NestableDropdownContext.Provider
value={{ selected, setSelected, menuId: id }}
>
{children}
</NestableDropdownContext.Provider>
)
}

View file

@ -1,10 +0,0 @@
import { MenuBarContext } from '@/shared/context/menu-bar-context'
import { useContext } from 'react'
export const useMenuBar = () => {
const context = useContext(MenuBarContext)
if (context === undefined) {
throw new Error('useMenuBarContext must be used within a MenuBarContext')
}
return context
}

View file

@ -0,0 +1,12 @@
import { NestableDropdownContext } from '@/shared/context/nestable-dropdown-context'
import { useContext } from 'react'
export const useNestableDropdown = () => {
const context = useContext(NestableDropdownContext)
if (context === undefined) {
throw new Error(
'useNestableDropdown must be used within a NestableDropdownContextProvider'
)
}
return context
}

View file

@ -72,7 +72,8 @@ $dropdown-item-min-height: 36px;
}
&:hover:not(.active),
&:focus:not(.active) {
&:focus:not(.active),
&.nested-dropdown-toggle-shown {
background-color: var(--bg-light-secondary);
cursor: pointer;
text-decoration: none;
@ -211,3 +212,12 @@ $dropdown-item-min-height: 36px;
text-align: center;
}
}
.nested-dropdown-toggle {
&::after {
content: none !important;
}
display: flex;
justify-content: space-between;
}