mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-20 11:03:40 +00:00
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:
parent
10b0d6333f
commit
542a52c510
10 changed files with 221 additions and 43 deletions
|
@ -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')}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue